├── bin └── .gitkeep ├── godin ├── readme.md ├── help.build.md ├── test │ └── example.odin ├── help.md ├── path.odin ├── main.odin ├── help.syntax.md ├── case.odin ├── templates │ └── class.odin.template ├── build_cmd.odin └── build_options.odin ├── examples ├── tests │ ├── bin │ │ └── .gitkeep │ ├── tests.gd.uid │ ├── tests.gdextension.uid │ ├── .gitignore │ ├── .gitattributes │ ├── tests.gdextension │ ├── tests.tscn │ ├── project.godot │ ├── tests.gd │ ├── icon.svg.import │ ├── src │ │ ├── main.odin │ │ └── test_class.odin │ └── icon.svg ├── godin-syntax │ ├── readme.md │ ├── .gitattributes │ ├── test_scene.tscn │ ├── test.gd │ ├── .gitignore │ ├── project.godot │ ├── icon.svg.import │ ├── icon.svg │ └── src │ │ └── main.odin ├── game │ ├── Math.gd.uid │ ├── game.gdextension.uid │ ├── player │ │ ├── camera.gd.uid │ │ ├── camera.gd │ │ └── player.tscn │ ├── .gitignore │ ├── csrc │ │ ├── player.h │ │ ├── lib.h │ │ ├── player.c │ │ └── lib.c │ ├── .gitattributes │ ├── dark07.png │ ├── NightSkyHDRI004_1K-HDR.exr │ ├── game.gdextension │ ├── Math.gd │ ├── src │ │ ├── math.odin │ │ ├── main.odin │ │ └── motion_test.odin │ ├── dark07.png.import │ ├── icon.svg │ ├── icon.svg.import │ ├── NightSkyHDRI004_1K-HDR.exr.import │ ├── game.tscn │ └── project.godot └── hello-gdextension │ ├── example.gd.uid │ ├── example.gdextension.uid │ ├── .gitignore │ ├── .gitattributes │ ├── plugin.cfg │ ├── example.gdextension │ ├── example.tscn │ ├── project.godot │ ├── example.gd │ ├── icon.svg.import │ ├── src │ ├── main.odin │ └── example_class.odin │ └── icon.svg ├── godin_test.bat ├── .gitattributes ├── templates ├── bindgen_view_enum.temple.twig ├── bindgen_enum.temple.twig ├── bindgen_view_bit_field.temple.twig ├── bindgen_global_enums.temple.twig ├── bindgen_view_structs.temple.twig ├── bindgen_variant_init.temple.twig ├── bindgen_native_structs.temple.twig ├── bindgen_init_editor.temple.twig ├── bindgen_init_core.temple.twig ├── bindgen_utility_functions.temple.twig ├── bindgen_view_core.temple.twig ├── bindgen_class.temple.twig ├── bindgen_view_engine.temple.twig └── bindgen_view_variant.temple.twig ├── .gitmodules ├── odinfmt.json ├── .gitignore ├── ols.json ├── bindgen ├── systeminfo_windows.odin ├── temple.odin ├── views │ ├── views.odin │ ├── native_structs.odin │ ├── engine_packages.odin │ └── engine_class.odin ├── systeminfo_linux.odin ├── main.odin ├── names │ ├── test_names.odin │ └── names.odin ├── gen.odin └── graph │ └── api.odin ├── libgd ├── input.odin └── classdb │ └── bind.odin ├── todo ├── gdext ├── common.odin └── context.odin ├── godot └── Strings.odin ├── readme.md └── Makefile /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /godin/readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /godin/help.build.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tests/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/godin-syntax/readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/game/Math.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cmbyx1g0f6boh 2 | -------------------------------------------------------------------------------- /examples/tests/tests.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c37bpcempd0hs 2 | -------------------------------------------------------------------------------- /examples/game/game.gdextension.uid: -------------------------------------------------------------------------------- 1 | uid://c700fw5vott84 2 | -------------------------------------------------------------------------------- /examples/game/player/camera.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bdgtgv5plcpgc 2 | -------------------------------------------------------------------------------- /examples/tests/tests.gdextension.uid: -------------------------------------------------------------------------------- 1 | uid://d3w8sytwhmeqn 2 | -------------------------------------------------------------------------------- /examples/hello-gdextension/example.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bhax7v8dx40py 2 | -------------------------------------------------------------------------------- /examples/hello-gdextension/example.gdextension.uid: -------------------------------------------------------------------------------- 1 | uid://dvhde11mv0v2s 2 | -------------------------------------------------------------------------------- /examples/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | !bin/ -------------------------------------------------------------------------------- /godin_test.bat: -------------------------------------------------------------------------------- 1 | odin run ./godin -out:godin/godin.exe -debug -- build godin/test 2 | -------------------------------------------------------------------------------- /examples/game/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | /android/ 4 | 5 | !bin/ -------------------------------------------------------------------------------- /examples/hello-gdextension/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | !bin/ -------------------------------------------------------------------------------- /examples/game/csrc/player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "lib.h" 4 | 5 | void player_class_register(); 6 | -------------------------------------------------------------------------------- /examples/game/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /examples/tests/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /examples/godin-syntax/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /examples/hello-gdextension/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rdbg filter=lfs diff=lfs merge=lfs -text 2 | *.exr filter=lfs diff=lfs merge=lfs -text 3 | *.png filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /examples/game/dark07.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5549f24c2758e5c151e386b21ae5bf547d045b796f69551c82e9ccc5974e22cc 3 | size 2743 4 | -------------------------------------------------------------------------------- /examples/hello-gdextension/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Example Extension" 4 | description="example" 5 | author="author" 6 | version="0.1.0" 7 | script="example.gd" -------------------------------------------------------------------------------- /templates/bindgen_view_enum.temple.twig: -------------------------------------------------------------------------------- 1 | {{ this.name }} :: enum { 2 | {% for value in this.values %} 3 | {{ value.name }} = {{ value.value }}, 4 | {% end %} 5 | } 6 | -------------------------------------------------------------------------------- /templates/bindgen_enum.temple.twig: -------------------------------------------------------------------------------- 1 | {{ this.odin_name }} :: enum { 2 | {% for value in this.odin_values %} 3 | {{ value.name }} = {{ int(value.value) }}, 4 | {% end %} 5 | } -------------------------------------------------------------------------------- /templates/bindgen_view_bit_field.temple.twig: -------------------------------------------------------------------------------- 1 | {{ this.name }} :: enum { 2 | {% for value in this.values %} 3 | {{ value.name }} = {{ value.value }}, 4 | {% end %} 5 | } 6 | -------------------------------------------------------------------------------- /examples/game/NightSkyHDRI004_1K-HDR.exr: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:29e3eb7d7b5298bd5d2f43880be3f890609ae44a9a7e92962c38a73739391e4a 3 | size 1692023 4 | -------------------------------------------------------------------------------- /examples/game/game.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | 3 | entry_symbol = "game_init" 4 | compatibility_minimum = "4.3" 5 | 6 | [libraries] 7 | 8 | windows.x86_64="res://bin/game.dll" 9 | -------------------------------------------------------------------------------- /examples/tests/tests.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | 3 | entry_symbol = "tests_library_init" 4 | compatibility_minimum = "4.2" 5 | 6 | [libraries] 7 | 8 | windows.x86_64="res://bin/tests.dll" -------------------------------------------------------------------------------- /godin/test/example.odin: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | //+class Player extends Node2D 4 | Player :: struct { 5 | health: i64, 6 | } 7 | 8 | player_kill :: proc(self: ^Player) { 9 | self.health = 0 10 | } 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "godot-cpp"] 2 | path = godot-cpp 3 | url = https://github.com/godotengine/godot-cpp 4 | branch = 4.5 5 | [submodule "temple"] 6 | path = temple 7 | url = https://github.com/dresswithpockets/temple 8 | -------------------------------------------------------------------------------- /examples/hello-gdextension/example.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | 3 | entry_symbol = "example_library_init" 4 | compatibility_minimum = "4.5" 5 | 6 | [libraries] 7 | windows.x86_64 = "bin/example.dll" 8 | linuxbsd.x86_64 = "bin/example.so" -------------------------------------------------------------------------------- /examples/godin-syntax/test_scene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://vmb1knu7v2dm"] 2 | 3 | [ext_resource type="Script" path="res://test.gd" id="1_qi6ng"] 4 | 5 | [node name="TestScene" type="Node2D"] 6 | script = ExtResource("1_qi6ng") 7 | -------------------------------------------------------------------------------- /examples/tests/tests.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://c0iicqjjkpesc"] 2 | 3 | [ext_resource type="Script" uid="uid://c37bpcempd0hs" path="res://tests.gd" id="1_4j8f0"] 4 | 5 | [node name="Tests" type="Node"] 6 | script = ExtResource("1_4j8f0") 7 | -------------------------------------------------------------------------------- /examples/godin-syntax/test.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var examples: Array[ExampleClass] = [] 4 | 5 | func _init(): 6 | for i in range(100): 7 | examples.append(ExampleClass.new()) 8 | 9 | func _exit_tree(): 10 | for e in examples: 11 | e.free() 12 | -------------------------------------------------------------------------------- /examples/godin-syntax/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /examples/hello-gdextension/example.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://q7srf6ao7jqj"] 2 | 3 | [ext_resource type="Script" uid="uid://bhax7v8dx40py" path="res://example.gd" id="1_2jas5"] 4 | 5 | [node name="Node2D" type="Node2D"] 6 | script = ExtResource("1_2jas5") 7 | 8 | [node name="ExampleClass" type="ExampleClass" parent="."] 9 | -------------------------------------------------------------------------------- /templates/bindgen_global_enums.temple.twig: -------------------------------------------------------------------------------- 1 | /* This file is generated by godin bindgen - DO NOT EDIT! */ 2 | package core 3 | 4 | {% for global_enum_type in this.global_enums %} 5 | 6 | {% if !global_enum_type.odin_skip %} 7 | {% embed "bindgen_enum.temple.twig" with global_enum_type.derived.(StateEnum) %} 8 | {% end %} 9 | 10 | {% end %} 11 | -------------------------------------------------------------------------------- /examples/game/player/camera.gd: -------------------------------------------------------------------------------- 1 | extends Camera3D 2 | 3 | func _on_player_stepped_up(distance: float) -> void: 4 | position.y -= distance 5 | 6 | func _on_player_stepped_down(distance: float) -> void: 7 | position.y += distance 8 | 9 | func _process(delta: float) -> void: 10 | position.y = Math.exp_decay_f(position.y, 0, 20, delta) 11 | -------------------------------------------------------------------------------- /odinfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json", 3 | "character_width": 120, 4 | "tabs": false, 5 | "tabs_width": 4, 6 | "spaces": 4, 7 | "newline_limit": 1, 8 | "brace_style": "_1TBS", 9 | "newline_style": "LF", 10 | "sort_imports": true 11 | } 12 | -------------------------------------------------------------------------------- /templates/bindgen_view_structs.temple.twig: -------------------------------------------------------------------------------- 1 | package godot 2 | 3 | {% for name, import_ in this.imports %} 4 | import {{ name }} "{{ import_.path }}" 5 | {% end %} 6 | 7 | {% for struct_ in this.structs %} 8 | {{ struct_.name }} :: struct { 9 | {% for field in struct_.fields %} 10 | {{ field.name }}: {{ field.type }}, 11 | {% end %} 12 | } 13 | 14 | {% end %} 15 | -------------------------------------------------------------------------------- /templates/bindgen_variant_init.temple.twig: -------------------------------------------------------------------------------- 1 | package variant 2 | 3 | init :: proc "contextless" () { 4 | __Object_init() 5 | {% for builtin_class in this.builtin_classes %}{% if !builtin_class.odin_skip %} 6 | __{{ builtin_class.odin_type }}_init() 7 | {% end %}{% end %} 8 | 9 | EmptyString = new_string_cstring("") 10 | EmptyStringName = new_string_name_cstring("", true) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | *.gen.odin 4 | !libgd/classdb/bind.gen.odin 5 | bindgen/templates.odin 6 | 7 | bin/ 8 | !bin/.gitkeep 9 | tools/ 10 | 11 | *.exp 12 | *.dll 13 | *.exe 14 | *.so 15 | *.a 16 | *.o 17 | *.lib 18 | *.pdb 19 | *.ilk 20 | *.obj 21 | 22 | # remedybg session 23 | *.rdbg 24 | 25 | # local tools used for dev and debugging 26 | vendor_godot/ 27 | vendor_remedy/ 28 | 29 | # some other local workspace things 30 | *.ipynb 31 | *.code-workspace 32 | -------------------------------------------------------------------------------- /godin/help.md: -------------------------------------------------------------------------------- 1 | godin is a tool to aid Godot dev with Odin 2 | Usage: 3 | godin [args...] 4 | 5 | Commands: 6 | build - process Odin files with Godin syntax 7 | 8 | Topics: 9 | syntax - special syntax for describing classdb objects 10 | 11 | For more information about a command or a topic, invoke help command: 12 | e.g. `godin help build` or `godin help syntax` 13 | 14 | You may also just invoke the topic on its own to see its help: 15 | e.g. `godin syntax` 16 | -------------------------------------------------------------------------------- /examples/tests/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Example" 14 | config/features=PackedStringArray("4.4", "GL Compatibility") 15 | config/icon="res://icon.svg" 16 | 17 | [rendering] 18 | 19 | renderer/rendering_method="gl_compatibility" 20 | renderer/rendering_method.mobile="gl_compatibility" 21 | -------------------------------------------------------------------------------- /examples/godin-syntax/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Example" 14 | config/features=PackedStringArray("4.0", "GL Compatibility") 15 | config/icon="res://icon.svg" 16 | 17 | [rendering] 18 | 19 | renderer/rendering_method="gl_compatibility" 20 | renderer/rendering_method.mobile="gl_compatibility" 21 | -------------------------------------------------------------------------------- /examples/hello-gdextension/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Example" 14 | run/main_scene="uid://q7srf6ao7jqj" 15 | config/features=PackedStringArray("4.5", "GL Compatibility") 16 | config/icon="res://icon.svg" 17 | 18 | [rendering] 19 | 20 | renderer/rendering_method="gl_compatibility" 21 | renderer/rendering_method.mobile="gl_compatibility" 22 | -------------------------------------------------------------------------------- /examples/game/Math.gd: -------------------------------------------------------------------------------- 1 | class_name Math 2 | 3 | const GodotToTb: float = 32.0 4 | const TbToGodot := 1.0 / GodotToTb 5 | 6 | static func exp_decay(a, b, decay: float, delta: float): 7 | return b + (a - b) * exp(-decay * delta) 8 | 9 | static func exp_decay_f(a: float, b: float, decay: float, delta: float) -> float: 10 | return b + (a - b) * exp(-decay * delta) 11 | 12 | static func exp_decay_v2(a: Vector2, b: Vector2, decay: float, delta: float) -> Vector2: 13 | return b + (a - b) * exp(-decay * delta) 14 | 15 | static func exp_decay_v3(a: Vector3, b: Vector3, decay: float, delta: float) -> Vector3: 16 | return b + (a - b) * exp(-decay * delta) 17 | -------------------------------------------------------------------------------- /examples/hello-gdextension/example.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var thing: ExampleClass = $ExampleClass 4 | 5 | func _ready(): 6 | print("speed: ", thing.get_speed()) 7 | thing.set_speed(2.0) 8 | print("speed: ", thing.speed) 9 | thing.speed = 3.0 10 | print("speed: ", thing.speed) 11 | 12 | print("amplitude: ", thing.get_amplitude()) 13 | thing.set_amplitude(2.0) 14 | print("amplitude: ", thing.amplitude) 15 | thing.amplitude = 3.0 16 | print("amplitude: ", thing.amplitude) 17 | 18 | thing.time_passed.connect(on_time_passed) 19 | 20 | func on_time_passed(time_passed: float): 21 | print(time_passed) 22 | thing.queue_free() 23 | thing = null 24 | -------------------------------------------------------------------------------- /examples/game/src/math.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "godot:godot" 4 | import "core:math" 5 | import "core:math/linalg" 6 | 7 | exp_decay :: proc "contextless" (a, b: $T, decay, delta: $F) -> T { 8 | return b + (a - b) * math.exp(-decay * delta) 9 | } 10 | 11 | CMP_EPSILON :: 0.00001 12 | 13 | is_zero_approx :: proc "contextless" (a: godot.Vector3) -> bool { 14 | return abs(a.x) < CMP_EPSILON && abs(a.y) < CMP_EPSILON && abs(a.z) < CMP_EPSILON 15 | } 16 | 17 | limit_length :: proc "contextless" (v: godot.Vector3, max_length: godot.Float) -> (r: godot.Vector3) { 18 | r = v 19 | length := linalg.length(v) 20 | if length > 0 && godot.Real(max_length) < length { 21 | r /= length 22 | r *= godot.Real(max_length) 23 | } 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /templates/bindgen_native_structs.temple.twig: -------------------------------------------------------------------------------- 1 | /* This file is generated by godin bindgen - DO NOT EDIT! */ 2 | package core 3 | 4 | import __bindgen_var "../variant" 5 | 6 | {% for type in this.native_structs %} 7 | {{ type.derived.(StateNativeStructure).odin_name }} :: struct { 8 | {% for field in type.derived.(StateNativeStructure).fields %} 9 | {{ field.name }}: {{ field.array_specifier }}{{ bindgen_class_reference_type(field.type) }},{% end %} 10 | } 11 | {% if type.derived.(StateNativeStructure).has_defaults %} 12 | {{ type.snake_type }}_init :: proc(self: ^{{ type.derived.(StateNativeStructure).odin_name }}) { 13 | {% for field in type.derived.(StateNativeStructure).fields %}{% if field.default != "" %} 14 | self.{{ field.name }} = {{ field.default }}{% end %}{% end %} 15 | } 16 | {% end %}{% end %} -------------------------------------------------------------------------------- /examples/game/player/player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://c186aybk0cgr4"] 2 | 3 | [ext_resource type="Script" uid="uid://bdgtgv5plcpgc" path="res://player/camera.gd" id="1_1ew6o"] 4 | 5 | [sub_resource type="CylinderShape3D" id="CylinderShape3D_42akx"] 6 | margin = 0.005 7 | height = 1.75 8 | 9 | [node name="Player" type="Player"] 10 | 11 | [node name="PlayerShape" type="CollisionShape3D" parent="."] 12 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.875, 0) 13 | shape = SubResource("CylinderShape3D_42akx") 14 | 15 | [node name="CameraYaw" type="Node3D" parent="."] 16 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.65, 0) 17 | 18 | [node name="Camera" type="Camera3D" parent="CameraYaw"] 19 | script = ExtResource("1_1ew6o") 20 | 21 | [node name="MouseMove" type="Node3D" parent="CameraYaw"] 22 | -------------------------------------------------------------------------------- /ols.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", 3 | "collections": [ 4 | { 5 | "name": "base", 6 | "path": "C:\\Odin\\base" 7 | }, 8 | { 9 | "name": "core", 10 | "path": "C:\\Odin\\core" 11 | }, 12 | { 13 | "name": "shared", 14 | "path": "C:\\Odin\\shared" 15 | }, 16 | { 17 | "name": "vendor", 18 | "path": "C:\\Odin\\vendor" 19 | }, 20 | { 21 | "name": "godot", 22 | "path": "." 23 | } 24 | ], 25 | "enable_document_symbols": true, 26 | "enable_semantic_tokens": true, 27 | "enable_hover": true, 28 | "enable_snippets": true, 29 | "checker_args": "-vet -strict-style -no-entry-point -define:REAL_PRECISION=single" 30 | } 31 | -------------------------------------------------------------------------------- /examples/tests/tests.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func _ready(): 4 | var test = Test.new() 5 | 6 | var src = Vector2(7, 13) 7 | var value1 = Vector2(7, 13) 8 | var value2 = Vector2(1, 2) 9 | 10 | test.set_vector(src) 11 | assert(test.vector.x == src.x) 12 | assert(test.vector.y == src.y) 13 | assert(test.vector == src) 14 | assert(test.vector_eq(value1) == (src == value1)) 15 | assert(test.vector_eq(value2) == (src == value2)) 16 | assert(test.vector_neq(value1) == (src != value1)) 17 | assert(test.vector_neq(value2) == (src != value2)) 18 | var result := test.vector_add(value1) 19 | assert(result == (src + value1), "expected: %v, result: %v" % [(src + value1), result]) 20 | assert(test.vector_add(value2) == (src + value2)) 21 | assert(test.vector_sub(value1) == (src - value1)) 22 | assert(test.vector_sub(value2) == (src - value2)) 23 | 24 | test.free() 25 | 26 | get_tree().quit(0) 27 | -------------------------------------------------------------------------------- /bindgen/systeminfo_windows.odin: -------------------------------------------------------------------------------- 1 | #+build windows 2 | package bindgen 3 | 4 | import "core:sys/windows" 5 | 6 | num_processors :: proc() -> int { 7 | system_info: windows.SYSTEM_INFO 8 | windows.GetSystemInfo(&system_info) 9 | return cast(int)system_info.dwNumberOfProcessors 10 | } 11 | 12 | /* 13 | Copyright 2025 Dresses Digital 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | */ 27 | -------------------------------------------------------------------------------- /examples/game/dark07.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://y8i2l66yfyjs" 6 | path.s3tc="res://.godot/imported/dark07.png-e0a0160952c7fd7fd9028ceab9727188.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://dark07.png" 15 | dest_files=["res://.godot/imported/dark07.png-e0a0160952c7fd7fd9028ceab9727188.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /examples/game/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/game/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://xhu0rbyrjlwv" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /examples/tests/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b6h4knfwyaqbx" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /examples/godin-syntax/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b6h4knfwyaqbx" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /examples/game/NightSkyHDRI004_1K-HDR.exr.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cvjfr4aynhwj1" 6 | path.bptc="res://.godot/imported/NightSkyHDRI004_1K-HDR.exr-5f29fc43b7689f1dc1e053038523056b.bptc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://NightSkyHDRI004_1K-HDR.exr" 15 | dest_files=["res://.godot/imported/NightSkyHDRI004_1K-HDR.exr-5f29fc43b7689f1dc1e053038523056b.bptc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /examples/tests/src/main.odin: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "godot:gdext" 4 | 5 | // see gdext.InitializationFunction 6 | @(export) 7 | tests_library_init :: proc "c" ( 8 | get_proc_address: gdext.ExtensionInterfaceGetProcAddress, 9 | library: gdext.ExtensionClassLibraryPtr, 10 | initialization: ^gdext.Initialization, 11 | ) -> bool { 12 | gdext.init(library, get_proc_address) 13 | 14 | initialization.initialize = init_module 15 | initialization.deinitialize = uninit_module 16 | initialization.user_data = nil 17 | initialization.minimum_initialization_level = .Scene 18 | 19 | return true 20 | } 21 | 22 | init_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 23 | context = gdext.godot_context() 24 | 25 | if level != .Scene { 26 | return 27 | } 28 | 29 | test_class_register() 30 | } 31 | 32 | uninit_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 33 | context = gdext.godot_context() 34 | 35 | if level != .Scene { 36 | return 37 | } 38 | } -------------------------------------------------------------------------------- /examples/game/src/main.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "godot:godot" 4 | import "godot:gdext" 5 | 6 | @(export) 7 | game_init :: proc "c" ( 8 | get_proc_address: gdext.ExtensionInterfaceGetProcAddress, 9 | library: gdext.ExtensionClassLibraryPtr, 10 | initialization: ^gdext.Initialization, 11 | ) -> bool { 12 | gdext.init(library, get_proc_address) 13 | godot.init() 14 | 15 | initialization.initialize = initialize_game_module 16 | initialization.deinitialize = uninitialize_game_module 17 | initialization.user_data = nil 18 | initialization.minimum_initialization_level = .Core 19 | 20 | return true 21 | } 22 | 23 | initialize_game_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 24 | if level != .Scene { 25 | return 26 | } 27 | 28 | context = gdext.godot_context() 29 | 30 | player_class_register() 31 | } 32 | 33 | uninitialize_game_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 34 | if level != .Scene { 35 | return 36 | } 37 | 38 | player_class_unregister() 39 | } 40 | -------------------------------------------------------------------------------- /examples/hello-gdextension/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b6h4knfwyaqbx" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/uastc_level=0 22 | compress/rdo_quality_loss=0.0 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=false 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/channel_remap/red=0 31 | process/channel_remap/green=1 32 | process/channel_remap/blue=2 33 | process/channel_remap/alpha=3 34 | process/fix_alpha_border=true 35 | process/premult_alpha=false 36 | process/normal_map_invert_y=false 37 | process/hdr_as_srgb=false 38 | process/hdr_clamp_exposure=false 39 | process/size_limit=0 40 | detect_3d/compress_to=1 41 | svg/scale=1.0 42 | editor/scale_with_editor_scale=false 43 | editor/convert_colors_with_editor_theme=false 44 | -------------------------------------------------------------------------------- /godin/path.odin: -------------------------------------------------------------------------------- 1 | #+private 2 | package godin 3 | 4 | // taken from core:os 5 | 6 | is_path_separator :: proc(c: byte) -> bool { 7 | return c == '/' || c == '\\' 8 | } 9 | 10 | is_abs_path :: proc(path: string) -> bool { 11 | if len(path) > 0 && path[0] == '/' { 12 | return true 13 | } 14 | when ODIN_OS == .Windows { 15 | if len(path) > 2 { 16 | switch path[0] { 17 | case 'A' ..= 'Z', 'a' ..= 'z': 18 | return path[1] == ':' && is_path_separator(path[2]) 19 | } 20 | } 21 | } 22 | return false 23 | } 24 | 25 | /* 26 | Copyright 2025 Dresses Digital 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | */ 40 | -------------------------------------------------------------------------------- /examples/hello-gdextension/src/main.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "godot:godot" 4 | import "godot:gdext" 5 | 6 | // see gdext.InitializationFunction 7 | @(export) 8 | example_library_init :: proc "c" ( 9 | get_proc_address: gdext.ExtensionInterfaceGetProcAddress, 10 | library: gdext.ExtensionClassLibraryPtr, 11 | initialization: ^gdext.Initialization, 12 | ) -> bool { 13 | // gdext procs MUST be initialized before using the binding! 14 | gdext.init(library, get_proc_address) 15 | 16 | // MUST be called before using any core classes, singletons, or utility functions 17 | godot.init() 18 | 19 | initialization.initialize = initialize_example_module 20 | initialization.deinitialize = uninitialize_example_module 21 | initialization.user_data = nil 22 | initialization.minimum_initialization_level = .Scene 23 | 24 | return true 25 | } 26 | 27 | initialize_example_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 28 | context = gdext.godot_context() 29 | 30 | if level != .Scene { 31 | return 32 | } 33 | 34 | example_class_register() 35 | } 36 | 37 | uninitialize_example_module :: proc "c" (user_data: rawptr, level: gdext.InitializationLevel) { 38 | context = gdext.godot_context() 39 | 40 | if level != .Scene { 41 | return 42 | } 43 | } -------------------------------------------------------------------------------- /bindgen/temple.odin: -------------------------------------------------------------------------------- 1 | package bindgen 2 | 3 | import "core:fmt" 4 | import "core:slice" 5 | import "core:strings" 6 | import "views" 7 | 8 | import temple "../temple" 9 | 10 | variant_template: temple.Compiled(views.Variant) 11 | engine_class_template: temple.Compiled(views.Engine_Class) 12 | core_template: temple.Compiled(views.Godot_Package) 13 | structs_template: temple.Compiled(views.Structs) 14 | 15 | init_templates :: proc() { 16 | variant_template = temple_compiled("../templates/bindgen_view_variant.temple.twig", views.Variant) 17 | engine_class_template = temple_compiled("../templates/bindgen_view_engine.temple.twig", views.Engine_Class) 18 | core_template = temple_compiled("../templates/bindgen_view_core.temple.twig", views.Godot_Package) 19 | structs_template = temple_compiled("../templates/bindgen_view_structs.temple.twig", views.Structs) 20 | } 21 | 22 | /* 23 | Copyright 2025 Dresses Digital 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | */ 37 | -------------------------------------------------------------------------------- /bindgen/views/views.odin: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | Import :: struct { 4 | name: string, 5 | path: string, 6 | } 7 | 8 | Enum :: struct { 9 | name: string, 10 | values: []Enum_Value, 11 | } 12 | 13 | Enum_Value :: struct { 14 | name: string, 15 | value: string, 16 | } 17 | 18 | Bit_Field :: struct { 19 | name: string, 20 | values: []Enum_Value, 21 | } 22 | 23 | File_Constant :: struct { 24 | name: string, 25 | type: string, 26 | value: string, 27 | } 28 | 29 | Init_Constant :: struct { 30 | name: string, 31 | type: string, 32 | constructor: string, 33 | args: []string, 34 | } 35 | 36 | Method :: struct { 37 | name: string, 38 | vararg: bool, 39 | hash: i64, 40 | return_type: Maybe(string), 41 | args: []Method_Arg, 42 | } 43 | 44 | Method_Arg :: struct { 45 | name: string, 46 | type: string, 47 | } 48 | 49 | /* 50 | Copyright 2025 Dresses Digital 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | */ 64 | -------------------------------------------------------------------------------- /libgd/input.odin: -------------------------------------------------------------------------------- 1 | package libgd 2 | 3 | import "godot:godot" 4 | 5 | get_input_vector :: proc "contextless" (negative_x: cstring, positive_x: cstring, negative_y: cstring, positive_y: cstring, static: bool) -> godot.Vector2 { 6 | negative_x_name := godot.new_string_name(negative_x, static) 7 | defer if !static { 8 | godot.free_string_name(negative_x_name) 9 | } 10 | 11 | positive_x_name := godot.new_string_name(positive_x, static) 12 | defer if !static { 13 | godot.free_string_name(positive_x_name) 14 | } 15 | 16 | negative_y_name := godot.new_string_name(negative_y, static) 17 | defer if !static { 18 | godot.free_string_name(negative_y_name) 19 | } 20 | 21 | positive_y_name := godot.new_string_name(positive_y, static) 22 | defer if !static { 23 | godot.free_string_name(positive_y_name) 24 | } 25 | 26 | return godot.input_get_vector(godot.singleton_input(), negative_x_name, positive_x_name, negative_y_name, positive_y_name, -1.0) 27 | } 28 | 29 | /* 30 | Copyright 2025 Dresses Digital 31 | 32 | Licensed under the Apache License, Version 2.0 (the "License"); 33 | you may not use this file except in compliance with the License. 34 | You may obtain a copy of the License at 35 | 36 | http://www.apache.org/licenses/LICENSE-2.0 37 | 38 | Unless required by applicable law or agreed to in writing, software 39 | distributed under the License is distributed on an "AS IS" BASIS, 40 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 41 | See the License for the specific language governing permissions and 42 | limitations under the License. 43 | */ 44 | -------------------------------------------------------------------------------- /examples/game/game.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://vetgijbipm8s"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://c186aybk0cgr4" path="res://player/player.tscn" id="1_xhqjb"] 4 | [ext_resource type="Texture2D" uid="uid://cvjfr4aynhwj1" path="res://NightSkyHDRI004_1K-HDR.exr" id="2_bn8kw"] 5 | [ext_resource type="Texture2D" uid="uid://y8i2l66yfyjs" path="res://dark07.png" id="3_raqes"] 6 | 7 | [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_gny3t"] 8 | panorama = ExtResource("2_bn8kw") 9 | 10 | [sub_resource type="Sky" id="Sky_lmidm"] 11 | sky_material = SubResource("PanoramaSkyMaterial_gny3t") 12 | 13 | [sub_resource type="Environment" id="Environment_dww2n"] 14 | background_mode = 2 15 | background_energy_multiplier = 2.6 16 | sky = SubResource("Sky_lmidm") 17 | ambient_light_source = 3 18 | ambient_light_color = Color(1, 1, 1, 1) 19 | ambient_light_sky_contribution = 0.59 20 | ambient_light_energy = 2.4 21 | 22 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ht268"] 23 | albedo_texture = ExtResource("3_raqes") 24 | 25 | [node name="Game" type="Node3D"] 26 | 27 | [node name="Player" parent="." instance=ExtResource("1_xhqjb")] 28 | ground_friction = 15.0 29 | ground_max_speed = 7.5 30 | gravity_down_scale = 2.0 31 | air_accel = 20.0 32 | air_max_speed = 7.5 33 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.389408, 0) 34 | 35 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."] 36 | environment = SubResource("Environment_dww2n") 37 | 38 | [node name="CSGBox3D" type="CSGBox3D" parent="."] 39 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0) 40 | material_override = SubResource("StandardMaterial3D_ht268") 41 | use_collision = true 42 | size = Vector3(25, 0.5, 25) 43 | -------------------------------------------------------------------------------- /examples/game/src/motion_test.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import core "../../../core" 4 | import gd "godot:gdext" 5 | import var "../../../variant" 6 | 7 | PhysicsMotion :: struct { 8 | params: core.PhysicsTestMotionParameters3d, 9 | result: core.PhysicsTestMotionResult3d, 10 | last_hit: bool, 11 | rid: var.Rid, 12 | remainder: var.Vector3, 13 | travel: var.Vector3, 14 | distance: var.Vector3, 15 | normal: var.Vector3, 16 | } 17 | 18 | new_physics_motion :: proc(rid: var.Rid, margin: gd.float) -> PhysicsMotion { 19 | mt := PhysicsMotion { 20 | params = core.new_physics_test_motion_parameters3d(), 21 | result = core.new_physics_test_motion_result3d(), 22 | last_hit = false, 23 | rid = rid, 24 | } 25 | 26 | core.physics_test_motion_parameters3d_set_margin(mt.params, margin) 27 | } 28 | 29 | physics_motion_test :: proc(motion: ^PhysicsMotion, from: var.Transform3d, move: var.Vector3) -> bool { 30 | core.physics_test_motion_parameters3d_set_from(mt.params, from) 31 | core.physics_test_motion_parameters3d_set_motion(mt.params, move) 32 | motion.last_hit = core.physics_server3d_body_test_motion( 33 | core.singleton_physics_server3d(), 34 | motion.rid, 35 | motion.params, 36 | motion.result, 37 | ) 38 | 39 | if motion.last_hit { 40 | motion.remainder = core.physics_test_motion_result3d_get_remainder(motion.result) 41 | motion.travel = core.physics_test_motion_result3d_get_travel(motion.result) 42 | motion.distance = var.vector3_length(motion.travel) 43 | motion.normal = core.physics_test_motion_result3d_get_collision_normal(motion.result, 0) 44 | } 45 | 46 | return motion.last_hit 47 | } 48 | 49 | -------------------------------------------------------------------------------- /templates/bindgen_init_editor.temple.twig: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import __bindgen_gde "godot:gdext" 4 | import __bindgen_var "../variant" 5 | 6 | init :: proc "contextless" () { 7 | {% for class in this.classes %}{% if !class.odin_skip && class.derived.(StateClass).api_type == "editor" %} 8 | __{{ class.odin_type }}_init() 9 | {% end %}{% end %} 10 | 11 | {% for singleton in this.editor_singletons %} 12 | __bindgen_gde.string_name_new_with_latin1_chars(&__{{ singleton.name }}__singleton_name, "{{ singleton.name }}", true) 13 | {% end %} 14 | } 15 | 16 | // casting 17 | class_name_of :: proc "contextless" ($C: typeid) -> __bindgen_var.StringName { 18 | switch typeid_of(C) { 19 | {% for class in this.classes %}{% if !class.odin_skip && class.derived.(StateClass).api_type == "editor" %} 20 | case typeid_of({{ class.odin_type }}): return __{{ class.odin_type }}__class_name 21 | {% end %}{% end %} 22 | } 23 | return __bindgen_var.StringName{} 24 | } 25 | 26 | @(require_results) 27 | cast_class :: proc "contextless" (object: $T, $C: typeid) -> (result: C, ok: bool) { 28 | class_name := class_name_of(C) 29 | class_tag := __bindgen_gde.classdb_get_class_tag(&class_name) 30 | result_ptr := __bindgen_gde.object_cast_to(object, class_tag) 31 | if result_ptr != nil { 32 | return (cast(C)result_ptr), true 33 | } 34 | return C{}, false 35 | } 36 | 37 | // singletons 38 | {% for singleton in this.editor_singletons %} 39 | singleton_{{ singleton.snake_name }} :: proc "contextless" () -> {{ singleton.odin_type }} { 40 | return cast({{ singleton.odin_type }})__bindgen_gde.global_get_singleton(&__{{ singleton.name }}__singleton_name) 41 | } 42 | {% end %} 43 | 44 | // singleton names 45 | {% for singleton in this.editor_singletons %} 46 | @(private="file") 47 | __{{ singleton.name }}__singleton_name: __bindgen_var.StringName 48 | {% end %} 49 | -------------------------------------------------------------------------------- /templates/bindgen_init_core.temple.twig: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import __bindgen_gde "godot:gdext" 4 | import __bindgen_var "../variant" 5 | 6 | init :: proc "contextless" () { 7 | __init_util_functions() 8 | {% for class in this.classes %}{% if !class.odin_skip && class.derived.(StateClass).api_type == "core" %} 9 | __{{ class.odin_type }}_init() 10 | {% end %}{% end %} 11 | 12 | {% for singleton in this.core_singletons %} 13 | __bindgen_gde.string_name_new_with_latin1_chars(&__{{ singleton.name }}__singleton_name, "{{ singleton.name }}", true) 14 | {% end %} 15 | } 16 | 17 | // casting 18 | class_name_of :: proc "contextless" ($C: typeid) -> __bindgen_var.StringName { 19 | switch typeid_of(C) { 20 | {% for class in this.classes %}{% if !class.odin_skip && class.derived.(StateClass).api_type == "core" %} 21 | case typeid_of({{ class.odin_type }}): return __{{ class.odin_type }}__class_name 22 | {% end %}{% end %} 23 | } 24 | return __bindgen_var.StringName{} 25 | } 26 | 27 | @(require_results) 28 | cast_class :: proc "contextless" (object: $T, $C: typeid) -> (result: C, ok: bool) { 29 | class_name := class_name_of(C) 30 | class_tag := __bindgen_gde.classdb_get_class_tag(&class_name) 31 | result_ptr := __bindgen_gde.object_cast_to(object, class_tag) 32 | if result_ptr != nil { 33 | return (cast(C)result_ptr), true 34 | } 35 | return C{}, false 36 | } 37 | 38 | // singletons 39 | {% for singleton in this.core_singletons %} 40 | singleton_{{ singleton.snake_name }} :: proc "contextless" () -> {{ singleton.odin_type }} { 41 | return cast({{ singleton.odin_type }})__bindgen_gde.global_get_singleton(&__{{ singleton.name }}__singleton_name) 42 | } 43 | {% end %} 44 | 45 | // singleton names 46 | {% for singleton in this.core_singletons %} 47 | @(private="file") 48 | __{{ singleton.name }}__singleton_name: __bindgen_var.StringName 49 | {% end %} 50 | -------------------------------------------------------------------------------- /godin/main.odin: -------------------------------------------------------------------------------- 1 | package godin 2 | 3 | import "core:fmt" 4 | import "core:os" 5 | 6 | help_base :: #load("help.md", string) 7 | help_build :: #load("help.build.md", string) 8 | help_syntax :: #load("help.syntax.md", string) 9 | 10 | help_docs := map[string]string { 11 | "build" = help_build, 12 | "syntax" = help_syntax, 13 | } 14 | 15 | main :: proc() { 16 | 17 | if len(os.args) <= 1 { 18 | fmt.println(help_base) 19 | return 20 | } 21 | 22 | command := os.args[1] 23 | switch command { 24 | case "build": 25 | cmd_build() 26 | case "syntax": 27 | fmt.println(help_syntax) 28 | case "help": 29 | { 30 | if len(os.args) == 2 { 31 | fmt.println(help_base) 32 | return 33 | } 34 | 35 | help_target := os.args[2] 36 | if help_doc, ok := help_docs[help_target]; ok { 37 | fmt.println(help_doc) 38 | return 39 | } 40 | 41 | fmt.println(help_base) 42 | } 43 | case: 44 | { 45 | if help_doc, ok := help_docs[command]; ok { 46 | fmt.println(help_doc) 47 | return 48 | } 49 | 50 | fmt.println(help_base) 51 | } 52 | } 53 | } 54 | 55 | /* 56 | Copyright 2025 Dresses Digital 57 | 58 | Licensed under the Apache License, Version 2.0 (the "License"); 59 | you may not use this file except in compliance with the License. 60 | You may obtain a copy of the License at 61 | 62 | http://www.apache.org/licenses/LICENSE-2.0 63 | 64 | Unless required by applicable law or agreed to in writing, software 65 | distributed under the License is distributed on an "AS IS" BASIS, 66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | See the License for the specific language governing permissions and 68 | limitations under the License. 69 | */ 70 | -------------------------------------------------------------------------------- /bindgen/views/native_structs.odin: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import g "../graph" 4 | import "../names" 5 | import "core:mem" 6 | import "core:strings" 7 | 8 | Structs :: struct { 9 | imports: map[string]Import, 10 | structs: []Struct, 11 | } 12 | 13 | Struct :: struct { 14 | name: string, 15 | fields: []Struct_Field, 16 | } 17 | 18 | Struct_Field :: struct { 19 | name: string, 20 | type: string, 21 | } 22 | 23 | native_structs :: proc(graph: ^g.Graph, allocator: mem.Allocator) -> (structs: Structs) { 24 | structs = Structs { 25 | structs = make([]Struct, len(graph.native_structs)), 26 | } 27 | 28 | for native_struct, struct_idx in graph.native_structs { 29 | new_struct := Struct { 30 | name = names.clone_string(native_struct.odin_name), 31 | fields = make([]Struct_Field, len(native_struct.fields)), 32 | } 33 | 34 | for field, field_idx in native_struct.fields { 35 | ensure_imports(&structs.imports, field.type, "godot:structs") 36 | new_struct.fields[field_idx] = Struct_Field { 37 | name = strings.clone(field.name), 38 | type = resolve_qualified_type(field.type, "godot:structs"), 39 | // TODO: default values 40 | } 41 | } 42 | 43 | structs.structs[struct_idx] = new_struct 44 | } 45 | 46 | return 47 | } 48 | 49 | /* 50 | Copyright 2025 Dresses Digital 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | */ 64 | -------------------------------------------------------------------------------- /examples/game/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="game" 14 | config/features=PackedStringArray("4.4", "GL Compatibility") 15 | config/icon="res://icon.svg" 16 | 17 | [input] 18 | 19 | move_forward={ 20 | "deadzone": 0.5, 21 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) 22 | ] 23 | } 24 | move_back={ 25 | "deadzone": 0.5, 26 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) 27 | ] 28 | } 29 | move_left={ 30 | "deadzone": 0.5, 31 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) 32 | ] 33 | } 34 | move_right={ 35 | "deadzone": 0.5, 36 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) 37 | ] 38 | } 39 | 40 | [rendering] 41 | 42 | renderer/rendering_method="gl_compatibility" 43 | renderer/rendering_method.mobile="gl_compatibility" 44 | -------------------------------------------------------------------------------- /templates/bindgen_utility_functions.temple.twig: -------------------------------------------------------------------------------- 1 | /* This file is generated by godin bindgen - DO NOT EDIT! */ 2 | package core 3 | 4 | import __bindgen_gde "godot:gdext" 5 | import __bindgen_variant "../variant" 6 | 7 | {% for function in this.utility_functions %} 8 | {{ function.odin_name }} :: proc "contextless" ( 9 | {% for argument, i in function.arguments %} 10 | {% if i > 0 %}, {% end %}{{argument.name}}: {% if _, is_class := argument.type.derived.(StateClass); is_class %}__bindgen_variant.{% end %}{{ argument.type.odin_type }} 11 | {% end %}) {% if return_type, has_return_type := function.return_type.(^StateType); has_return_type %} -> {% if _, is_class := return_type.derived.(StateClass); is_class %}__bindgen_variant.{% end %}{{ return_type.odin_type }}{% end %} { 12 | {% for argument in function.arguments %} 13 | {{ argument.name }} := {{ argument.name }} 14 | {% end %} 15 | {% if return_type, has_return_type := function.return_type.(^StateType); has_return_type %} 16 | return __bindgen_gde.call_utility_function_ptr_ret(__{{ function.godot_name }}__Ptr, {% if _, is_class := return_type.derived.(StateClass); is_class %}__bindgen_variant.{% end %}{{ return_type.odin_type }} 17 | {% else %} 18 | __bindgen_gde.call_utility_function_ptr_no_ret(__{{function.godot_name}}__Ptr 19 | {% end %} 20 | {% for argument in function.arguments %}, cast(__bindgen_gde.TypePtr)&{{ argument.name }}{% end %}) 21 | } 22 | 23 | {% end %} 24 | 25 | @(private) 26 | __init_util_functions :: proc "contextless" () { 27 | _gde_name: __bindgen_variant.StringName 28 | {% for function in this.utility_functions %} 29 | __bindgen_gde.string_name_new_with_latin1_chars(cast(__bindgen_gde.StringNamePtr)&_gde_name, "{{ function.godot_name }}", true) 30 | __{{ function.godot_name }}__Ptr = __bindgen_gde.variant_get_ptr_utility_function(cast(__bindgen_gde.StringNamePtr)&_gde_name, {{ i64(function.hash) }}) 31 | {% end %} 32 | } 33 | 34 | {% for function in this.utility_functions %} 35 | @(private="file") 36 | __{{ function.godot_name }}__Ptr: __bindgen_gde.PtrUtilityFunction 37 | {% end %} 38 | -------------------------------------------------------------------------------- /bindgen/systeminfo_linux.odin: -------------------------------------------------------------------------------- 1 | #+build linux 2 | package bindgen 3 | 4 | import "core:c" 5 | import "core:sys/linux" 6 | 7 | CPU_SETSIZE :: 1024 8 | CPU_BITTYPE :: c.ulong 9 | CPU_BITS :: size_of(CPU_BITTYPE) 10 | 11 | Cpu_Set :: struct { 12 | bits: [CPU_SETSIZE / CPU_BITS]CPU_BITTYPE, 13 | } 14 | 15 | errno_unwrap2 :: #force_inline proc "contextless" (ret: $P, $T: typeid) -> (T, linux.Errno) { 16 | if ret < 0 { 17 | default_value: T 18 | return default_value, linux.Errno(-ret) 19 | } else { 20 | return cast(T)ret, linux.Errno(.NONE) 21 | } 22 | } 23 | 24 | sched_getaffinity :: proc(pid: linux.Pid, cpusetsize: c.size_t, mask: ^Cpu_Set) -> (int, linux.Errno) { 25 | ret := linux.syscall(linux.SYS_sched_getaffinity, pid, cpusetsize, mask) 26 | return errno_unwrap2(ret, int) 27 | } 28 | 29 | @(private) 30 | countbits :: proc(v: CPU_BITTYPE) -> int { 31 | s := 0 32 | v := v 33 | for v != 0 { 34 | v &= v - 1 35 | s += 1 36 | } 37 | return s 38 | } 39 | 40 | @(private) 41 | sched_cpucount :: proc(setsize: c.size_t, set: ^Cpu_Set) -> int { 42 | s := 0 43 | for i in 0 ..< (setsize / CPU_BITS) { 44 | s += countbits(set.bits[i]) 45 | } 46 | return s 47 | } 48 | 49 | num_processors :: proc() -> int { 50 | cpu_set: Cpu_Set 51 | sched_getaffinity(0, size_of(Cpu_Set), &cpu_set) 52 | return sched_cpucount(CPU_SETSIZE, &cpu_set) 53 | } 54 | 55 | /* 56 | Copyright 2025 Dresses Digital 57 | 58 | Licensed under the Apache License, Version 2.0 (the "License"); 59 | you may not use this file except in compliance with the License. 60 | You may obtain a copy of the License at 61 | 62 | http://www.apache.org/licenses/LICENSE-2.0 63 | 64 | Unless required by applicable law or agreed to in writing, software 65 | distributed under the License is distributed on an "AS IS" BASIS, 66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | See the License for the specific language governing permissions and 68 | limitations under the License. 69 | */ 70 | -------------------------------------------------------------------------------- /todo: -------------------------------------------------------------------------------- 1 | # bindgen 2 | TODO generate class signal bindings; N.B. so the user can e.g. connect or emit a signal 3 | TODO create a basic demo game using just the gdextension/, core/, and variant/ packages 4 | TODO generate common global/static StringNames like _process and _ready 5 | 6 | TODO refactor multi-passes to do less work 7 | TODO generate Variant constructors that convert from another type (see get_variant_from_type_constructor) 8 | TODO generate Object in core instead of in variant/ 9 | 10 | TODO rewrite state code to pre-calculate all fields used by templates 11 | TODO add support for vararg utility functions 12 | TODO add option to generate classes and methods in a class-per-package hierarchy (each class gets its own package) 13 | 14 | DONE add init proc in editor/ 15 | DONE add generation for editor singletons 16 | DONE add init proc in core/ 17 | DONE add generation for core singletons 18 | DONE generate special constructors for String, StringName, and Variant (see gdextension/interface.odin) 19 | DONE generate class methods 20 | DONE add generation for native structures 21 | 22 | DONE rewrite codegen to use temple templating engine 23 | DONE add support for utility functions with default arg values 24 | DONE add frontend generation for class methods 25 | DONE add generation for engine (non-builtin) classes 26 | 27 | # godin 28 | TODO rewrite godin with better Odin parsing support via core:odin/parser 29 | TODO rewrite templating & codegen using the temple template engine 30 | 31 | 32 | - write libgodin 33 | ```c 34 | class_info := Class_Info{ 35 | 36 | } 37 | 38 | classdb_class := register_class(class_info) 39 | 40 | register_class_method(classdb_class, "method_name", ...details...) 41 | 42 | register_class_int_const(classdb_class, "CONST_NAME", CONST_VALUE, is_bitfield=true /*or false*/) 43 | 44 | register_class_enum_const(classdb_class, "ENUM_CONST_NAME", My_Enum.Value, is_bitfield=true /*or false*/) 45 | 46 | register_class_property(classdb_class, "property_name", VariantType.SomeType, property_getter_proc, property_setter_proc, hint = PropertyHint.None, hint_usage = "...", usage = PropertyUsageFlags.None) 47 | 48 | // todo: register_class_property_group (how does it work?!) 49 | // todo: register_class_property_subgroup (how does it work?!) 50 | 51 | register_class_signal(classdb_class, "signal_name", args) 52 | 53 | unregister_class(classdb_class) 54 | ``` 55 | 56 | - rewrite godin to use core:odin/parser -------------------------------------------------------------------------------- /examples/game/csrc/lib.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../../../godot-cpp/gdextension/gdextension_interface.h" 3 | 4 | typedef uint8_t NodePath[8]; 5 | typedef uint8_t StringName[8]; 6 | typedef uint8_t String[8]; 7 | typedef uint8_t Variant[24]; 8 | 9 | enum InitializationLevel { 10 | InitializationLevelCore, 11 | InitializationLevelServers, 12 | InitializationLevelScene, 13 | InitializationLevelEditor, 14 | InitializationLevelMax, 15 | }; 16 | 17 | typedef void* (*ExtensionInterfaceGetProcAddress)(char* function_name); 18 | typedef void (*InitializeProc)(void* user_data, GDExtensionInitializationLevel level); 19 | 20 | typedef struct { 21 | enum InitializationLevel minimum_initialization_level; 22 | void* user_data; 23 | InitializeProc initialize; 24 | InitializeProc deinitialize; 25 | } Initialization; 26 | 27 | extern GDExtensionInterfaceMemAlloc gd_alloc; 28 | extern GDExtensionInterfaceMemFree gd_free; 29 | extern GDExtensionInterfaceGetProcAddress ext_get_proc_address; 30 | extern GDExtensionClassLibraryPtr ext_library; 31 | extern GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars; 32 | extern GDExtensionInterfaceStringNewWithLatin1Chars string_new_with_latin1_chars; 33 | extern GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind; 34 | extern GDExtensionInterfaceGlobalGetSingleton global_get_singleton; 35 | extern GDExtensionInterfaceClassdbConstructObject classdb_construct_object; 36 | extern GDExtensionInterfaceObjectSetInstance object_set_instance; 37 | extern GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding; 38 | extern GDExtensionInterfaceClassdbRegisterExtensionClass3 classdb_register_extension_class3; 39 | extern GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall; 40 | 41 | // constructors 42 | extern GDExtensionPtrConstructor node_path_from_string; 43 | 44 | // hashes & method binds 45 | extern const GDExtensionInt get_node_hash; 46 | extern GDExtensionMethodBindPtr get_node_method_bind; 47 | 48 | // operators 49 | extern GDExtensionPtrOperatorEvaluator StringName_eq_StringName_op; 50 | 51 | // helpers 52 | GDExtensionBool call_builtin_op_bool(GDExtensionPtrOperatorEvaluator op, GDExtensionConstTypePtr a, GDExtensionConstTypePtr b); 53 | void print_object(GDExtensionTypePtr object); 54 | 55 | // string_name_new_with_latin1_chars = cast(ExtensionInterfaceStringNameNewWithLatin1Chars)get_proc_address("string_name_new_with_latin1_chars") 56 | -------------------------------------------------------------------------------- /gdext/common.odin: -------------------------------------------------------------------------------- 1 | package gdextension 2 | 3 | library: ExtensionClassLibraryPtr 4 | 5 | call_builtin_constructor :: proc "contextless" ( 6 | constructor: PtrConstructor, 7 | base: UninitializedTypePtr, 8 | args: ..TypePtr, 9 | ) { 10 | constructor(base, raw_data(args)) 11 | } 12 | 13 | call_builtin_operator_ptr :: proc "contextless" (op: PtrOperatorEvaluator, a, b: TypePtr, $T: typeid) -> T { 14 | ret: T 15 | op(a, b, cast(TypePtr)&ret) 16 | return ret 17 | } 18 | 19 | call_builtin_method_ptr_ret :: proc "contextless" ( 20 | method: PtrBuiltInMethod, 21 | base: TypePtr, 22 | $T: typeid, 23 | args: ..TypePtr, 24 | ) -> T { 25 | ret: T 26 | method(base, raw_data(args), cast(TypePtr)&ret, len(args)) 27 | return ret 28 | } 29 | 30 | call_builtin_method_ptr_no_ret :: proc "contextless" (method: PtrBuiltInMethod, base: TypePtr, args: ..TypePtr) { 31 | method(base, raw_data(args), cast(TypePtr)nil, len(args)) 32 | } 33 | 34 | call_method_ptr_no_ret :: proc "contextless" (method: MethodBindPtr, base: ObjectPtr, args: ..TypePtr) { 35 | object_method_bind_ptrcall(method, base, raw_data(args), cast(TypePtr)nil) 36 | } 37 | 38 | call_method_ptr_ret :: proc "contextless" (method: MethodBindPtr, $T: typeid, base: ObjectPtr, args: ..TypePtr) -> T { 39 | ret: T 40 | object_method_bind_ptrcall(method, base, raw_data(args), cast(TypePtr)&ret) 41 | return ret 42 | } 43 | 44 | call_utility_function_ptr_ret :: proc "contextless" ( 45 | func: PtrUtilityFunction, 46 | $T: typeid, 47 | args: ..TypePtr, 48 | ) -> ( 49 | ret: T, 50 | ) { 51 | func(cast(TypePtr)&ret, raw_data(args), len(args)) 52 | return 53 | } 54 | 55 | call_utility_function_ptr_no_ret :: proc "contextless" (func: PtrUtilityFunction, args: ..TypePtr) { 56 | func(cast(TypePtr)nil, raw_data(args), len(args)) 57 | } 58 | 59 | /* 60 | Copyright 2025 Dresses Digital 61 | 62 | Licensed under the Apache License, Version 2.0 (the "License"); 63 | you may not use this file except in compliance with the License. 64 | You may obtain a copy of the License at 65 | 66 | http://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, 70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 | See the License for the specific language governing permissions and 72 | limitations under the License. 73 | */ 74 | -------------------------------------------------------------------------------- /gdext/context.odin: -------------------------------------------------------------------------------- 1 | package gdextension 2 | 3 | import "base:runtime" 4 | import "core:c" 5 | import "core:mem" 6 | 7 | godot_allocator_proc :: proc( 8 | allocator_data: rawptr, 9 | mode: mem.Allocator_Mode, 10 | size, alignment: int, 11 | old_memory: rawptr, 12 | old_size: int, 13 | loc := #caller_location, 14 | ) -> ( 15 | []byte, 16 | mem.Allocator_Error, 17 | ) { 18 | 19 | switch mode { 20 | case .Alloc, .Alloc_Non_Zeroed: 21 | ptr := mem_alloc(cast(c.size_t)size) 22 | if ptr == nil { 23 | return nil, .Out_Of_Memory 24 | } 25 | return mem.byte_slice(ptr, size), nil 26 | 27 | case .Free: 28 | mem_free(old_memory) 29 | 30 | case .Free_All: 31 | return nil, .Mode_Not_Implemented 32 | 33 | case .Resize, .Resize_Non_Zeroed: 34 | ptr: rawptr 35 | if old_memory == nil { 36 | ptr = mem_alloc(cast(c.size_t)size) 37 | if ptr == nil { 38 | return nil, .Out_Of_Memory 39 | } 40 | return mem.byte_slice(ptr, size), nil 41 | } 42 | ptr = mem_realloc(ptr, cast(c.size_t)size) 43 | if ptr == nil { 44 | return nil, .Out_Of_Memory 45 | } 46 | return mem.byte_slice(ptr, size), nil 47 | 48 | case .Query_Features: 49 | set := (^mem.Allocator_Mode_Set)(old_memory) 50 | if set != nil { 51 | set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Query_Features} 52 | } 53 | return nil, nil 54 | 55 | case .Query_Info: 56 | return nil, .Mode_Not_Implemented 57 | } 58 | 59 | return nil, nil 60 | } 61 | 62 | godot_allocator :: #force_inline proc "contextless" () -> (a: runtime.Allocator) { 63 | return mem.Allocator{procedure = godot_allocator_proc} 64 | } 65 | 66 | @(private) 67 | default_godot_allocator := godot_allocator() 68 | 69 | @(private) 70 | temp_arena := runtime.Arena { 71 | backing_allocator = default_godot_allocator, 72 | } 73 | 74 | @(private) 75 | default_temp_godot_allocator := runtime.Allocator{runtime.arena_allocator_proc, &temp_arena} 76 | 77 | godot_context :: #force_inline proc "contextless" () -> (c: runtime.Context) { 78 | c.allocator = default_godot_allocator 79 | c.temp_allocator = default_temp_godot_allocator 80 | return 81 | } 82 | 83 | /* 84 | Copyright 2025 Dresses Digital 85 | 86 | Licensed under the Apache License, Version 2.0 (the "License"); 87 | you may not use this file except in compliance with the License. 88 | You may obtain a copy of the License at 89 | 90 | http://www.apache.org/licenses/LICENSE-2.0 91 | 92 | Unless required by applicable law or agreed to in writing, software 93 | distributed under the License is distributed on an "AS IS" BASIS, 94 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 95 | See the License for the specific language governing permissions and 96 | limitations under the License. 97 | */ 98 | -------------------------------------------------------------------------------- /godot/Strings.odin: -------------------------------------------------------------------------------- 1 | package godot 2 | 3 | import gd "godot:gdext" 4 | import "core:mem" 5 | 6 | @(private) 7 | EmptyString := String{} 8 | 9 | @(private) 10 | EmptyStringName := String_Name{} 11 | 12 | string_empty :: proc "contextless" () -> String { 13 | return EmptyString 14 | } 15 | 16 | string_name_empty :: proc "contextless" () -> String_Name { 17 | return EmptyStringName 18 | } 19 | 20 | string_empty_ref :: proc "contextless" () -> ^String { 21 | return &EmptyString 22 | } 23 | 24 | string_name_empty_ref :: proc "contextless" () -> ^String_Name { 25 | return &EmptyStringName 26 | } 27 | 28 | /* 29 | Clones a UTF8 Odin string into a Godot String 30 | 31 | Inputs: 32 | - from: The string to be cloned 33 | 34 | Returns: 35 | - res: A cloned Godot String 36 | */ 37 | new_string_odin :: proc "contextless" (from: string) -> (ret: String) { 38 | ret = String{} 39 | 40 | // N.B. we're transmuting the odin string into a cstring regardless of if it has a terminating null 41 | // byte or not. `string_new_with_utf8_chars_and_len2` takes a length, so we don't depend on the 42 | // terminating null byte. 43 | as_cstring := cast(cstring)(transmute(mem.Raw_String)from).data 44 | gd.string_new_with_utf8_chars_and_len2(&ret, as_cstring, cast(i64)len(from)) 45 | return 46 | } 47 | 48 | /* 49 | Clones a UTF8 cstring into a Godot String 50 | 51 | Inputs: 52 | - from: The cstring to be cloned. Must be null-terminated. 53 | 54 | Returns: 55 | - res: A cloned Godot String 56 | */ 57 | new_string_cstring :: proc "contextless" (from: cstring) -> (ret: String) { 58 | ret = String{} 59 | gd.string_new_with_utf8_chars(&ret, from) 60 | return 61 | } 62 | 63 | /* 64 | Clones a UTF8 Odin string into a Godot String_Name 65 | 66 | Inputs: 67 | - from: The string to be cloned 68 | 69 | Returns: 70 | - res: A cloned Godot String_Name 71 | */ 72 | new_string_name_odin :: proc "contextless" (from: string) -> (ret: String_Name) { 73 | ret = String_Name{} 74 | 75 | // N.B. we're transmuting the odin string into a cstring regardless of if it has a terminating null 76 | // byte or not. `string_new_with_utf8_chars_and_len2` takes a length, so we don't depend on the 77 | // terminating null byte. 78 | as_cstring := cast(cstring)(transmute(mem.Raw_String)from).data 79 | gd.string_name_new_with_utf8_chars_and_len(&ret, as_cstring, cast(i64)len(from)) 80 | return 81 | } 82 | 83 | /* 84 | Clones a UTF8 cstring into a Godot String_Name 85 | 86 | Inputs: 87 | - from: The cstring to be cloned. Must be null-terminated. 88 | 89 | Returns: 90 | - res: A cloned Godot String_Name 91 | */ 92 | new_string_name_cstring :: proc "contextless" (from: cstring, static: bool) -> (ret: String_Name) { 93 | ret = String_Name{} 94 | gd.string_name_new_with_utf8_chars(&ret, from) 95 | return 96 | } 97 | 98 | 99 | new_node_path_cstring :: proc "contextless" (from: cstring) -> Node_Path { 100 | str: String 101 | gd.string_new_with_utf8_chars(&str, from) 102 | return new_node_path_string(str) 103 | } 104 | -------------------------------------------------------------------------------- /templates/bindgen_view_core.temple.twig: -------------------------------------------------------------------------------- 1 | package godot 2 | 3 | import __bindgen_gde "godot:gdext" 4 | 5 | {% for class in this.classes %} 6 | {{ class.name }} :: {{ class.derives }} 7 | {% end %} 8 | 9 | {% for enum_ in this.enums %} 10 | {% embed "bindgen_view_enum.temple.twig" with enum_ %} 11 | {% end %} 12 | 13 | {% for bit_field_ in this.bit_fields %} 14 | {% embed "bindgen_view_bit_field.temple.twig" with bit_field_ %} 15 | {% end %} 16 | 17 | init :: proc "contextless" () { 18 | __name: String_Name 19 | {% for function in this.functions %} 20 | __name = new_string_name_cstring("{{ function.godot_name }}", true) 21 | __{{ function.name }}_ptr = __bindgen_gde.variant_get_ptr_utility_function(&__name, {{ i64(function.hash) }}) 22 | {% end %} 23 | {% for singleton in this.singletons %} 24 | __{{ singleton.name }}_name = new_string_name_cstring("{{ singleton.name }}", true) 25 | {% end %} 26 | {% for snake_name in this.inits %} 27 | {{ snake_name }}_init() 28 | {% end %} 29 | } 30 | 31 | {% for function in this.functions %} 32 | {% if return_type, has_return_type := function.return_type.(string); has_return_type %} 33 | gd_{{ function.name }} :: proc "contextless" ( 34 | {% for arg in function.args %} 35 | {{ arg.name }}_: {{ arg.type }}, 36 | {% end %} 37 | ) -> (ret: {{ return_type }}) { 38 | {% for arg in function.args %} 39 | {{ arg.name }}_ := {{ arg.name }}_ 40 | {% end %} 41 | args := []__bindgen_gde.TypePtr { 42 | {% for arg in function.args %} 43 | &{{ arg.name }}_, 44 | {% end %} 45 | } 46 | __{{ function.name }}_ptr(&ret, raw_data(args), len(args)) 47 | return 48 | } 49 | 50 | {% else %} 51 | gd_{{ function.name }} :: proc "contextless" ( 52 | {% for arg in function.args %} 53 | {{ arg.name }}_: {{ arg.type }}, 54 | {% end %} 55 | ) { 56 | {% for arg in function.args %} 57 | {{ arg.name }}_ := {{ arg.name }}_ 58 | {% end %} 59 | args := []__bindgen_gde.TypePtr { 60 | {% for arg in function.args %} 61 | &{{ arg.name }}_, 62 | {% end %} 63 | } 64 | __{{ function.name }}_ptr(nil, raw_data(args), len(args)) 65 | } 66 | 67 | {% end %} 68 | {% end %} 69 | 70 | {% for singleton in this.singletons %} 71 | singleton_{{ singleton.snake_name }} :: proc "contextless" () -> {{ singleton.type }} { 72 | @(static) __ptr: __bindgen_gde.ObjectPtr 73 | if __ptr == nil { 74 | __ptr = __bindgen_gde.global_get_singleton(&__{{ singleton.name }}_name) 75 | } 76 | 77 | return __ptr 78 | } 79 | {% end %} 80 | 81 | @(require_results) 82 | cast_class :: proc "contextless" (object: $T, $C: typeid) -> (result: C, ok: bool) { 83 | class_tag := __bindgen_gde.classdb_get_class_tag(class_name_ref(C)) 84 | result_ptr := __bindgen_gde.object_cast_to(object, class_tag) 85 | if result_ptr != nil { 86 | return (cast(C)result_ptr), true 87 | } 88 | return C{}, false 89 | } 90 | 91 | class_name_ref :: proc "contextless" ($C: typeid) -> ^String_Name { 92 | switch typeid_of(C) { 93 | {% for class in this.classes %} 94 | case typeid_of({{ class.name }}): return {{ class.snake_name }}_name_ref() 95 | {% end %} 96 | } 97 | return nil 98 | } 99 | 100 | {% for function in this.functions %} 101 | @(private="file") 102 | __{{ function.name }}_ptr: __bindgen_gde.PtrUtilityFunction 103 | {% end %} 104 | 105 | {% for singleton in this.singletons %} 106 | @(private="file") 107 | __{{ singleton.name }}_name: String_Name 108 | {% end %} -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Godot Toolkit for Odin 2 | 3 | > [!WARNING] 4 | > **This toolkit is a Work In Progress!** 5 | > 6 | > If you are using parts of it, beware of sudden major changes to the API, structure, and features. 7 | 8 | This currently targets Godot v4.4. The base interface is backwards compatible with v4.3 and v4.2, and may even work with v4.1, **but it will not work with v4.0**. For a version which works with `v4.0` use the [dev-4.0-2024-02 release](https://github.com/dresswithpockets/odin-godot/tree/dev-4.0-2024-02). 9 | 10 | Checkout releases for each Godot version here: https://github.com/dresswithpockets/odin-godot/releases - Some are more stable than others. 11 | 12 | ## Base GDExtension Bindings 13 | 14 | The bindings to the C Interface are in `gdextension`. Check out [hello-gdextension](examples/hello-gdextension/) for example usage of these bindings. 15 | 16 | ## Generating Complete Bindings 17 | 18 | Clone & generate bindings 19 | ```sh 20 | git clone --recurse-submodules -j8 https://github.com/dresswithpockets/odin-godot 21 | cd odin-godot 22 | make bindings 23 | ``` 24 | 25 | Bindings for all enums, classes, utility functions, singletons, and native structs will be generated in `core`, `editor`, and `variant`. 26 | 27 | > [!NOTE] 28 | > `make bindings` expects odin on path, all submodules updated, and odin-godot (this repo) is the working directory. 29 | 30 | Alternatively, you may build and run `bindgen` yourself: 31 | ```sh 32 | # temple is the templating engine used by bindgen, temple_cli is temple's preprocessor. 33 | odin build temple/cli/ -o:speed -out:bin/temple_cli.exe 34 | 35 | # temple_cli will recursively search all odin files in the specified directory for 36 | # usages of `compiled` and `compiled_inline`, then it will generate Odin code with 37 | # the built templates in templates.odin, in the specified directory. in this case, 38 | # its searching in the bindgen directory, and outputs to bindgen/templates.odin, with 39 | # the `bindgen` package name. 40 | ./bin/temple_cli.exe bindgen bindgen bindgen 41 | 42 | # bindgen is the tool which uses the temple templates to generate the entire 43 | # GDExtension binding 44 | odin build bindgen/ -o:speed -out:bin/bindgen.exe 45 | 46 | # bindgen requires a path to a json file which describes the GDExtension API. 47 | # One is available in godot-cpp. 48 | ./bin/bindgen.exe godot-cpp/gdextension/extension_api.json 49 | ``` 50 | 51 | ## Creating a GDExtension 52 | 53 | See [the example game](examples/game) for a working usage of these bindings. 54 | 55 | Then, follow the instructions for [using the extension module](https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/gdextension_cpp_example.html#using-the-gdextension-module). 56 | 57 | ## Godin 58 | 59 | Godin is a preprocessor which generates most of the boilerplate for Extension Classes, Methods, Enums, Properties, Signals, Groups, and Subgroups. 60 | 61 | > [!WARNING] 62 | > **Godin is suuuuper work in progress!** 63 | > 64 | > Godin doesn't produce correct gdextension useage in its current state, and may not even run in some cases. 65 | 66 | For example, the following Odin code will produce all of the boilerplate for an extension class "Player" that extends "Node2D". 67 | ```odin 68 | package test 69 | 70 | //+class Player extends Node2D 71 | Player :: struct { 72 | health: i64, 73 | } 74 | ``` 75 | 76 | To build Godin: 77 | ```sh 78 | # while in the odin-godot directory 79 | ./build_godin.sh 80 | ``` 81 | 82 | Run `godin help` for usage details, including documentation about the Godin preprocessor syntax. 83 | -------------------------------------------------------------------------------- /templates/bindgen_class.temple.twig: -------------------------------------------------------------------------------- 1 | /* This file is generated by godin bindgen - DO NOT EDIT! */ 2 | package {{ this.derived.(StateClass).api_type }} 3 | 4 | import __bindgen_gde "godot:gdext" 5 | import __bindgen_var "../variant" 6 | {% if this.depends_on_core_math %} 7 | import __bindgen_math "core:math" 8 | {% end %} 9 | 10 | {{ this.odin_type }} :: distinct __bindgen_gde.ObjectPtr 11 | 12 | // enums 13 | {% for state_enum in this.derived.(StateClass).enums %} 14 | 15 | {% if !state_enum.odin_skip %} 16 | {% embed "bindgen_enum.temple.twig" with state_enum.derived.(StateEnum) %} 17 | {% end %} 18 | 19 | {% end %} 20 | 21 | // constants 22 | {% for constant in this.derived.(StateClass).constants %} 23 | {% if !constant.odin_skip %} 24 | {% if constant.type != nil %} 25 | {{ constant.name }}: {{ constant.type.odin_type }} = {% if int_value, is_int := constant.value.(int); is_int %}{{ int(int_value) }}{% else %}{{ constant.value.(string) }}{% end %} 26 | {% else %} 27 | {{ constant.name }} :: {% if int_value, is_int := constant.value.(int); is_int %}{{ int(int_value) }}{% else %}{{ constant.value.(string) }}{% end %} 28 | {% end %} 29 | {% end %} 30 | {% end %} 31 | 32 | // method frontends 33 | {% for method in this.derived.(StateClass).methods %} 34 | {% if !method.odin_skip %} 35 | {{ method.odin_name }} :: proc "contextless" (self: {{ this.odin_type }} 36 | {% for argument in method.arguments %}, {{ argument.name }}_: {{ bindgen_class_reference_type(argument.type) }} 37 | {% end %}) 38 | {% if return_type, has_return_type := method.return_type.(^StateType); has_return_type %} -> {{ bindgen_class_reference_type(return_type) }}{% end %} { 39 | self := self 40 | {% for argument in method.arguments %} 41 | {{ argument.name }}_ := {{ argument.name }}_ 42 | {% end %} 43 | {% if return_type, has_return_type := method.return_type.(^StateType); has_return_type %} 44 | return __bindgen_gde.call_method_ptr_ret( 45 | __{{ method.odin_name }}__backing_ptr, 46 | {{ bindgen_class_reference_type(return_type) }}, 47 | self, 48 | {% else %} 49 | __bindgen_gde.call_method_ptr_no_ret( 50 | __{{ method.odin_name }}__backing_ptr, 51 | self, 52 | {% end %} 53 | {% for argument in method.arguments %} 54 | &{{ argument.name }}_, 55 | {% end %} 56 | ) 57 | } 58 | 59 | {% end %} 60 | {% end %} 61 | 62 | @(private) 63 | __{{ this.odin_type }}_init :: proc "contextless" () { 64 | __bindgen_gde.string_name_new_with_latin1_chars(&__{{ this.odin_type }}__class_name, "{{ this.godot_type }}", true) 65 | _gde_name := __bindgen_var.StringName{} 66 | 67 | {% for method in this.derived.(StateClass).methods %} 68 | {% if !method.odin_skip %} 69 | __bindgen_gde.string_name_new_with_latin1_chars(&_gde_name, "{{ method.godot_name }}", true) 70 | __{{ method.odin_name }}__backing_ptr = __bindgen_gde.classdb_get_method_bind(&__{{ this.odin_type }}__class_name, &_gde_name, {{ i64(method.hash) }}) 71 | _gde_name = __bindgen_var.StringName{} 72 | {% end %} 73 | {% end %} 74 | } 75 | 76 | new_{{ this.snake_type }} :: proc "contextless" () -> {{ this.odin_type }} { 77 | return cast({{ this.odin_type }})__bindgen_gde.classdb_construct_object(&__{{ this.odin_type }}__class_name) 78 | } 79 | 80 | @(private) 81 | __{{ this.odin_type }}__class_name: __bindgen_var.StringName 82 | 83 | // method backing ptrs 84 | {% for method in this.derived.(StateClass).methods %} 85 | {% if !method.odin_skip %} 86 | @(private="file") 87 | __{{ method.odin_name }}__backing_ptr: __bindgen_gde.MethodBindPtr 88 | {% end %} 89 | {% end %} 90 | -------------------------------------------------------------------------------- /examples/tests/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/godin-syntax/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/hello-gdextension/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /godin/help.syntax.md: -------------------------------------------------------------------------------- 1 | # 1. Godin Syntax 2 | 3 | Godot's native interface is very tedious to use. For every class or class member 4 | you want to define, there is a significant amount of boilerplate needed. 5 | 6 | Godin provides a simple syntax to inform the preprocessor what bindings need to 7 | be generated. 8 | 9 | # 1. Classes 10 | 11 | To declare an Extension Class to be registered with Godot's ClassDB, use the class 12 | declaration: 13 | 14 | //+class Player extends Node2D 15 | Player :: struct { 16 | health: i64, 17 | } 18 | 19 | Unlike in GDScript, the `extends` declaration is required and indicates what this 20 | class inherits from. In GDScript, classes inherit from RefCounted by default. 21 | 22 | By default, directives will always produce code in a separate file named `{FILE}.gen.odin`. 23 | For example, if the previous example was written in a file `player.odin`, then 24 | the output bindings will be in `player.gen.odin`. 25 | 26 | ## 1.1 Methods 27 | 28 | To declare an instance method for an extension class, use the method declaration: 29 | 30 | //+method(Player) kill() 31 | player_kill :: proc(self: ^Player) { 32 | self.health = 0 33 | } 34 | 35 | Methods can have arguments, whose type arguments must be godot types: 36 | 37 | //+method(Player) damage(amount: int) 38 | player_damage :: proc(self: ^Player, amount: i64) { 39 | self.health -= amount 40 | } 41 | 42 | Methods can have return values: 43 | 44 | //+method(Player) heal(self: ^Player, amount: int) -> int 45 | player_heal :: proc(self: ^Player, amount: i64) { 46 | return self.health += amount 47 | } 48 | 49 | You can declare a static method by prepending `static` to a method declaration: 50 | 51 | //+static method(Player) do_thing(a: int, b: int) -> int 52 | player_do_thing :: proc(a, b: i64) -> i64 { 53 | return a + b 54 | } 55 | 56 | ### 1.1.1 Special Methods 57 | 58 | You may override the default behaviour of some pre-defined methods by name: 59 | 60 | //+method(Player) _set(property: StringName, value: Variant) -> bool 61 | player_set :: proc(self: ^Player, property: var.StringName, value: var.Variant) -> bool { 62 | // check property name and field value on self 63 | // return true if property found & set, false otherwise 64 | } 65 | 66 | These are the methods you may override by name: 67 | 68 | _init() 69 | _notification(what: int) 70 | _set(property: StringName, value: Variant) -> bool 71 | _get(property: StringName, value: ^Variant) -> bool 72 | _get_property_list() -> Dictionary 73 | _property_can_revert(name: StringName) -> bool 74 | _property_get_revert(name: StringName, value: ^Variant) -> bool 75 | _to_string() -> String 76 | 77 | See https://docs.godotengine.org/en/latest/classes/class_object.html for more 78 | details on these methods 79 | 80 | You may also override `_destroy`, which will be called when the object is freed. 81 | 82 | ## 1.2 Properties 83 | 84 | To declare a class property getter, use the property-get declaration: 85 | 86 | //+property(Player) health get: int 87 | player_get_health :: proc(self: ^Player) -> i64 { 88 | return self.health 89 | } 90 | 91 | Setters use similar syntax: 92 | 93 | //+property(Player) health set: int 94 | player_set_health :: proc(self: ^Player, value: i64) { 95 | self.health = value 96 | } 97 | 98 | ## 1.3 Signals 99 | 100 | To declare a class signal, use the signal declaration: 101 | 102 | //+class Player extends Node2D 103 | Player :: struct { 104 | // ... 105 | 106 | //+signal on_kill(last_damage: int) 107 | killed: Signal, 108 | } 109 | 110 | You can emit the signal directly, for example: 111 | 112 | //+method(Player) damage(amount: int) 113 | player_damage :: proc(self: ^Player, amount: i64) { 114 | self.health -= amount 115 | if self.health <= 0 { 116 | gd.emit_signal(self.killed, amount) 117 | } 118 | } 119 | 120 | ## 1.4 Enums 121 | 122 | To declare an enum, use the enum declaration: 123 | 124 | //+enum(Player) PlayerState 125 | PlayerState :: enum { 126 | Alive, 127 | Dead, 128 | } 129 | -------------------------------------------------------------------------------- /examples/game/csrc/player.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "lib.h" 3 | #include "player.h" 4 | 5 | typedef GDExtensionObjectPtr Node; 6 | typedef GDExtensionObjectPtr Node3d; 7 | typedef GDExtensionObjectPtr Camera3d; 8 | typedef GDExtensionObjectPtr CharacterBody3d; 9 | typedef GDExtensionObjectPtr CollisionShape3d; 10 | typedef GDExtensionObjectPtr Shape3d; 11 | 12 | String CameraYaw_Name; 13 | StringName CharacterBody3D_ClassName; 14 | StringName Player_ClassName; 15 | StringName Ready_VirtualName; 16 | 17 | typedef struct { 18 | CharacterBody3d object; 19 | Node3d camera_yaw; 20 | } Player; 21 | 22 | void player_ready(Player* self) { 23 | Node self_node = self->object; 24 | 25 | NodePath camera_yaw_path; 26 | { 27 | GDExtensionConstTypePtr args[1] = { &CameraYaw_Name }; 28 | node_path_from_string(&camera_yaw_path, args); 29 | } 30 | 31 | { 32 | GDExtensionConstTypePtr args[1] = { &camera_yaw_path }; 33 | object_method_bind_ptrcall(get_node_method_bind, self->object, args, &self->camera_yaw); 34 | } 35 | 36 | print_object(&self->camera_yaw); 37 | } 38 | 39 | static GDExtensionInstanceBindingCallbacks player_binding_callbacks = { 40 | .create_callback = NULL, 41 | .free_callback = NULL, 42 | .reference_callback = NULL, 43 | }; 44 | 45 | GDExtensionObjectPtr __cdecl player_create_instance(void* class_userdata) { 46 | Player* self = gd_alloc(sizeof(Player)); 47 | self->object = classdb_construct_object(&CharacterBody3D_ClassName); 48 | 49 | object_set_instance(self->object, &Player_ClassName, self); 50 | object_set_instance_binding(self->object, ext_library, self, &player_binding_callbacks); 51 | 52 | return self->object; 53 | } 54 | 55 | void __cdecl player_free_instance(void* class_userdata, GDExtensionClassInstancePtr instance) { 56 | if (!instance) { 57 | return; 58 | } 59 | 60 | gd_free(instance); 61 | } 62 | 63 | void* __cdecl player_get_virtual_with_data(void* class_userdata, GDExtensionConstStringNamePtr name) { 64 | if (call_builtin_op_bool(StringName_eq_StringName_op, &Ready_VirtualName, name)) { 65 | return player_ready; 66 | } 67 | 68 | return NULL; 69 | } 70 | 71 | void __cdecl player_call_virtual_with_data(GDExtensionClassInstancePtr instance, GDExtensionConstStringNamePtr name, void *virtual_call_userdata, const GDExtensionConstTypePtr *args, GDExtensionTypePtr ret) { 72 | if (virtual_call_userdata == player_ready) { 73 | player_ready(instance); 74 | return; 75 | } 76 | } 77 | 78 | void player_class_register() { 79 | string_new_with_latin1_chars(&CameraYaw_Name, "CameraYaw"); 80 | string_name_new_with_latin1_chars(&CharacterBody3D_ClassName, "CharacterBody3D", true); 81 | string_name_new_with_latin1_chars(&Player_ClassName, "Player", true); 82 | string_name_new_with_latin1_chars(&Ready_VirtualName, "_ready", true); 83 | 84 | GDExtensionClassCreationInfo3 class_info = { 85 | .is_virtual = false, 86 | .is_abstract = false, 87 | .is_exposed = true, 88 | .is_runtime = false, 89 | .set_func = NULL, 90 | .get_func = NULL, 91 | .get_property_list_func = NULL, 92 | .free_property_list_func = NULL, 93 | .property_can_revert_func = NULL, 94 | .property_get_revert_func = NULL, 95 | .validate_property_func = NULL, 96 | .notification_func = NULL, 97 | .to_string_func = NULL, 98 | .reference_func = NULL, 99 | .unreference_func = NULL, 100 | .create_instance_func = player_create_instance, 101 | .free_instance_func = player_free_instance, 102 | .recreate_instance_func = NULL, 103 | .get_virtual_func = NULL, 104 | .get_virtual_call_data_func = player_get_virtual_with_data, 105 | .call_virtual_with_data_func = player_call_virtual_with_data, 106 | .get_rid_func = NULL, 107 | .class_userdata = NULL, 108 | }; 109 | 110 | classdb_register_extension_class3(ext_library, &Player_ClassName, &CharacterBody3D_ClassName, &class_info); 111 | } 112 | -------------------------------------------------------------------------------- /godin/case.odin: -------------------------------------------------------------------------------- 1 | package godin 2 | 3 | import "core:fmt" 4 | import "core:strings" 5 | import "core:unicode" 6 | import "core:unicode/utf8" 7 | 8 | // godot uses ACRONYMPascalCase, but we use AcronymPascalCase 9 | // return string must be freed 10 | godot_to_odin_case :: proc(name: string) -> (s: string) { 11 | assert(len(name) > 0) 12 | 13 | sb := strings.builder_make() 14 | defer strings.builder_destroy(&sb) 15 | 16 | runes := utf8.string_to_runes(name) 17 | defer delete(runes) 18 | 19 | fmt.sbprint(&sb, unicode.to_upper(runes[0])) 20 | for i := 1; i < len(runes) - 1; i += 1 { 21 | r := runes[i] 22 | previous := runes[i - 1] 23 | next := runes[i + 1] 24 | if unicode.is_upper(r) && 25 | (unicode.is_upper(previous) || unicode.is_number(previous)) && 26 | unicode.is_upper(next) { 27 | fmt.sbprint(&sb, unicode.to_lower(r)) 28 | continue 29 | } 30 | 31 | fmt.sbprint(&sb, r) 32 | } 33 | // always push the last rune as lower 34 | if len(runes) > 1 { 35 | fmt.sbprint(&sb, unicode.to_lower(runes[len(runes) - 1])) 36 | } 37 | 38 | s = strings.clone(strings.to_string(sb)) 39 | return 40 | } 41 | 42 | godot_to_snake_case :: proc(name: string) -> (s: string) { 43 | // lol (: 44 | s = odin_to_snake_case(godot_to_odin_case(name)) 45 | return 46 | } 47 | 48 | odin_to_snake_case :: proc(name: string) -> (s: string) { 49 | assert(len(name) > 0) 50 | 51 | sb := strings.builder_make() 52 | defer strings.builder_destroy(&sb) 53 | 54 | runes := utf8.string_to_runes(name) 55 | defer delete(runes) 56 | 57 | fmt.sbprint(&sb, unicode.to_lower(runes[0])) 58 | for i := 1; i < len(runes); i += 1 { 59 | r := runes[i] 60 | if unicode.is_alpha(r) && unicode.is_upper(r) { 61 | fmt.sbprint(&sb, "_") 62 | fmt.sbprint(&sb, unicode.to_lower(r)) 63 | continue 64 | } 65 | 66 | fmt.sbprint(&sb, unicode.to_lower(r)) 67 | } 68 | 69 | s = strings.clone(strings.to_string(sb)) 70 | return 71 | } 72 | 73 | odin_to_const_case :: proc(name: string) -> (s: string) { 74 | assert(len(name) > 0) 75 | 76 | sb := strings.builder_make() 77 | defer strings.builder_destroy(&sb) 78 | 79 | runes := utf8.string_to_runes(name) 80 | defer delete(runes) 81 | 82 | fmt.sbprint(&sb, runes[0]) 83 | for i := 1; i < len(runes); i += 1 { 84 | r := runes[i] 85 | if unicode.is_alpha(r) && unicode.is_upper(r) { 86 | fmt.sbprint(&sb, "_") 87 | fmt.sbprint(&sb, r) 88 | continue 89 | } 90 | 91 | fmt.sbprint(&sb, unicode.to_upper(r)) 92 | } 93 | 94 | s = strings.clone(strings.to_string(sb)) 95 | return 96 | } 97 | 98 | const_to_odin_case :: proc(name: string) -> (s: string) { 99 | assert(len(name) > 0) 100 | 101 | sb := strings.builder_make() 102 | defer strings.builder_destroy(&sb) 103 | 104 | runes := utf8.string_to_runes(name) 105 | defer delete(runes) 106 | 107 | if unicode.is_number(runes[0]) { 108 | fmt.sbprint(&sb, "_") 109 | } 110 | 111 | fmt.sbprint(&sb, runes[0]) 112 | for i := 1; i < len(runes); i += 1 { 113 | r := runes[i] 114 | if r == '_' { 115 | continue 116 | } 117 | 118 | previous := runes[i - 1] 119 | if previous == '_' { 120 | fmt.sbprint(&sb, r) 121 | continue 122 | } 123 | 124 | fmt.sbprint(&sb, unicode.to_lower(r)) 125 | } 126 | 127 | s = strings.clone(strings.to_string(sb)) 128 | return 129 | } 130 | 131 | /* 132 | Copyright 2025 Dresses Digital 133 | 134 | Licensed under the Apache License, Version 2.0 (the "License"); 135 | you may not use this file except in compliance with the License. 136 | You may obtain a copy of the License at 137 | 138 | http://www.apache.org/licenses/LICENSE-2.0 139 | 140 | Unless required by applicable law or agreed to in writing, software 141 | distributed under the License is distributed on an "AS IS" BASIS, 142 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 143 | See the License for the specific language governing permissions and 144 | limitations under the License. 145 | */ 146 | -------------------------------------------------------------------------------- /bindgen/main.odin: -------------------------------------------------------------------------------- 1 | package bindgen 2 | 3 | import "core:encoding/json" 4 | import "core:fmt" 5 | import "core:os" 6 | import "core:strconv" 7 | import "core:strings" 8 | import "core:time" 9 | import g "graph" 10 | import "views" 11 | 12 | Options :: struct { 13 | api_file: string, 14 | job_count: int, 15 | map_mode: views.Package_Map_Mode, 16 | } 17 | 18 | default_options :: proc() -> Options { 19 | return Options{api_file = "", job_count = 0, map_mode = .Nested} 20 | } 21 | 22 | print_usage :: proc() { 23 | fmt.println("bindgen generates Odin bindings from godot's extension_api.json") 24 | fmt.println("Usage:") 25 | fmt.println("\tbindgen api_json_path [options]") 26 | fmt.println("Options:") 27 | fmt.println("\t-jobs:") 28 | fmt.println("\t\tThe number of threads to use when building the bindings.") 29 | fmt.println("\t\tDefaults to the number of logical CPU cores available.") 30 | } 31 | 32 | parse_args :: proc(options: ^Options) -> (ok: bool) { 33 | ok = true 34 | options.api_file = os.args[1] 35 | if len(os.args) > 1 { 36 | for i := 2; i < len(os.args); i += 1 { 37 | arg := os.args[i] 38 | if strings.contains_rune(arg, ':') { 39 | if strings.has_prefix(arg, "-jobs:") { 40 | if len(arg) < 7 { 41 | fmt.eprintln("Value missing for arg '-jobs'") 42 | ok = false 43 | continue 44 | } 45 | 46 | right := arg[6:] 47 | if result, ok := strconv.parse_int(right); ok { 48 | if result < 0 { 49 | fmt.eprintln("Job count must be at least 0") 50 | ok = false 51 | continue 52 | } 53 | options.job_count = result 54 | } else { 55 | fmt.eprintfln("Expected an integer for job count, but got '%v' instead", right) 56 | ok = false 57 | continue 58 | } 59 | 60 | continue 61 | } 62 | } 63 | // we don't yet have any options which are just flags 64 | fmt.eprintfln("Invalid option: %v", arg) 65 | ok = false 66 | } 67 | } 68 | 69 | if options.job_count == 0 { 70 | options.job_count = num_processors() 71 | } 72 | return 73 | } 74 | 75 | load_api :: proc(options: Options) -> (api: ^g.Api, ok: bool) { 76 | data := os.read_entire_file(options.api_file) or_return 77 | defer delete(data) 78 | 79 | api = new(g.Api) 80 | err := json.unmarshal(data, api) 81 | ok = err == nil 82 | return 83 | } 84 | 85 | main :: proc() { 86 | options := default_options() 87 | 88 | if len(os.args) < 2 { 89 | print_usage() 90 | os.exit(1) 91 | } 92 | 93 | if ok := parse_args(&options); !ok { 94 | os.exit(1) 95 | } 96 | 97 | fmt.printfln("Parsing API: %v", options.api_file) 98 | api, ok := load_api(options) 99 | if !ok { 100 | fmt.println("There was an error loading the api from the file.") 101 | os.exit(1) 102 | } 103 | 104 | graph: g.Graph 105 | 106 | fmt.println("Running Pass: Init") 107 | g.graph_init(&graph, api, context.allocator) 108 | 109 | fmt.println("Running Pass: Map") 110 | g.graph_type_info_pass(&graph, api) 111 | g.graph_builtins_structure_pass(&graph, api) 112 | 113 | fmt.println("Running Pass: TDG") 114 | g.graph_relationship_pass(&graph, api) 115 | 116 | fmt.printfln("Running Codegen (%v)", api.version.full_name) 117 | generate_bindings(graph, options) 118 | 119 | // since we wanna keep state around until the end of the program's lifetime, 120 | // no need to be particular about freeing the bits and pieces of the struct (: 121 | free_all() 122 | } 123 | 124 | /* 125 | Copyright 2025 Dresses Digital 126 | 127 | Licensed under the Apache License, Version 2.0 (the "License"); 128 | you may not use this file except in compliance with the License. 129 | You may obtain a copy of the License at 130 | 131 | http://www.apache.org/licenses/LICENSE-2.0 132 | 133 | Unless required by applicable law or agreed to in writing, software 134 | distributed under the License is distributed on an "AS IS" BASIS, 135 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136 | See the License for the specific language governing permissions and 137 | limitations under the License. 138 | */ 139 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | exe_suffix := .exe 3 | shared_suffix := .dll 4 | endif 5 | 6 | OUT_DIR := bin/ 7 | REAL_PRECISION := single 8 | JOBS := 0 9 | 10 | temple_cli_dir := temple/cli/ 11 | temple_cli_deps := $(wildcard $(temple_cli_dir)*.odin) 12 | temple_cli_out := $(OUT_DIR)/temple_cli$(exe_suffix) 13 | 14 | bindgen_dir := bindgen/ 15 | bindgen_deps := $(wildcard $(bindgen_dir)*.odin) $(wildcard $(bindgen_dir)**/*.odin) $(bindgen_dir)templates.odin $(wildcard temple/*.odin) 16 | bindgen_out := $(OUT_DIR)/bindgen$(exe_suffix) 17 | debug_bindgen_out := $(OUT_DIR)/bindgen_debug$(exe_suffix) 18 | 19 | temple_dir := temple 20 | temple_deps := $(wildcard templates/*.temple.twig) $(bindgen_dir)temple.odin 21 | 22 | gdextension_api := ./godot-cpp/gdextension/extension_api.json 23 | 24 | bindings: godot/godot.gen.odin 25 | 26 | # hack: we don't need to regenerate if core/init is up to date! 27 | godot/godot.gen.odin: $(bindgen_out) $(gdextension_api) 28 | $(bindgen_out) $(gdextension_api) -jobs:$(JOBS) 29 | 30 | debug_bindings: $(debug_bindgen_out) $(gdextension_api) 31 | $(debug_bindgen_out) $(gdextension_api) 32 | 33 | ### temple 34 | $(temple_cli_out): $(temple_cli_deps) 35 | odin build $(temple_cli_dir) -out:$(temple_cli_out) -o:speed -show-timings 36 | temple: $(temple_cli_out) 37 | ### 38 | 39 | ### templates 40 | bindgen/templates.odin: $(temple_cli_out) $(temple_deps) 41 | $(temple_cli_out) $(bindgen_dir) $(bindgen_dir) bindgen 42 | templates: bindgen/templates.odin 43 | ### 44 | 45 | ### bindgen 46 | $(bindgen_out): $(bindgen_deps) 47 | odin build $(bindgen_dir) -out:$(bindgen_out) -o:speed -show-timings 48 | bindgen: $(bindgen_out) 49 | 50 | $(debug_bindgen_out): $(bindgen_deps) 51 | odin build $(bindgen_dir) -out:$(debug_bindgen_out) -show-timings -debug 52 | debug_bindgen: $(debug_bindgen_out) 53 | ### 54 | 55 | 56 | ### examples 57 | examples_hello_dir := examples/hello-gdextension/ 58 | examples_hello_deps := $(wildcard $(examples_hello_dir)src/*.odin) 59 | examples_hello_out := $(examples_hello_dir)bin/example$(shared_suffix) 60 | 61 | examples_game_dir := examples/game/ 62 | examples_game_deps := $(wildcard $(examples_game_dir)src/*.odin) 63 | examples_game_out := $(examples_game_dir)bin/game$(shared_suffix) 64 | 65 | $(examples_hello_out): godot/godot.gen.odin $(examples_hello_deps) 66 | odin build $(examples_hello_dir)src/ \ 67 | -define:REAL_PRECISION=$(REAL_PRECISION) \ 68 | -collection:godot=. \ 69 | -build-mode:shared \ 70 | -out:$(examples_hello_out) \ 71 | -warnings-as-errors \ 72 | -default-to-nil-allocator \ 73 | -target:windows_amd64 \ 74 | -debug \ 75 | -show-timings 76 | 77 | $(examples_game_out): godot/godot.gen.odin $(examples_game_deps) 78 | odin build $(examples_game_dir)src/ \ 79 | -define:REAL_PRECISION=$(REAL_PRECISION) \ 80 | -collection:godot=. \ 81 | -build-mode:shared \ 82 | -out:$(examples_game_out) \ 83 | -warnings-as-errors \ 84 | -default-to-nil-allocator \ 85 | -target:windows_amd64 \ 86 | -debug \ 87 | -show-timings 88 | 89 | examples/tests/bin/tests$(shared_suffix): godot/godot.gen.odin $(wildcard examples/tests/src/*.odin) 90 | odin build examples/tests/src/ \ 91 | -define:REAL_PRECISION=$(REAL_PRECISION) \ 92 | -collection:godot=. \ 93 | -build-mode:shared \ 94 | -out:examples/tests/bin/tests$(shared_suffix) \ 95 | -warnings-as-errors \ 96 | -default-to-nil-allocator \ 97 | -target:windows_amd64 \ 98 | -debug \ 99 | -show-timings 100 | 101 | examples: examples/tests/bin/tests$(shared_suffix) $(examples_hello_out) $(examples_game_out) 102 | 103 | examples/game/cbin/game.dll: $(wildcard examples/game/csrc/*) 104 | cl.exe /std:clatest /ZI /D_USRDLL /D_WINDLL $(wildcard examples/game/csrc/*.c) /link /DLL /OUT:examples/game/cbin/game.dll 105 | 106 | cexamples: examples/game/cbin/game.dll 107 | ### 108 | 109 | tests: 110 | odin test bindgen/ -all-packages -debug -out:$(OUT_DIR)/bindgen_tests$(exe_suffix) 111 | 112 | check: 113 | odin check bindgen/ 114 | odin check gdextension/ -no-entry-point -collection:godot=. -vet 115 | odin check godot/ -no-entry-point -collection:godot=. -define:REAL_PRECISION=$(REAL_PRECISION) -vet 116 | odin check examples/game/src -no-entry-point -collection:godot=. -define:REAL_PRECISION=$(REAL_PRECISION) -vet 117 | odin check examples/hello-gdextension/src -no-entry-point -collection:godot=. -define:REAL_PRECISION=$(REAL_PRECISION) -vet 118 | odin check examples/tests/src -no-entry-point -collection:godot=. -define:REAL_PRECISION=$(REAL_PRECISION) -vet 119 | 120 | .PHONY: clean 121 | 122 | clean: 123 | rm -f bindgen/templates.odin 124 | rm -f $(OUT_DIR)/* 125 | rm -f godot/*.gen.odin 126 | rm -f $(examples_hello_dir)bin/* 127 | rm -f $(examples_game_dir)bin/* 128 | rm -f examples/tests/bin/* 129 | rm -f examples/game/cbin/* 130 | -------------------------------------------------------------------------------- /godin/templates/class.odin.template: -------------------------------------------------------------------------------- 1 | package {0:s} 2 | 3 | import gd "{1:s}gdextension" 4 | import var "{1:s}variant" 5 | import "core:strings" 6 | import "core:fmt" 7 | 8 | @(private="file") 9 | __{2:s}__Class__StringName: var.StringName 10 | 11 | @(private="file") 12 | __{2:s}__Parent__StringName: var.StringName 13 | 14 | @(private="file") 15 | __{2:s}__Empty__StringName: var.StringName 16 | 17 | init_{4:s}_bindings :: proc() {{ 18 | using gd 19 | __{2:s}__Class__StringName = var.new_string_name_cstring("{2:s}") 20 | __{2:s}__Parent__StringName = var.new_string_name_cstring("{3:s}") 21 | __{2:s}__Empty__StringName = var.new_string_name_cstring("Object") 22 | class_info := ExtensionClassCreationInfo {{ 23 | is_virtual = false, 24 | is_abstract = false, 25 | set_func = {6:s}, 26 | get_func = {7:s}, 27 | get_property_list_func = {8:s}, 28 | free_property_list_func = {9:s}, 29 | property_can_revert_func = {10:s}, 30 | property_get_revert_func = {11:s}, 31 | notification_func = {12:s}, 32 | to_string_func = {4:s}_to_string_bind, 33 | reference_func = nil, 34 | unreference_func = nil, 35 | create_instance_func = {4:s}_create, 36 | free_instance_func = {4:s}_free, 37 | get_virtual_func = gd.class_db_get_virtual_func, 38 | get_rid_func = nil, 39 | class_user_data = &__{2:s}__Class__StringName, 40 | }} 41 | gd.interface.classdb_register_extension_class(gd.library, cast(StringNamePtr)&__{2:s}__Class__StringName._opaque, cast(StringNamePtr)&__{2:s}__Parent__StringName._opaque, &class_info) 42 | }} 43 | 44 | {4:s}_set_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, name: gd.StringNamePtr, value: gd.VariantPtr) -> bool {{ 45 | return false 46 | }} 47 | 48 | {4:s}_get_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, name: gd.StringNamePtr, ret: gd.VariantPtr) -> bool {{ 49 | return false 50 | }} 51 | 52 | {4:s}_get_property_list_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, count: ^u32) -> [^]gd.PropertyInfo {{ 53 | if count != nil {{ 54 | count^ = 0 55 | }} 56 | return nil 57 | }} 58 | 59 | {4:s}_free_property_list_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, list: ^gd.PropertyInfo) {{ 60 | 61 | }} 62 | 63 | {4:s}_property_can_revert_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, name: gd.StringNamePtr) -> bool {{ 64 | return false 65 | }} 66 | 67 | {4:s}_property_get_revert_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, name: gd.StringNamePtr, ret: gd.VariantPtr) -> bool {{ 68 | return false 69 | }} 70 | 71 | {4:s}_notification_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, what: i32) {{ 72 | // override _notification to provide your own implementation 73 | }} 74 | 75 | {4:s}_to_string_bind :: proc "c" (instance: gd.ExtensionClassInstancePtr, is_valid: ^bool, out: gd.StringPtr) {{ 76 | out^ = var.new_string_cstring("[{2:s}:0]") 77 | is_valid^ = true 78 | }} 79 | 80 | {4:s}_create :: proc "c" (data: rawptr) -> gd.ObjectPtr {{ 81 | context = gd.godot_context() 82 | 83 | instance := new({2:s}) 84 | // TODO: {4:s}_init(instance) 85 | instance._owner = gd.interface.classdb_construct_object(cast(gd.StringNamePtr)&__{2:s}__Empty__StringName._opaque) 86 | __{2:s}__post_initialization(instance) 87 | 88 | return instance._owner 89 | }} 90 | 91 | {4:s}_free :: proc "c" (data: rawptr, ptr: gd.ExtensionClassInstancePtr) {{ 92 | if data == nil {{ 93 | return 94 | }} 95 | 96 | context = gd.godot_context() 97 | 98 | instance := cast(^{5:s})ptr 99 | // TODO: {4:s}_destroy(instance) 100 | free(instance) 101 | }} 102 | 103 | @(private="file") 104 | __{2:s}__post_initialization :: proc(self: ^{5:s}) {{ 105 | gd.interface.object_set_instance(self._owner, cast(gd.StringNamePtr)&__{2:s}__Class__StringName._opaque, cast(gd.ExtensionClassInstancePtr)self) 106 | gd.interface.object_set_instance_binding(self._owner, gd.library, self, &__{2:s}__BindingCallbacks) 107 | }} 108 | 109 | @(private="file") 110 | __{2:s}__BindingCallbacks := gd.InstanceBindingCallbacks {{ 111 | __{2:s}__create_callback, 112 | __{2:s}__free_callback, 113 | __{2:s}__reference_callback, 114 | }} 115 | 116 | @(private="file") 117 | __{2:s}__create_callback :: proc "c" (token: rawptr, instance: rawptr) -> rawptr {{ 118 | return nil 119 | }} 120 | 121 | @(private="file") 122 | __{2:s}__free_callback :: proc "c" (token: rawptr, instance: rawptr, binding: rawptr) {{ 123 | }} 124 | 125 | @(private="file") 126 | __{2:s}__reference_callback :: proc "c" (token: rawptr, instance: rawptr, reference: bool) -> bool {{ 127 | return true 128 | }} 129 | -------------------------------------------------------------------------------- /templates/bindgen_view_engine.temple.twig: -------------------------------------------------------------------------------- 1 | package godot 2 | 3 | {% for name, import_ in this.imports %} 4 | import {{ name }} "{{ import_.path }}" 5 | {% end %} 6 | 7 | {{ this.name }}_Constants :: enum { 8 | {% for constant in this.file_constants %} 9 | {{ constant.name }} = {{ constant.value }}, 10 | {% end %} 11 | } 12 | 13 | {% for enum_ in this.enums %} 14 | {% embed "bindgen_view_enum.temple.twig" with enum_ %} 15 | {% end %} 16 | 17 | {% for bit_field_ in this.bit_fields %} 18 | {% embed "bindgen_view_bit_field.temple.twig" with bit_field_ %} 19 | {% end %} 20 | 21 | {{ this.snake_name }}_name_ref :: proc "contextless" () -> ^String_Name { 22 | return &__class_name 23 | } 24 | 25 | {{ this.snake_name }}_name :: proc "contextless" () -> String_Name { 26 | return __class_name 27 | } 28 | 29 | new_{{ this.snake_name }} :: proc "contextless" () -> {{ this.self }} { 30 | {% if this.cast_on_new %} 31 | return cast({{ this.self }})__bindgen_gde.classdb_construct_object({{ this.snake_name }}_name_ref()) 32 | {% else %} 33 | return __bindgen_gde.classdb_construct_object({{ this.snake_name }}_name_ref()) 34 | {% end %} 35 | } 36 | 37 | // methods 38 | {% for method in this.static_methods %} 39 | {% if return_type, has_return_type := method.return_type.(string); has_return_type %} 40 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 41 | {% for arg in method.args %} 42 | {{ arg.name }}_: {{ arg.type }}, 43 | {% end %} 44 | ) -> (ret: {{ return_type }}) { 45 | {% for arg in method.args %} 46 | {{ arg.name }}_ := {{ arg.name }}_ 47 | {% end %} 48 | args := []__bindgen_gde.TypePtr { 49 | {% for arg in method.args %} 50 | &{{ arg.name }}_, 51 | {% end %} 52 | } 53 | __bindgen_gde.object_method_bind_ptrcall(__{{ method.name }}_method_ptr, nil, raw_data(args), &ret) 54 | return 55 | } 56 | 57 | {% else %} 58 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 59 | {% for arg in method.args %} 60 | {{ arg.name }}_: {{ arg.type }}, 61 | {% end %} 62 | ) { 63 | {% for arg in method.args %} 64 | {{ arg.name }}_ := {{ arg.name }}_ 65 | {% end %} 66 | args := []__bindgen_gde.TypePtr { 67 | {% for arg in method.args %} 68 | &{{ arg.name }}_, 69 | {% end %} 70 | } 71 | __bindgen_gde.object_method_bind_ptrcall(__{{ method.name }}_method_ptr, nil, raw_data(args), nil) 72 | } 73 | 74 | {% end %} 75 | {% end %} 76 | 77 | {% for method in this.instance_methods %} 78 | {% if return_type, has_return_type := method.return_type.(string); has_return_type %} 79 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 80 | self: {{ this.self }}, 81 | {% for arg in method.args %} 82 | {{ arg.name }}_: {{ arg.type }}, 83 | {% end %} 84 | ) -> (ret: {{ return_type }}) { 85 | self := self 86 | {% for arg in method.args %} 87 | {{ arg.name }}_ := {{ arg.name }}_ 88 | {% end %} 89 | args := []__bindgen_gde.TypePtr { 90 | {% for arg in method.args %} 91 | &{{ arg.name }}_, 92 | {% end %} 93 | } 94 | __bindgen_gde.object_method_bind_ptrcall(__{{ method.name }}_method_ptr, &self, raw_data(args), &ret) 95 | return 96 | } 97 | 98 | {% else %} 99 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 100 | self: {{ this.self }}, 101 | {% for arg in method.args %} 102 | {{ arg.name }}_: {{ arg.type }}, 103 | {% end %} 104 | ) { 105 | self := self 106 | {% for arg in method.args %} 107 | {{ arg.name }}_ := {{ arg.name }}_ 108 | {% end %} 109 | args := []__bindgen_gde.TypePtr { 110 | {% for arg in method.args %} 111 | &{{ arg.name }}_, 112 | {% end %} 113 | } 114 | __bindgen_gde.object_method_bind_ptrcall(__{{ method.name }}_method_ptr, &self, raw_data(args), nil) 115 | } 116 | 117 | {% end %} 118 | {% end %} 119 | 120 | {{ this.snake_name}}_init :: proc "contextless" () { 121 | __class_name = new_string_name_cstring("{{ this.godot_name }}", true) 122 | {% if len(this.instance_methods) + len(this.static_methods) > 0 %} 123 | __name: String_Name 124 | {% end %} 125 | 126 | {% for method in this.instance_methods %} 127 | __name = new_string_name_cstring("{{ method.name }}", true) 128 | __{{ method.name }}_method_ptr = __bindgen_gde.classdb_get_method_bind(&__class_name, &__name, {{ i64(method.hash) }}) 129 | {% end %} 130 | {% for method in this.static_methods %} 131 | __name = new_string_name_cstring("{{ method.name }}", true) 132 | __{{ method.name }}_method_ptr = __bindgen_gde.classdb_get_method_bind(&__class_name, &__name, {{ i64(method.hash) }}) 133 | {% end %} 134 | } 135 | 136 | @(private = "file") 137 | __class_name: String_Name 138 | 139 | {% for method in this.instance_methods %} 140 | @(private = "file") 141 | __{{ method.name }}_method_ptr: __bindgen_gde.MethodBindPtr 142 | {% end %} 143 | {% for method in this.static_methods %} 144 | @(private = "file") 145 | __{{ method.name }}_method_ptr: __bindgen_gde.MethodBindPtr 146 | {% end %} -------------------------------------------------------------------------------- /bindgen/names/test_names.odin: -------------------------------------------------------------------------------- 1 | package names 2 | 3 | import "core:testing" 4 | 5 | Test_Pair :: struct($I: typeid, $E: typeid) { 6 | input: I, 7 | expect: E, 8 | } 9 | 10 | @(test) 11 | test_godot_to_odin :: proc(t: ^testing.T) { 12 | test_pairs := [?]Test_Pair(Godot_Name, Odin_Name) { 13 | {input = "Generic6DOFJoint3D", expect = "Generic6dof_Joint3d"}, 14 | {input = "Transform3D", expect = "Transform3d"}, 15 | {input = "Vector3", expect = "Vector3"}, 16 | {input = "AudioStreamMP3", expect = "Audio_Stream_Mp3"}, 17 | } 18 | 19 | for pair in test_pairs { 20 | result := godot_to_odin(pair.input) 21 | defer delete(cast(string)result) 22 | 23 | testing.expect_value(t, result, pair.expect) 24 | } 25 | } 26 | 27 | @(test) 28 | test_const_to_odin :: proc(t: ^testing.T) { 29 | test_pairs := [?]Test_Pair(Const_Name, Odin_Name) { 30 | {input = "GENERIC6DOF_JOINT3D", expect = "Generic6dof_Joint3d"}, 31 | {input = "TRANSFORM3D", expect = "Transform3d"}, 32 | {input = "VECTOR3", expect = "Vector3"}, 33 | {input = "AUDIO_STREAM_MP3", expect = "Audio_Stream_Mp3"}, 34 | } 35 | 36 | for pair in test_pairs { 37 | result := const_to_odin(pair.input) 38 | defer delete(cast(string)result) 39 | 40 | testing.expect_value(t, result, pair.expect) 41 | } 42 | } 43 | 44 | @(test) 45 | test_snake_to_odin :: proc(t: ^testing.T) { 46 | test_pairs := [?]Test_Pair(Snake_Name, Odin_Name) { 47 | {input = "generic6dof_joint3d", expect = "Generic6dof_Joint3d"}, 48 | {input = "transform3d", expect = "Transform3d"}, 49 | {input = "vector3", expect = "Vector3"}, 50 | {input = "audio_stream_mp3", expect = "Audio_Stream_Mp3"}, 51 | } 52 | 53 | for pair in test_pairs { 54 | result := snake_to_odin(pair.input) 55 | defer delete(cast(string)result) 56 | 57 | testing.expect_value(t, result, pair.expect) 58 | } 59 | } 60 | 61 | @(test) 62 | test_odin_to_snake :: proc(t: ^testing.T) { 63 | test_pairs := [?]Test_Pair(Odin_Name, Snake_Name) { 64 | {input = "Generic6dof_Joint3d", expect = "generic6dof_joint3d"}, 65 | {input = "Transform3d", expect = "transform3d"}, 66 | {input = "Vector3", expect = "vector3"}, 67 | {input = "Audio_Stream_Mp3", expect = "audio_stream_mp3"}, 68 | } 69 | 70 | for pair in test_pairs { 71 | result := odin_to_snake(pair.input) 72 | defer delete(cast(string)result) 73 | 74 | testing.expect_value(t, result, pair.expect) 75 | } 76 | } 77 | 78 | @(test) 79 | test_godot_to_snake :: proc(t: ^testing.T) { 80 | test_pairs := [?]Test_Pair(Godot_Name, Snake_Name) { 81 | {input = "Generic6DOFJoint3D", expect = "generic6dof_joint3d"}, 82 | {input = "Transform3D", expect = "transform3d"}, 83 | {input = "Vector3", expect = "vector3"}, 84 | {input = "AudioStreamMP3", expect = "audio_stream_mp3"}, 85 | } 86 | 87 | for pair in test_pairs { 88 | result := godot_to_snake(pair.input) 89 | defer delete(cast(string)result) 90 | 91 | testing.expect_value(t, result, pair.expect) 92 | } 93 | } 94 | 95 | @(test) 96 | test_godot_to_const :: proc(t: ^testing.T) { 97 | test_pairs := [?]Test_Pair(Godot_Name, Const_Name) { 98 | {input = "Generic6DOFJoint3D", expect = "GENERIC6DOF_JOINT3D"}, 99 | {input = "Transform3D", expect = "TRANSFORM3D"}, 100 | {input = "Vector3", expect = "VECTOR3"}, 101 | {input = "AudioStreamMP3", expect = "AUDIO_STREAM_MP3"}, 102 | } 103 | 104 | for pair in test_pairs { 105 | result := godot_to_const(pair.input) 106 | defer delete(cast(string)result) 107 | 108 | testing.expect_value(t, result, pair.expect) 109 | } 110 | } 111 | 112 | @(test) 113 | test_odin_to_const :: proc(t: ^testing.T) { 114 | test_pairs := [?]Test_Pair(Odin_Name, Const_Name) { 115 | {input = "Generic6dof_Joint3D", expect = "GENERIC6DOF_JOINT3D"}, 116 | {input = "Transform3d", expect = "TRANSFORM3D"}, 117 | {input = "Vector3", expect = "VECTOR3"}, 118 | {input = "Audio_Stream_Mp3", expect = "AUDIO_STREAM_MP3"}, 119 | } 120 | 121 | for pair in test_pairs { 122 | result := odin_to_const(pair.input) 123 | defer delete(cast(string)result) 124 | 125 | testing.expect_value(t, result, pair.expect) 126 | } 127 | } 128 | 129 | /* 130 | Copyright 2025 Dresses Digital 131 | 132 | Licensed under the Apache License, Version 2.0 (the "License"); 133 | you may not use this file except in compliance with the License. 134 | You may obtain a copy of the License at 135 | 136 | http://www.apache.org/licenses/LICENSE-2.0 137 | 138 | Unless required by applicable law or agreed to in writing, software 139 | distributed under the License is distributed on an "AS IS" BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 141 | See the License for the specific language governing permissions and 142 | limitations under the License. 143 | */ 144 | -------------------------------------------------------------------------------- /bindgen/gen.odin: -------------------------------------------------------------------------------- 1 | #+private 2 | package bindgen 3 | 4 | import "../temple" 5 | import "base:runtime" 6 | import "core:fmt" 7 | import "core:io" 8 | import "core:mem" 9 | import "core:os" 10 | import "core:thread" 11 | 12 | import g "graph" 13 | import "views" 14 | 15 | @(private = "file") 16 | UNIX_ALLOW_READ_WRITE_ALL :: 0o666 17 | 18 | open_write_template :: proc(file_path: string, view: $T, template: temple.Compiled(T)) { 19 | fhandle, ferr := os.open(file_path, os.O_CREATE | os.O_TRUNC | os.O_RDWR, UNIX_ALLOW_READ_WRITE_ALL) 20 | if ferr != 0 { 21 | fmt.eprintfln("Error opening %v", file_path) 22 | return 23 | } 24 | defer { 25 | os.flush(fhandle) 26 | os.close(fhandle) 27 | } 28 | 29 | fstream := os.stream_from_handle(fhandle) 30 | flusher, flusher_ok := io.to_flusher(fstream) 31 | defer if flusher_ok { 32 | io.flush(flusher) 33 | } 34 | 35 | _, terr := template.with(fstream, view) 36 | if terr != nil { 37 | fmt.eprintfln("Error writing template %v (%v): %v", typeid_of(T), file_path, terr) 38 | } 39 | } 40 | 41 | codegen_godot :: proc(task: thread.Task) { 42 | graph := cast(^g.Graph)task.data 43 | 44 | tracking_alloc: mem.Tracking_Allocator 45 | mem.tracking_allocator_init(&tracking_alloc, context.allocator) 46 | 47 | allocator := mem.tracking_allocator(&tracking_alloc) 48 | 49 | view := views.godot_package(graph, allocator = allocator) 50 | open_write_template("godot/godot.gen.odin", view, core_template) 51 | 52 | free_all(allocator) 53 | } 54 | 55 | codegen_engine_class :: proc(task: thread.Task) { 56 | class := cast(^g.Engine_Class)task.data 57 | 58 | tracking_alloc: mem.Tracking_Allocator 59 | mem.tracking_allocator_init(&tracking_alloc, context.allocator) 60 | 61 | allocator := mem.tracking_allocator(&tracking_alloc) 62 | if view, should_render := views.engine_class(class, allocator = allocator); should_render { 63 | file_path := fmt.aprintf("godot/%v.gen.odin", view.snake_name, allocator = allocator) 64 | open_write_template(file_path, view, engine_class_template) 65 | } 66 | 67 | free_all(allocator) 68 | } 69 | 70 | codegen_native_structs :: proc(task: thread.Task) { 71 | graph := cast(^g.Graph)task.data 72 | 73 | tracking_alloc: mem.Tracking_Allocator 74 | mem.tracking_allocator_init(&tracking_alloc, context.allocator) 75 | 76 | allocator := mem.tracking_allocator(&tracking_alloc) 77 | 78 | view := views.native_structs(graph, allocator = allocator) 79 | open_write_template("godot/structs.gen.odin", view, structs_template) 80 | 81 | free_all(allocator) 82 | } 83 | 84 | codegen_variant :: proc(task: thread.Task) { 85 | class := cast(^g.Builtin_Class)task.data 86 | 87 | tracking_alloc: mem.Tracking_Allocator 88 | mem.tracking_allocator_init(&tracking_alloc, context.allocator) 89 | 90 | allocator := mem.tracking_allocator(&tracking_alloc) 91 | if view, should_render := views.variant(class, allocator = allocator); should_render { 92 | file_path := fmt.aprintf("godot/%v.gen.odin", view.name, allocator = allocator) 93 | open_write_template(file_path, view, variant_template) 94 | } 95 | 96 | free_all(allocator) 97 | } 98 | 99 | generate_bindings :: proc(graph: g.Graph, options: Options) { 100 | graph := graph 101 | 102 | views.map_types_to_imports(graph, options.map_mode) 103 | 104 | if options.job_count == 1 { 105 | codegen_godot(thread.Task{data = &graph})\ 106 | codegen_native_structs(thread.Task{data = &graph}) 107 | for &builtin_class in graph.builtin_classes { 108 | codegen_variant(thread.Task{data = &builtin_class}) 109 | } 110 | 111 | for &engine_class in graph.engine_classes { 112 | codegen_engine_class(thread.Task{data = &engine_class}) 113 | } 114 | 115 | return 116 | } 117 | 118 | thread_pool: thread.Pool 119 | thread.pool_init(&thread_pool, context.allocator, options.job_count) 120 | thread.pool_add_task(&thread_pool, context.allocator, codegen_godot, &graph) 121 | thread.pool_add_task(&thread_pool, context.allocator, codegen_native_structs, &graph) 122 | for &builtin_class in graph.builtin_classes { 123 | thread.pool_add_task(&thread_pool, context.allocator, codegen_variant, &builtin_class) 124 | } 125 | 126 | for &engine_class in graph.engine_classes { 127 | thread.pool_add_task(&thread_pool, context.allocator, codegen_engine_class, &engine_class) 128 | } 129 | 130 | thread.pool_start(&thread_pool) 131 | thread.pool_finish(&thread_pool) 132 | } 133 | 134 | /* 135 | Copyright 2025 Dresses Digital 136 | 137 | Licensed under the Apache License, Version 2.0 (the "License"); 138 | you may not use this file except in compliance with the License. 139 | You may obtain a copy of the License at 140 | 141 | http://www.apache.org/licenses/LICENSE-2.0 142 | 143 | Unless required by applicable law or agreed to in writing, software 144 | distributed under the License is distributed on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 146 | See the License for the specific language governing permissions and 147 | limitations under the License. 148 | */ 149 | -------------------------------------------------------------------------------- /examples/tests/src/test_class.odin: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "godot:gdext" 4 | import "godot:libgd/classdb" 5 | import "godot:godot" 6 | 7 | @(private = "file") 8 | test_class_name: godot.String_Name 9 | 10 | @(private = "file") 11 | node_class_name: godot.String_Name 12 | 13 | @(private = "file") 14 | object_class_name: godot.String_Name 15 | 16 | Test_Class :: struct { 17 | object: gdext.ObjectPtr, 18 | vector: godot.Vector2, 19 | } 20 | 21 | @(private = "file") 22 | set_vector :: proc "contextless" (self: ^Test_Class, vector: godot.Vector2) { 23 | self.vector = vector 24 | } 25 | 26 | @(private = "file") 27 | get_vector :: proc "contextless" (self: ^Test_Class) -> godot.Vector2 { 28 | return self.vector 29 | } 30 | 31 | @(private = "file") 32 | vector_eq :: proc "contextless" (self: ^Test_Class, other: godot.Vector2) -> bool { 33 | return self.vector == other 34 | } 35 | 36 | @(private = "file") 37 | vector_neq :: proc "contextless" (self: ^Test_Class, other: godot.Vector2) -> bool { 38 | return self.vector != other 39 | } 40 | 41 | @(private = "file") 42 | vector_add :: proc "contextless" (self: ^Test_Class, other: godot.Vector2) -> godot.Vector2 { 43 | return self.vector + other 44 | } 45 | 46 | @(private = "file") 47 | vector_sub :: proc "contextless" (self: ^Test_Class, other: godot.Vector2) -> godot.Vector2 { 48 | return self.vector - other 49 | } 50 | 51 | @(private = "file") 52 | get_virtual_with_data :: proc "c" (class_user_data: rawptr, name: gdext.StringNamePtr) -> rawptr { 53 | return nil 54 | } 55 | 56 | @(private = "file") 57 | call_virtual_with_data :: proc "c" ( 58 | instance: gdext.ExtensionClassInstancePtr, 59 | name: gdext.StringNamePtr, 60 | virtual_call_user_data: rawptr, 61 | args: [^]gdext.TypePtr, 62 | ret: gdext.TypePtr, 63 | ) {} 64 | 65 | @(private = "file") 66 | binding_callbacks := gdext.InstanceBindingCallbacks{} 67 | 68 | @(private = "file") 69 | create_instance :: proc "c" (class_user_data: rawptr) -> gdext.ObjectPtr { 70 | context = gdext.godot_context() 71 | 72 | object := gdext.classdb_construct_object(&node_class_name) 73 | 74 | self := new(Test_Class) 75 | self.object = object 76 | 77 | gdext.object_set_instance(object, &test_class_name, self) 78 | gdext.object_set_instance_binding(object, gdext.library, self, &binding_callbacks) 79 | 80 | return object 81 | } 82 | 83 | @(private = "file") 84 | free_instance :: proc "c" (class_user_data: rawptr, instance: gdext.ExtensionClassInstancePtr) { 85 | context = gdext.godot_context() 86 | 87 | if instance == nil { 88 | return 89 | } 90 | 91 | self := cast(^Test_Class)instance 92 | free(self) 93 | } 94 | 95 | test_class_register :: proc() { 96 | test_class_name = godot.new_string_name_cstring("Test", true) 97 | node_class_name = godot.new_string_name_cstring("Node", true) 98 | object_class_name = godot.new_string_name_cstring("Object", true) 99 | 100 | class_info := gdext.ExtensionClassCreationInfo2 { 101 | is_virtual = false, 102 | is_abstract = false, 103 | is_exposed = true, 104 | set_func = nil, 105 | get_func = nil, 106 | get_property_list_func = nil, 107 | free_property_list_func = nil, 108 | property_can_revert_func = nil, 109 | property_get_revert_func = nil, 110 | validate_property_func = nil, 111 | notification_func = nil, 112 | to_string_func = nil, 113 | reference_func = nil, 114 | unreference_func = nil, 115 | create_instance_func = create_instance, 116 | free_instance_func = free_instance, 117 | recreate_instance_func = nil, 118 | get_virtual_func = nil, 119 | get_virtual_call_data_func = get_virtual_with_data, 120 | call_virtual_with_data_func = call_virtual_with_data, 121 | get_rid_func = nil, 122 | class_userdata = nil, 123 | } 124 | 125 | gdext.classdb_register_extension_class2(gdext.library, &test_class_name, &node_class_name, &class_info) 126 | 127 | get_vector_name := godot.new_string_name_cstring("get_vector", true) 128 | set_vector_name := godot.new_string_name_cstring("set_vector", true) 129 | vector_name := godot.new_string_name_cstring("vector", true) 130 | classdb.bind_property_and_methods( 131 | &test_class_name, 132 | &vector_name, 133 | &get_vector_name, 134 | &set_vector_name, 135 | get_vector, 136 | set_vector, 137 | ) 138 | 139 | other_name := godot.new_string_name_cstring("other", true) 140 | vector_eq_name := godot.new_string_name_cstring("vector_eq", true) 141 | classdb.bind_returning_method(&test_class_name, &vector_eq_name, vector_eq, &other_name) 142 | 143 | vector_neq_name := godot.new_string_name_cstring("vector_neq", true) 144 | classdb.bind_returning_method(&test_class_name, &vector_neq_name, vector_neq, &other_name) 145 | 146 | vector_add_name := godot.new_string_name_cstring("vector_add", true) 147 | classdb.bind_returning_method(&test_class_name, &vector_add_name, vector_add, &other_name) 148 | 149 | vector_sub_name := godot.new_string_name_cstring("vector_sub", true) 150 | classdb.bind_returning_method(&test_class_name, &vector_sub_name, vector_sub, &other_name) 151 | } 152 | -------------------------------------------------------------------------------- /bindgen/views/engine_packages.odin: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import g "../graph" 4 | import "../names" 5 | import "core:mem" 6 | import "core:slice" 7 | import "core:strings" 8 | 9 | Package_Class :: struct { 10 | name: string, 11 | snake_name: string, 12 | derives: string, 13 | } 14 | 15 | Godot_Package :: struct { 16 | classes: []Package_Class, 17 | inits: []string, 18 | functions: []Function, 19 | enums: []Enum, 20 | bit_fields: []Bit_Field, 21 | singletons: []Singleton, 22 | } 23 | 24 | Function :: struct { 25 | name: string, 26 | godot_name: string, 27 | hash: i64, 28 | args: []Function_Arg, 29 | return_type: Maybe(string), 30 | } 31 | 32 | Function_Arg :: struct { 33 | name: string, 34 | type: string, 35 | } 36 | 37 | Singleton :: struct { 38 | name: string, 39 | snake_name: string, 40 | type: string, 41 | } 42 | 43 | godot_package :: proc(graph: ^g.Graph, allocator: mem.Allocator) -> (core: Godot_Package) { 44 | context.allocator = allocator 45 | 46 | // some types are declared as builtins in variant/ instead of as classes in core/ 47 | core_class_count := len(graph.engine_classes) - len(declared_builtins) 48 | 49 | // taking some off since we're going to skip enums in gdextension_enums 50 | enum_count := len(graph.enums) - len(gdextension_enums) 51 | 52 | core = Godot_Package { 53 | classes = make([]Package_Class, core_class_count), 54 | inits = make([]string, len(graph.engine_classes)), 55 | functions = make([]Function, len(graph.util_procs)), 56 | enums = make([]Enum, enum_count), 57 | bit_fields = make([]Bit_Field, len(graph.bit_fields)), 58 | singletons = make([]Singleton, len(graph.singletons)), 59 | } 60 | 61 | class_idx := 0 62 | for class, init_idx in graph.engine_classes { 63 | core.inits[init_idx] = names.clone_string(class.snake_name) 64 | 65 | if slice.contains(declared_builtins, class.godot_name) { 66 | continue 67 | } 68 | 69 | core.classes[class_idx] = Package_Class { 70 | name = names.clone_string(class.odin_name), 71 | snake_name = names.clone_string(class.snake_name), 72 | derives = resolve_qualified_type(class.inherits, "godot:core"), 73 | } 74 | class_idx += 1 75 | } 76 | 77 | for util_proc, proc_idx in graph.util_procs { 78 | function := Function { 79 | name = strings.clone(util_proc.name), 80 | godot_name = strings.clone(util_proc.name), 81 | hash = util_proc.hash, 82 | args = make([]Function_Arg, len(util_proc.args)), 83 | } 84 | 85 | if util_proc.return_type != nil { 86 | function.return_type = resolve_qualified_type(util_proc.return_type, "godot:core") 87 | } 88 | 89 | for arg, arg_idx in util_proc.args { 90 | function.args[arg_idx] = Function_Arg { 91 | name = strings.clone(arg.name), 92 | type = resolve_qualified_type(arg.type, "godot:core"), 93 | // TODO: default values? 94 | } 95 | } 96 | 97 | core.functions[proc_idx] = function 98 | } 99 | 100 | enum_idx := 0 101 | for graph_enum in graph.enums { 102 | // skip over enums that are defined in gdextension 103 | if slice.contains(gdextension_enums, graph_enum.godot_name) { 104 | continue 105 | } 106 | 107 | new_enum := Enum { 108 | name = names.clone_string(graph_enum.odin_name), 109 | values = make([]Enum_Value, len(graph_enum.values)), 110 | } 111 | 112 | for value, value_idx in graph_enum.values { 113 | new_enum.values[value_idx] = Enum_Value { 114 | name = names.clone_string(value.odin_name), 115 | value = strings.clone(value.value), 116 | } 117 | } 118 | 119 | core.enums[enum_idx] = new_enum 120 | enum_idx += 1 121 | } 122 | 123 | for graph_bit_field, enum_idx in graph.bit_fields { 124 | new_bit_field := Bit_Field { 125 | name = names.clone_string(graph_bit_field.odin_name), 126 | values = make([]Enum_Value, len(graph_bit_field.values)), 127 | } 128 | 129 | for value, value_idx in graph_bit_field.values { 130 | new_bit_field.values[value_idx] = Enum_Value { 131 | name = names.clone_string(value.odin_name), 132 | value = strings.clone(value.value), 133 | } 134 | } 135 | 136 | core.bit_fields[enum_idx] = new_bit_field 137 | } 138 | 139 | for singleton, singleton_idx in graph.singletons { 140 | core.singletons[singleton_idx] = Singleton { 141 | name = names.clone_string(singleton.odin_name), 142 | snake_name = names.clone_string(singleton.snake_name), 143 | type = resolve_qualified_type(singleton.type, "godot:godot"), 144 | } 145 | } 146 | 147 | return 148 | } 149 | 150 | /* 151 | Copyright 2025 Dresses Digital 152 | 153 | Licensed under the Apache License, Version 2.0 (the "License"); 154 | you may not use this file except in compliance with the License. 155 | You may obtain a copy of the License at 156 | 157 | http://www.apache.org/licenses/LICENSE-2.0 158 | 159 | Unless required by applicable law or agreed to in writing, software 160 | distributed under the License is distributed on an "AS IS" BASIS, 161 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 162 | See the License for the specific language governing permissions and 163 | limitations under the License. 164 | */ 165 | -------------------------------------------------------------------------------- /examples/game/csrc/lib.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lib.h" 4 | #include "player.h" 5 | 6 | GDExtensionClassLibraryPtr ext_library; 7 | 8 | // ext interface 9 | GDExtensionInterfaceGetProcAddress ext_get_proc_address; 10 | GDExtensionInterfaceMemAlloc gd_alloc; 11 | GDExtensionInterfaceMemFree gd_free; 12 | GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars; 13 | GDExtensionInterfaceStringNewWithLatin1Chars string_new_with_latin1_chars; 14 | GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind; 15 | GDExtensionInterfaceGlobalGetSingleton global_get_singleton; 16 | GDExtensionInterfaceClassdbConstructObject classdb_construct_object; 17 | GDExtensionInterfaceObjectSetInstance object_set_instance; 18 | GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding; 19 | GDExtensionInterfaceClassdbRegisterExtensionClass3 classdb_register_extension_class3; 20 | GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall; 21 | GDExtensionInterfaceVariantGetPtrUtilityFunction variant_get_ptr_utility_function; 22 | GDExtensionInterfaceVariantGetPtrOperatorEvaluator variant_get_ptr_operator_evaluator; 23 | GDExtensionInterfaceVariantGetPtrConstructor variant_get_ptr_constructor; 24 | GDExtensionInterfaceGetVariantFromTypeConstructor get_variant_from_type_constructor; 25 | 26 | // utils 27 | GDExtensionPtrUtilityFunction print_ptr; 28 | 29 | // constructors 30 | GDExtensionPtrConstructor node_path_from_string; 31 | 32 | // hashes & method binds 33 | const GDExtensionInt get_node_hash = 2734337346; 34 | GDExtensionMethodBindPtr get_node_method_bind; 35 | 36 | // operators 37 | GDExtensionPtrOperatorEvaluator StringName_eq_StringName_op; 38 | 39 | void __cdecl initialize_game_module(void* user_data, GDExtensionInitializationLevel level) { 40 | if (level != GDEXTENSION_INITIALIZATION_SCENE) { 41 | return; 42 | } 43 | 44 | gd_alloc = (GDExtensionInterfaceMemAlloc)ext_get_proc_address("mem_alloc"); 45 | gd_free = (GDExtensionInterfaceMemFree)ext_get_proc_address("mem_free"); 46 | string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)ext_get_proc_address("string_name_new_with_latin1_chars"); 47 | string_new_with_latin1_chars = (GDExtensionInterfaceStringNewWithLatin1Chars)ext_get_proc_address("string_new_with_latin1_chars"); 48 | classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)ext_get_proc_address("classdb_get_method_bind"); 49 | global_get_singleton = (GDExtensionInterfaceGlobalGetSingleton)ext_get_proc_address("global_get_singleton"); 50 | classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)ext_get_proc_address("classdb_construct_object"); 51 | object_set_instance = (GDExtensionInterfaceObjectSetInstance)ext_get_proc_address("object_set_instance"); 52 | object_set_instance_binding = (GDExtensionInterfaceObjectSetInstanceBinding)ext_get_proc_address("object_set_instance_binding"); 53 | classdb_register_extension_class3 = (GDExtensionInterfaceClassdbRegisterExtensionClass3)ext_get_proc_address("classdb_register_extension_class3"); 54 | object_method_bind_ptrcall = (GDExtensionInterfaceObjectMethodBindPtrcall)ext_get_proc_address("object_method_bind_ptrcall"); 55 | variant_get_ptr_utility_function = (GDExtensionInterfaceVariantGetPtrUtilityFunction)ext_get_proc_address("variant_get_ptr_utility_function"); 56 | variant_get_ptr_operator_evaluator = (GDExtensionInterfaceVariantGetPtrOperatorEvaluator)ext_get_proc_address("variant_get_ptr_operator_evaluator"); 57 | variant_get_ptr_constructor = (GDExtensionInterfaceVariantGetPtrConstructor)ext_get_proc_address("variant_get_ptr_constructor"); 58 | get_variant_from_type_constructor = (GDExtensionInterfaceGetVariantFromTypeConstructor)ext_get_proc_address("get_variant_from_type_constructor"); 59 | 60 | { 61 | StringName node_classname; 62 | string_name_new_with_latin1_chars(&node_classname, "Node", true); 63 | 64 | StringName get_node_methodname; 65 | string_name_new_with_latin1_chars(&get_node_methodname, "get_node", true); 66 | 67 | get_node_method_bind = classdb_get_method_bind(&node_classname, &get_node_methodname, get_node_hash); 68 | } 69 | 70 | StringName_eq_StringName_op = variant_get_ptr_operator_evaluator(GDEXTENSION_VARIANT_OP_EQUAL, GDEXTENSION_VARIANT_TYPE_STRING_NAME, GDEXTENSION_VARIANT_TYPE_STRING_NAME); 71 | node_path_from_string = variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_NODE_PATH, 2); 72 | 73 | { 74 | StringName print_name; 75 | string_name_new_with_latin1_chars(&print_name, "print", true); 76 | 77 | print_ptr = variant_get_ptr_utility_function(&print_name, 2648703342); 78 | } 79 | 80 | player_class_register(); 81 | } 82 | 83 | void __cdecl deinitialize_game_module(void* user_data, GDExtensionInitializationLevel level) { 84 | if (level != GDEXTENSION_INITIALIZATION_SCENE) { 85 | return; 86 | } 87 | } 88 | 89 | __declspec(dllexport) GDExtensionBool __cdecl game_init( 90 | GDExtensionInterfaceGetProcAddress get_proc_address, 91 | GDExtensionClassLibraryPtr library, 92 | GDExtensionInitialization* initialization) 93 | { 94 | ext_get_proc_address = get_proc_address; 95 | ext_library = library; 96 | 97 | initialization->initialize = initialize_game_module; 98 | initialization->deinitialize = deinitialize_game_module; 99 | initialization->userdata = NULL; 100 | initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_CORE; 101 | 102 | return 1; 103 | } 104 | 105 | GDExtensionBool call_builtin_op_bool(GDExtensionPtrOperatorEvaluator op, GDExtensionConstTypePtr a, GDExtensionConstTypePtr b) { 106 | GDExtensionBool result; 107 | op(a, b, &result); 108 | return result; 109 | } 110 | 111 | void print_object(GDExtensionTypePtr object) { 112 | GDExtensionVariantFromTypeConstructorFunc var_from_obj = get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_OBJECT); 113 | Variant as_variant; 114 | var_from_obj(&as_variant, object); 115 | 116 | GDExtensionConstTypePtr args[1] = { &as_variant }; 117 | print_ptr(NULL, args, 1); 118 | } 119 | -------------------------------------------------------------------------------- /libgd/classdb/bind.odin: -------------------------------------------------------------------------------- 1 | package libgd_classdb 2 | 3 | import "base:intrinsics" 4 | import "godot:gdext" 5 | import "godot:godot" 6 | 7 | simple_property_info :: proc "contextless" (type: gdext.Variant_Type, name: ^godot.String_Name) -> gdext.PropertyInfo { 8 | return gdext.PropertyInfo { 9 | name = name, 10 | type = type, 11 | hint = 0, // .None 12 | hint_string = godot.string_empty_ref(), 13 | class_name = godot.string_name_empty_ref(), 14 | usage = 0, // .Default 15 | } 16 | } 17 | 18 | expect_args :: proc "contextless" ( 19 | args: [^]gdext.VariantPtr, 20 | arg_count: i64, 21 | error: ^gdext.CallError, 22 | arg_types: ..gdext.Variant_Type, 23 | ) -> bool { 24 | if arg_count < cast(i64)len(arg_types) { 25 | error.error = .Too_Few_Arguments 26 | error.expected = cast(i32)len(arg_types) 27 | return false 28 | } 29 | 30 | if arg_count > cast(i64)len(arg_types) { 31 | error.error = .Too_Many_Arguments 32 | error.expected = cast(i32)len(arg_types) 33 | return false 34 | } 35 | 36 | for arg_type, arg_idx in arg_types { 37 | type := gdext.variant_get_type(cast(^godot.Variant)args[arg_idx]) 38 | if type != arg_type { 39 | error.error = .Invalid_Argument 40 | error.expected = cast(i32)arg_type 41 | error.argument = cast(i32)arg_idx 42 | return false 43 | } 44 | } 45 | 46 | return true 47 | } 48 | 49 | bind_property_group :: proc(class_name: string, name: string, prefix: string) { 50 | class_name := godot.new_string_odin(class_name) 51 | defer godot.free_string(class_name) 52 | 53 | name_str := godot.new_string_odin(name) 54 | defer godot.free_string(name_str) 55 | 56 | prefix_str := godot.new_string_odin(prefix) 57 | defer godot.free_string(prefix_str) 58 | 59 | gdext.classdb_register_extension_class_property_group(gdext.library, &class_name, &name_str, &prefix_str) 60 | } 61 | 62 | bind_property_subgroup :: proc(class_name: string, name: string, prefix: string) { 63 | class_name := godot.new_string_odin(class_name) 64 | defer godot.free_string(class_name) 65 | 66 | name_str := godot.new_string_odin(name) 67 | defer godot.free_string(name_str) 68 | 69 | prefix_str := godot.new_string_odin(prefix) 70 | defer godot.free_string(prefix_str) 71 | 72 | gdext.classdb_register_extension_class_property_subgroup(gdext.library, &class_name, &name_str, &prefix_str) 73 | } 74 | 75 | bind_property_and_methods :: proc { 76 | bind_property_and_methods_cstring, 77 | bind_property_and_methods_gdstringname, 78 | } 79 | 80 | bind_property_and_methods_cstring :: proc( 81 | class_name: cstring, 82 | name: cstring, 83 | getter_name: cstring, 84 | setter_name: cstring, 85 | getter: proc "contextless" (self: ^$Self) -> $Value, 86 | setter: proc "contextless" (self: ^Self, value: Value), 87 | static_strings := true, 88 | ) { 89 | class_name := godot.new_string_name_cstring(class_name, static_strings) 90 | defer if !static_strings { 91 | godot.free_string_name(class_name) 92 | } 93 | 94 | name := godot.new_string_name_cstring(name, static_strings) 95 | defer if !static_strings { 96 | godot.free_string_name(name) 97 | } 98 | 99 | getter_name := godot.new_string_name_cstring(getter_name, static_strings) 100 | defer if !static_strings { 101 | godot.free_string_name(getter_name) 102 | } 103 | 104 | setter_name := godot.new_string_name_cstring(setter_name, static_strings) 105 | defer if !static_strings { 106 | godot.free_string_name(setter_name) 107 | } 108 | 109 | bind_property_and_methods_gdstringname(&class_name, &name, &getter_name, &setter_name, getter, setter) 110 | } 111 | 112 | bind_property_and_methods_gdstringname :: proc( 113 | class_name: ^godot.String_Name, 114 | name: ^godot.String_Name, 115 | getter_name: ^godot.String_Name, 116 | setter_name: ^godot.String_Name, 117 | getter: proc "contextless" (self: ^$Self) -> $Value, 118 | setter: proc "contextless" (self: ^Self, value: Value), 119 | ) { 120 | bind_returning_method_0_args(class_name, getter_name, getter) 121 | bind_void_method_1_args(class_name, setter_name, setter, name) 122 | 123 | type := godot.variant_type(Value) 124 | info := simple_property_info(type, name) 125 | gdext.classdb_register_extension_class_property(gdext.library, class_name, &info, setter_name, getter_name) 126 | } 127 | 128 | bind_property :: proc( 129 | class_name: ^godot.String_Name, 130 | name: ^godot.String_Name, 131 | type: gdext.Variant_Type, 132 | getter: ^godot.String_Name, 133 | setter: ^godot.String_Name, 134 | ) { 135 | info := simple_property_info(type, name) 136 | gdext.classdb_register_extension_class_property(gdext.library, class_name, &info, setter, getter) 137 | } 138 | 139 | Signal_Arg :: struct { 140 | name: ^godot.String_Name, 141 | type: gdext.Variant_Type, 142 | } 143 | 144 | bind_signal :: proc(class_name: ^godot.String_Name, signal_name: ^godot.String_Name, args: ..Signal_Arg) { 145 | if len(args) == 0 { 146 | gdext.classdb_register_extension_class_signal(gdext.library, class_name, signal_name, nil, 0) 147 | return 148 | } 149 | 150 | args_info := make([]gdext.PropertyInfo, len(args)) 151 | defer delete(args_info) 152 | 153 | for arg, idx in args { 154 | args_info[idx] = simple_property_info(arg.type, arg.name) 155 | } 156 | 157 | gdext.classdb_register_extension_class_signal( 158 | gdext.library, 159 | class_name, 160 | signal_name, 161 | raw_data(args_info), 162 | cast(i64)len(args), 163 | ) 164 | } 165 | 166 | /* 167 | Copyright 2025 Dresses Digital 168 | 169 | Licensed under the Apache License, Version 2.0 (the "License"); 170 | you may not use this file except in compliance with the License. 171 | You may obtain a copy of the License at 172 | 173 | http://www.apache.org/licenses/LICENSE-2.0 174 | 175 | Unless required by applicable law or agreed to in writing, software 176 | distributed under the License is distributed on an "AS IS" BASIS, 177 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 178 | See the License for the specific language governing permissions and 179 | limitations under the License. 180 | */ 181 | -------------------------------------------------------------------------------- /examples/hello-gdextension/src/example_class.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "godot:gdext" 4 | import "godot:libgd/classdb" 5 | import "godot:godot" 6 | 7 | @(private = "file") 8 | example_class_name: godot.String_Name 9 | time_passed_name := godot.String_Name{} 10 | emit_signal_name := godot.String_Name{} 11 | 12 | ExampleClass :: struct { 13 | amplitude: f64, 14 | speed: f64, 15 | time_emit: f64, 16 | time_passed: f64, 17 | object: ^godot.Object, 18 | } 19 | 20 | set_amplitude :: proc "contextless" (self: ^ExampleClass, amplitude: f64) { 21 | self.amplitude = amplitude 22 | } 23 | 24 | get_amplitude :: proc "contextless" (self: ^ExampleClass) -> f64 { 25 | return self.amplitude 26 | } 27 | 28 | set_speed :: proc "contextless" (self: ^ExampleClass, speed: f64) { 29 | self.speed = speed 30 | 31 | msg := godot.new_string_cstring("set_speed") 32 | defer godot.free_string(msg) 33 | 34 | godot.gd_print(godot.variant_from(&msg)) 35 | godot.gd_print(godot.variant_from(&self.speed)) 36 | } 37 | 38 | get_speed :: proc "contextless" (self: ^ExampleClass) -> f64 { 39 | return self.speed 40 | } 41 | 42 | process :: proc "contextless" (self: ^ExampleClass, delta: f64) { 43 | self.time_passed += self.speed * delta 44 | 45 | if self.time_passed >= self.time_emit { 46 | emit_time_passed(self, self.time_passed) 47 | self.time_passed -= self.time_emit 48 | } 49 | } 50 | 51 | emit_time_passed :: proc "contextless" (self: ^ExampleClass, time_passed: f64) { 52 | object_emit_signal := gdext.classdb_get_method_bind( 53 | godot.object_name_ref(), 54 | &emit_signal_name, 55 | 4047867050, 56 | ) 57 | 58 | signal_name_argument := godot.variant_from(&time_passed_name) 59 | time_passed_argument := godot.variant_from(&self.time_passed) 60 | 61 | args := [2]gdext.VariantPtr{&signal_name_argument, &time_passed_argument} 62 | ret := godot.Variant{} 63 | gdext.object_method_bind_call(object_emit_signal, self.object, &args[0], len(args), &ret, nil) 64 | defer gdext.variant_destroy(&ret) 65 | } 66 | 67 | get_virtual_with_data :: proc "c" (class_user_data: rawptr, name: gdext.StringNamePtr) -> rawptr { 68 | name := cast(^godot.String_Name)name 69 | process_name := godot.new_string_name_cstring("_process", true) 70 | if godot.string_name_equal(name^, process_name) { 71 | return cast(rawptr)process 72 | } 73 | 74 | return nil 75 | } 76 | 77 | call_virtual_with_data :: proc "c" ( 78 | instance: gdext.ExtensionClassInstancePtr, 79 | name: gdext.StringNamePtr, 80 | virtual_call_user_data: rawptr, 81 | args: [^]gdext.TypePtr, 82 | ret: gdext.TypePtr, 83 | ) { 84 | if virtual_call_user_data == cast(rawptr)process { 85 | delta := cast(^f64)args[0] 86 | process(cast(^ExampleClass)instance, delta^) 87 | } 88 | } 89 | 90 | example_class_binding_callbacks := gdext.InstanceBindingCallbacks { 91 | create = nil, 92 | free = nil, 93 | reference = nil, 94 | } 95 | 96 | create_instance :: proc "c" (class_user_data: rawptr) -> gdext.ObjectPtr { 97 | context = gdext.godot_context() 98 | 99 | object := gdext.classdb_construct_object(godot.sprite2d_name_ref()) 100 | 101 | self := new_clone( 102 | ExampleClass { 103 | object = cast(^godot.Object)object, 104 | amplitude = 1.0, 105 | speed = 1.0, 106 | time_passed = 0.0, 107 | time_emit = 5.0, 108 | }, 109 | ) 110 | 111 | gdext.object_set_instance(object, &example_class_name, self) 112 | gdext.object_set_instance_binding(object, gdext.library, self, &example_class_binding_callbacks) 113 | 114 | return object 115 | } 116 | 117 | free_instance :: proc "c" (class_user_data: rawptr, instance: gdext.ExtensionClassInstancePtr) { 118 | context = gdext.godot_context() 119 | 120 | if instance == nil { 121 | return 122 | } 123 | 124 | self := cast(^ExampleClass)instance 125 | free(self) 126 | } 127 | 128 | example_class_register :: proc() { 129 | // we use string_name_new_with_latin1_chars because we know the lifetime of the string literal to be static 130 | gdext.string_name_new_with_latin1_chars(&example_class_name, "ExampleClass", true) 131 | gdext.string_name_new_with_latin1_chars(&time_passed_name, "time_passed", true) 132 | gdext.string_name_new_with_latin1_chars(&emit_signal_name, "emit_signal", true) 133 | 134 | class_info := gdext.ExtensionClassCreationInfo2 { 135 | is_virtual = false, 136 | is_abstract = false, 137 | is_exposed = true, 138 | set_func = nil, 139 | get_func = nil, 140 | get_property_list_func = nil, 141 | free_property_list_func = nil, 142 | property_can_revert_func = nil, 143 | property_get_revert_func = nil, 144 | validate_property_func = nil, 145 | notification_func = nil, 146 | to_string_func = nil, 147 | reference_func = nil, 148 | unreference_func = nil, 149 | create_instance_func = create_instance, 150 | free_instance_func = free_instance, 151 | recreate_instance_func = nil, 152 | get_virtual_func = nil, 153 | get_virtual_call_data_func = get_virtual_with_data, 154 | call_virtual_with_data_func = call_virtual_with_data, 155 | get_rid_func = nil, 156 | class_userdata = nil, 157 | } 158 | 159 | gdext.classdb_register_extension_class2( 160 | gdext.library, 161 | &example_class_name, 162 | godot.sprite2d_name_ref(), 163 | &class_info, 164 | ) 165 | 166 | amplitdue_name := godot.new_string_name_cstring("amplitude", true) 167 | get_amplitdue_name := godot.new_string_name_cstring("get_amplitude", true) 168 | set_amplitdue_name := godot.new_string_name_cstring("set_amplitude", true) 169 | classdb.bind_property_and_methods( 170 | &example_class_name, 171 | &litdue_name, 172 | &get_amplitdue_name, 173 | &set_amplitdue_name, 174 | get_amplitude, 175 | set_amplitude, 176 | ) 177 | 178 | speed_name := godot.new_string_name_cstring("speed", true) 179 | get_speed_name := godot.new_string_name_cstring("get_speed", true) 180 | set_speed_name := godot.new_string_name_cstring("set_speed", true) 181 | classdb.bind_property_and_methods( 182 | &example_class_name, 183 | &speed_name, 184 | &get_speed_name, 185 | &set_speed_name, 186 | get_speed, 187 | set_speed, 188 | ) 189 | 190 | classdb.bind_signal( 191 | &example_class_name, 192 | &time_passed_name, 193 | classdb.Signal_Arg{name = &time_passed_name, type = .Float}, 194 | ) 195 | } 196 | -------------------------------------------------------------------------------- /godin/build_cmd.odin: -------------------------------------------------------------------------------- 1 | package godin 2 | 3 | import "core:os" 4 | import "core:fmt" 5 | import "core:strings" 6 | import scan "core:text/scanner" 7 | import "core:io" 8 | 9 | import "core:odin/tokenizer" 10 | 11 | cmd_build :: proc() { 12 | if len(os.args) < 3 { 13 | fmt.println(help_build) 14 | return 15 | } 16 | 17 | options, ok := parse_build_args(os.args[2:]) 18 | defer clean_build_options(&options) 19 | if !ok { 20 | return 21 | } 22 | 23 | state := State{ 24 | classes = make(map[string]StateClass), 25 | 26 | error_handler = proc(pos: tokenizer.Pos, format: string, args: ..any) { 27 | fmt.printf(format, args) 28 | fmt.println() 29 | }, 30 | } 31 | build_state(&state, options) 32 | 33 | gen_backend(state, options) 34 | 35 | delete(state.classes) 36 | } 37 | 38 | /* 39 | class_template 40 | 41 | This is a template string that expects the following format parameters, 42 | in the order given: 43 | 44 | 0: package_name 45 | 1: godot_import_path 46 | 2: class_godot_name 47 | 3: class_parent_name 48 | 4: class_snake_name 49 | 5: class_struct_name 50 | 51 | 6: set_func = {4:s}_set_bind 52 | 7: get_func = {4:s}_get_bind 53 | 8: get_property_list_func = {4:s}_get_property_list_bind 54 | 9: free_property_list_func = {4:s}_free_property_list_bind 55 | 10: property_can_revert_func = {4:s}_property_can_revert_bind 56 | 11: property_get_revert_func = {4:s}_property_get_revert_bind 57 | 12: notification_func = {4:s}_notification_bind 58 | */ 59 | class_template :: #load("templates/class.odin.template", string) 60 | 61 | // TODO: turns out the template is only nice when the output isnt conditional, so lets use live formatting (: 62 | 63 | template_format_class :: proc( 64 | package_name, godot_import_path, class_godot_name, class_parent_name, class_snake_name, class_struct_name: string, 65 | has_set_func, has_get_func, has_get_property_list_func, has_property_can_revert_func, has_notification_func: bool, 66 | ) -> string { 67 | sb := strings.Builder{} 68 | strings.builder_init(&sb) 69 | defer strings.builder_destroy(&sb) 70 | 71 | set_func := "nil" 72 | get_func := "nil" 73 | get_property_list_func := "nil" 74 | free_property_list_func := "nil" 75 | property_can_revert_func := "nil" 76 | property_get_revert_func := "nil" 77 | notification_func := "nil" 78 | 79 | if has_set_func { 80 | set_func = strings.concatenate({class_snake_name, "_set_bind"}) 81 | } 82 | 83 | if has_get_func { 84 | get_func = strings.concatenate({class_snake_name, "_get_bind"}) 85 | } 86 | 87 | if has_get_property_list_func { 88 | get_property_list_func = strings.concatenate({class_snake_name, "_get_property_list_bind"}) 89 | free_property_list_func = strings.concatenate({class_snake_name, "_free_property_list_bind"}) 90 | } 91 | 92 | if has_property_can_revert_func { 93 | property_can_revert_func = strings.concatenate({class_snake_name, "_property_can_revert_bind"}) 94 | property_get_revert_func = strings.concatenate({class_snake_name, "_property_get_revert_bind"}) 95 | } 96 | 97 | if has_notification_func { 98 | notification_func = strings.concatenate({class_snake_name, "_notification_bind"}) 99 | } 100 | 101 | fmt.sbprintf( 102 | &sb, 103 | class_template, 104 | 105 | package_name, 106 | godot_import_path, 107 | class_godot_name, 108 | class_parent_name, 109 | class_snake_name, 110 | class_struct_name, 111 | 112 | set_func, 113 | get_func, 114 | get_property_list_func, 115 | free_property_list_func, 116 | property_can_revert_func, 117 | property_get_revert_func, 118 | notification_func, 119 | ) 120 | 121 | if has_set_func { 122 | delete(set_func) 123 | } 124 | 125 | if has_get_func { 126 | delete(get_func) 127 | } 128 | 129 | if has_get_property_list_func { 130 | delete(get_property_list_func) 131 | delete(free_property_list_func) 132 | } 133 | 134 | if has_property_can_revert_func { 135 | delete(property_can_revert_func) 136 | delete(property_get_revert_func) 137 | } 138 | 139 | if has_notification_func { 140 | delete(notification_func) 141 | } 142 | 143 | return strings.clone(strings.to_string(sb)) 144 | } 145 | 146 | gen_backend :: proc(state: State, options: BuildOptions) { 147 | for _, class in state.classes { 148 | fmt.printf( 149 | "Found StateClass '%v', extends '%v', backend path: '%v'.\n", 150 | class.name, 151 | class.extends, 152 | class.out_file, 153 | ) 154 | 155 | out_file_handle, err := os.open(class.out_file, os.O_CREATE | os.O_TRUNC | os.O_RDWR) 156 | if err != 0 { 157 | print_err(err, class.out_file) 158 | return 159 | } 160 | 161 | // streams take ownership of the handle, so we dont need to close the handle ourselves 162 | stream := os.stream_from_handle(out_file_handle) 163 | defer io.destroy(stream) 164 | 165 | writer, ok := io.to_writer(stream) 166 | if !ok { 167 | fmt.printf("There was an error opening a writer on the file: %v\n", class.out_file) 168 | return 169 | } 170 | 171 | fmt.println("Package name: ", class.source.package_name) 172 | 173 | sb := strings.Builder{} 174 | strings.builder_init(&sb) 175 | defer strings.builder_destroy(&sb) 176 | 177 | class_snake_name := odin_to_snake_case(class.name) 178 | defer delete(class_snake_name) 179 | 180 | class_backend := template_format_class( 181 | class.source.package_name, 182 | options.godot_import_prefix, 183 | class.name, 184 | class.extends, 185 | class_snake_name, 186 | class.odin_struct_name, 187 | false, 188 | false, 189 | false, 190 | false, 191 | false, 192 | ) 193 | defer delete(class_backend) 194 | 195 | io.write_string(writer, class_backend) 196 | 197 | // TODO: import dependent packages (i.e other generated classes) 198 | 199 | io.write_string(writer, strings.to_string(sb)) 200 | } 201 | } 202 | 203 | /* 204 | Copyright 2025 Dresses Digital 205 | 206 | Licensed under the Apache License, Version 2.0 (the "License"); 207 | you may not use this file except in compliance with the License. 208 | You may obtain a copy of the License at 209 | 210 | http://www.apache.org/licenses/LICENSE-2.0 211 | 212 | Unless required by applicable law or agreed to in writing, software 213 | distributed under the License is distributed on an "AS IS" BASIS, 214 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 215 | See the License for the specific language governing permissions and 216 | limitations under the License. 217 | */ 218 | -------------------------------------------------------------------------------- /bindgen/views/engine_class.odin: -------------------------------------------------------------------------------- 1 | #+feature dynamic-literals 2 | package views 3 | 4 | import g "../graph" 5 | import "../names" 6 | import "core:fmt" 7 | import "core:mem" 8 | import "core:strings" 9 | 10 | Engine_Class :: struct { 11 | imports: map[string]Import, 12 | self: string, 13 | name: string, 14 | godot_name: string, 15 | snake_name: string, 16 | cast_on_new: bool, 17 | enums: []Enum, 18 | bit_fields: []Bit_Field, 19 | file_constants: []File_Constant, 20 | static_methods: []Method, 21 | instance_methods: []Method, 22 | } 23 | 24 | @(private = "file") 25 | default_imports := []Import{{name = "__bindgen_gde", path = "godot:gdext"}} 26 | 27 | _constant_constructor :: proc(initializer: g.Initialize_By_Constructor, current_package: string) -> (result: string) { 28 | sb := strings.builder_make() 29 | 30 | fmt.sbprint(&sb, resolve_qualified_type(initializer.type, current_package)) 31 | fmt.sbprint(&sb, "{ ") 32 | for arg in initializer.arg_values { 33 | fmt.sbprint(&sb, arg) 34 | fmt.sbprint(&sb, ", ") 35 | } 36 | fmt.sbprint(&sb, " }") 37 | 38 | result = strings.clone(strings.to_string(sb)) 39 | strings.builder_destroy(&sb) 40 | return 41 | } 42 | 43 | engine_class :: proc(class: ^g.Engine_Class, allocator: mem.Allocator) -> (engine_class: Engine_Class, render: bool) { 44 | context.allocator = allocator 45 | 46 | static_method_count := 0 47 | instance_method_count := 0 48 | for method in class.methods { 49 | if method.static { 50 | static_method_count += 1 51 | } else { 52 | instance_method_count += 1 53 | } 54 | } 55 | 56 | engine_class = Engine_Class { 57 | name = names.clone_string(class.odin_name), 58 | godot_name = names.clone_string(class.godot_name), 59 | snake_name = names.clone_string(class.snake_name), 60 | cast_on_new = class.refcounted, 61 | enums = make([]Enum, len(class.enums)), 62 | bit_fields = make([]Bit_Field, len(class.bit_fields)), 63 | file_constants = make([]File_Constant, len(class.constants)), 64 | static_methods = make([]Method, static_method_count), 65 | instance_methods = make([]Method, instance_method_count), 66 | } 67 | 68 | for default_import in default_imports { 69 | engine_class.imports[default_import.name] = default_import 70 | } 71 | 72 | package_name := fmt.aprintf("godot:%v/%v", g.to_string(class.api_type), engine_class.snake_name) 73 | // package_name := fmt.aprintf("godot:%v", g.to_string(class.api_type)) 74 | // engine_class.derives = resolve_qualified_type(class.inherits, package_name) 75 | 76 | ensure_imports(&engine_class.imports, class, package_name) 77 | engine_class.self = resolve_qualified_type(class, package_name) 78 | 79 | for class_enum, enum_idx in class.enums { 80 | new_enum := Enum { 81 | name = names.clone_string(class_enum.odin_name), 82 | values = make([]Enum_Value, len(class_enum.values)), 83 | } 84 | 85 | for value, value_idx in class_enum.values { 86 | new_enum.values[value_idx] = Enum_Value { 87 | name = names.clone_string(value.odin_name), 88 | value = strings.clone(value.value), 89 | } 90 | } 91 | 92 | engine_class.enums[enum_idx] = new_enum 93 | } 94 | 95 | for class_bit_field, bit_field_idx in class.bit_fields { 96 | new_bit_field := Bit_Field { 97 | name = names.clone_string(class_bit_field.odin_name), 98 | values = make([]Enum_Value, len(class_bit_field.values)), 99 | } 100 | 101 | for value, value_idx in class_bit_field.values { 102 | new_bit_field.values[value_idx] = Enum_Value { 103 | name = names.clone_string(value.odin_name), 104 | value = strings.clone(value.value), 105 | } 106 | } 107 | 108 | engine_class.bit_fields[bit_field_idx] = new_bit_field 109 | } 110 | 111 | for constant, constant_idx in class.constants { 112 | file_constant := File_Constant { 113 | name = names.clone_string(constant.name), 114 | type = resolve_qualified_type(constant.type, package_name), 115 | } 116 | 117 | switch v in constant.initializer { 118 | case string: 119 | file_constant.value = strings.clone(v) 120 | case g.Initialize_By_Constructor: 121 | // TODO: some types can be file constants, while others require initialization 122 | file_constant.value = _constant_constructor(v, package_name) 123 | } 124 | 125 | engine_class.file_constants[constant_idx] = file_constant 126 | } 127 | 128 | static_method_idx := 0 129 | instance_method_idx := 0 130 | for class_method, method_idx in class.methods { 131 | method := Method { 132 | name = strings.clone(class_method.name), 133 | hash = class_method.hash, 134 | args = make([]Method_Arg, len(class_method.args)), 135 | vararg = class_method.vararg, 136 | return_type = nil, 137 | } 138 | 139 | if class_method.return_type != nil { 140 | method.return_type = resolve_qualified_type(class_method.return_type, package_name) // TODO: other package modes 141 | ensure_imports(&engine_class.imports, class_method.return_type, package_name) // TODO: other package modes 142 | } 143 | 144 | for class_method_arg, arg_idx in class_method.args { 145 | method.args[arg_idx] = Method_Arg { 146 | name = strings.clone(class_method_arg.name), 147 | type = resolve_qualified_type(class_method_arg.type, package_name), // TODO: other package modes 148 | // TODO: defaults? 149 | } 150 | 151 | ensure_imports(&engine_class.imports, class_method_arg.type, package_name) // TODO: other package modes 152 | } 153 | 154 | if class_method.static { 155 | engine_class.static_methods[static_method_idx] = method 156 | static_method_idx += 1 157 | } else { 158 | engine_class.instance_methods[instance_method_idx] = method 159 | instance_method_idx += 1 160 | } 161 | } 162 | 163 | render = true 164 | return 165 | } 166 | 167 | /* 168 | Copyright 2025 Dresses Digital 169 | 170 | Licensed under the Apache License, Version 2.0 (the "License"); 171 | you may not use this file except in compliance with the License. 172 | You may obtain a copy of the License at 173 | 174 | http://www.apache.org/licenses/LICENSE-2.0 175 | 176 | Unless required by applicable law or agreed to in writing, software 177 | distributed under the License is distributed on an "AS IS" BASIS, 178 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 179 | See the License for the specific language governing permissions and 180 | limitations under the License. 181 | */ 182 | -------------------------------------------------------------------------------- /godin/build_options.odin: -------------------------------------------------------------------------------- 1 | package godin 2 | 3 | import "core:os" 4 | import "core:strings" 5 | import "core:fmt" 6 | 7 | BuildOptions :: struct { 8 | target_path: string, 9 | allow_single_file: bool, 10 | backend_suffix: string, 11 | target_path_handle: os.Handle, 12 | target_files: [dynamic]os.Handle, 13 | godot_import_prefix: string, 14 | } 15 | 16 | parse_build_args :: proc(args: []string) -> (options: BuildOptions, success: bool) { 17 | success = false 18 | 19 | setup_build_options(&options) 20 | 21 | // first arg is always a file or folder path 22 | options.target_path = args[0] 23 | 24 | // the rest are options 25 | for i := 1; i < len(args); i += 1 { 26 | left_arg := args[i] 27 | right_arg: Maybe(string) = nil 28 | if strings.contains_rune(left_arg, ':') { 29 | split := strings.split_n(left_arg, ":", 2) 30 | defer delete(split) 31 | 32 | left_arg = split[0] 33 | right_arg = split[1] 34 | } 35 | 36 | switch left_arg { 37 | case "-file": 38 | options.allow_single_file = true 39 | case "-backend-suffix": 40 | right_val, ok := right_arg.(string) 41 | if !ok { 42 | fmt.eprintln( 43 | "Error: '-backend-suffix' was given without a value. Correct example usage: `-backend-suffix:_my_suffix.odin`.", 44 | ) 45 | return 46 | } 47 | options.backend_suffix = right_val 48 | case "-godot-import-path": 49 | right_val, ok := right_arg.(string) 50 | if !ok { 51 | fmt.eprintln( 52 | "Error: '-godot-import-path' was given without a value. Correct example usage: `-godot-import-path:shared:godot/`.", 53 | ) 54 | return 55 | } 56 | options.godot_import_prefix = right_val 57 | } 58 | } 59 | 60 | // verify the options are valid together 61 | // path must exist 62 | if !os.exists(options.target_path) { 63 | fmt.eprintf("Error: the path '%v' does not exist\n", options.target_path) 64 | } 65 | 66 | target_handle, err := os.open(options.target_path, os.O_RDWR) 67 | if err != 0 { 68 | print_err(err, options.target_path) 69 | return 70 | } 71 | 72 | options.target_path_handle = target_handle 73 | 74 | // target_path must be a file if -file is specified, otherwise 75 | // it must be a directory 76 | path_is_dir := os.is_dir(options.target_path) 77 | if options.allow_single_file { 78 | if path_is_dir { 79 | fmt.eprintf("Error: the path '%v' points to a directory, but '-file' was specified.\n", options.target_path) 80 | return 81 | } 82 | } else { 83 | if !path_is_dir { 84 | fmt.eprintf("Error: the path '%v' points to a directory, but '-file' was specified.\n", options.target_path) 85 | return 86 | } 87 | 88 | files, target_err := os.read_dir(target_handle, -1) 89 | if target_err != 0 { 90 | print_err(target_err, options.target_path) 91 | return 92 | } 93 | 94 | if len(files) == 0 { 95 | fmt.eprintf("Error: The directory '%v' is empty.\n", options.target_path) 96 | return 97 | } 98 | 99 | // store a list of every valid odin file, recursively 100 | _recurse_files :: proc(files: []os.File_Info, target_files: ^[dynamic]os.Handle, ignore_suffix: string) { 101 | directories := [dynamic]os.File_Info{} 102 | for file in files { 103 | if file.is_dir { 104 | append(&directories, file) 105 | } else if strings.has_suffix(file.fullpath, ".odin") && 106 | !strings.has_suffix(file.fullpath, ignore_suffix) { 107 | odin_file, odin_file_err := os.open(file.fullpath) 108 | if odin_file_err != 0 { 109 | print_err(odin_file_err, file.fullpath) 110 | return 111 | } 112 | append(target_files, odin_file) 113 | } 114 | } 115 | 116 | for dir in directories { 117 | dir_handle, dir_err := os.open(dir.fullpath) 118 | if dir_err != 0 { 119 | print_err(dir_err, dir.fullpath) 120 | return 121 | } 122 | 123 | defer os.close(dir_handle) 124 | dir_files, dir_read_err := os.read_dir(dir_handle, -1) 125 | if dir_read_err != 0 { 126 | print_err(dir_err, dir.fullpath) 127 | return 128 | } 129 | 130 | _recurse_files(dir_files, target_files, ignore_suffix) 131 | } 132 | } 133 | 134 | _recurse_files(files, &options.target_files, options.backend_suffix) 135 | 136 | if len(options.target_files) == 0 { 137 | fmt.printf( 138 | "Error: There are no valid .odin files in the directory '%v' and its subdirectories.\n", 139 | options.target_path, 140 | ) 141 | return 142 | } 143 | } 144 | 145 | success = true 146 | return 147 | } 148 | 149 | setup_build_options :: proc(options: ^BuildOptions) { 150 | // no default target_path 151 | options.allow_single_file = false 152 | options.backend_suffix = ".gen.odin" 153 | 154 | options.godot_import_prefix = "shared:odin-godot/" 155 | 156 | options.target_files = make([dynamic]os.Handle) 157 | } 158 | 159 | clean_build_options :: proc(options: ^BuildOptions) { 160 | if options.target_path_handle != os.INVALID_HANDLE { 161 | os.close(options.target_path_handle) 162 | } 163 | 164 | for file in options.target_files { 165 | os.close(file) 166 | } 167 | delete(options.target_files) 168 | } 169 | 170 | when ODIN_OS == .Windows { 171 | _err_map := map[os.Errno]string { 172 | os.ERROR_FILE_NOT_FOUND = "That file or directory couldn't be found.", 173 | } 174 | } else { 175 | _err_map := map[os.Errno]string{} 176 | } 177 | 178 | print_err :: proc(err: os.Errno, file: string) { 179 | if err_string, ok := _err_map[err]; ok { 180 | fmt.eprintf("Error opening '%v': %v\n", file, err_string) 181 | return 182 | } 183 | 184 | fmt.eprintf("Unknown errno when opening '%v': %v\n", file, err) 185 | } 186 | 187 | /* 188 | Copyright 2025 Dresses Digital 189 | 190 | Licensed under the Apache License, Version 2.0 (the "License"); 191 | you may not use this file except in compliance with the License. 192 | You may obtain a copy of the License at 193 | 194 | http://www.apache.org/licenses/LICENSE-2.0 195 | 196 | Unless required by applicable law or agreed to in writing, software 197 | distributed under the License is distributed on an "AS IS" BASIS, 198 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 199 | See the License for the specific language governing permissions and 200 | limitations under the License. 201 | */ 202 | -------------------------------------------------------------------------------- /examples/godin-syntax/src/main.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import gd "godot:gdext" 4 | import var "../../variant" 5 | import "core:strings" 6 | 7 | // TODO: add class gen 8 | // TODO: add static method gen 9 | // TODO: add method gen 10 | // TODO: add property get gen 11 | // TODO: add property set gen 12 | // TODO: add property get+set gen 13 | // TODO: add enum gen 14 | 15 | //+class ExampleClass extends Sprite2D 16 | ExampleClass :: struct { 17 | _owner: gd.ObjectPtr, 18 | thing: i64, 19 | 20 | // +signal(ExampleClass) on_thing(a: String, b: ExampleClass) 21 | // +group(ExampleClass) Test group: group_ 22 | // +subgroup(ExampleClass) Test subgroup: group_subgroup_ 23 | } 24 | 25 | // +static method(ExampleClass) do_thing_static(a: int, b: int) -> int 26 | example_class_do_thing_static :: proc(a, b: i64) -> i64 { 27 | return a + b 28 | } 29 | 30 | // +method(ExampleClass) do_thing() 31 | example_class_do_thing :: proc(self: ^ExampleClass) { 32 | self.thing -= 50 33 | } 34 | 35 | // +property(ExampleClass) thing get: int 36 | example_class_get_thing :: proc(self: ^ExampleClass) -> i64 { 37 | return self.thing 38 | } 39 | 40 | // +property(ExampleClass) thing set: int 41 | example_class_set_thing :: proc(self: ^ExampleClass, value: i64) { 42 | self.thing = value 43 | } 44 | 45 | // +enum(ExampleClass) ExampleEnum 46 | ExampleEnum :: enum { 47 | A, 48 | B, 49 | C, 50 | D, 51 | } 52 | 53 | @(export) 54 | example_library_init :: proc "c" ( 55 | interface: ^gd.Interface, 56 | library: gd.ExtensionClassLibraryPtr, 57 | initialization: ^gd.Initialization, 58 | ) -> bool { 59 | // do usual initialization 60 | gd.interface = interface 61 | gd.library = library 62 | 63 | context = gd.godot_context() 64 | 65 | test_str := strings.clone("uwu") 66 | delete(test_str) 67 | 68 | var.init_string_constructors() 69 | var.init_string_name_constructors() 70 | var.init_string_bindings() 71 | var.init_string_name_bindings() 72 | 73 | initialization.initialize = initialize_example_module 74 | initialization.deinitialize = uninitialize_example_module 75 | initialization.minimum_initialization_level = .Core 76 | 77 | return true 78 | } 79 | 80 | initialize_example_module :: proc "c" (user_data: rawptr, level: gd.InitializationLevel) { 81 | context = gd.godot_context() 82 | 83 | if level != .Scene { 84 | return 85 | } 86 | 87 | test_str := strings.clone("uwu") 88 | delete(test_str) 89 | 90 | init_example_class_bindings() 91 | } 92 | 93 | uninitialize_example_module :: proc "c" (user_data: rawptr, level: gd.InitializationLevel) { 94 | context = gd.godot_context() 95 | 96 | if level != .Scene { 97 | return 98 | } 99 | 100 | } 101 | 102 | // initialize_example_module :: proc "c" (user_data: rawptr, level: gd.InitializationLevel) { 103 | // context = gd.godot_context() 104 | 105 | // if level != .Scene { 106 | // return 107 | // } 108 | 109 | // class_name := var.new_string_name_cstring("ExampleClass") 110 | // parent_name := var.new_string_name_cstring("Node2D") 111 | 112 | // class_info := gd.ExtensionClassCreationInfo { 113 | // is_virtual = false, 114 | // is_abstract = false, 115 | // set_func = example_class_set, 116 | // get_func = example_class_get, 117 | // get_property_list_func = example_class_get_property_list, 118 | // free_property_list_func = example_class_free_property_list, 119 | // property_can_revert_func = example_class_property_can_revert, 120 | // property_get_revert_func = example_class_property_get_revert, 121 | // notification_func = example_class_notification_func, 122 | // to_string_func = example_class_to_string, 123 | // reference_func = nil, 124 | // unreference_func = nil, 125 | // create_instance_func = example_class_create, 126 | // free_instance_func = example_class_free, 127 | // get_virtual_func = class_db_get_virtual_func, 128 | // get_rid_func = nil, 129 | // class_user_data = nil, 130 | // } 131 | // gd.interface.classdb_register_extension_class( 132 | // gd.library, 133 | // cast(gd.StringNamePtr)&class_name._opaque, 134 | // cast(gd.StringNamePtr)&parent_name._opaque, 135 | // &class_info, 136 | // ) 137 | // } 138 | 139 | // uninitialize_example_module :: proc "c" (user_data: rawptr, level: gd.InitializationLevel) { 140 | // context = gd.godot_context() 141 | 142 | // if level != .Scene { 143 | // return 144 | // } 145 | 146 | // class_name := var.new_string_name_cstring("ExampleClass") 147 | // gd.interface.classdb_unregister_extension_class(gd.library, cast(gd.StringNamePtr)&class_name) 148 | // } 149 | 150 | // example_class_set :: proc "c" ( 151 | // instance: gd.ExtensionClassInstancePtr, 152 | // name: gd.StringNamePtr, 153 | // value: gd.VariantPtr, 154 | // ) -> bool { 155 | // context = gd.godot_context() 156 | // return false 157 | // } 158 | 159 | // example_class_get :: proc "c" ( 160 | // instance: gd.ExtensionClassInstancePtr, 161 | // name: gd.StringNamePtr, 162 | // ret: gd.VariantPtr, 163 | // ) -> bool { 164 | // context = gd.godot_context() 165 | // return false 166 | // } 167 | 168 | // example_class_get_property_list :: proc "c" ( 169 | // instance: gd.ExtensionClassInstancePtr, 170 | // count: ^u32, 171 | // ) -> [^]gd.PropertyInfo { 172 | // context = gd.godot_context() 173 | // return nil 174 | // } 175 | 176 | // example_class_free_property_list :: proc "c" (instance: gd.ExtensionClassInstancePtr, list: ^gd.PropertyInfo) { 177 | // context = gd.godot_context() 178 | 179 | // } 180 | 181 | // example_class_property_can_revert :: proc "c" ( 182 | // instance: gd.ExtensionClassInstancePtr, 183 | // name: gd.StringNamePtr, 184 | // ) -> bool { 185 | // context = gd.godot_context() 186 | // return false 187 | // } 188 | 189 | // example_class_property_get_revert :: proc "c" ( 190 | // instance: gd.ExtensionClassInstancePtr, 191 | // name: gd.StringNamePtr, 192 | // ret: gd.VariantPtr, 193 | // ) -> bool { 194 | // context = gd.godot_context() 195 | // return false 196 | // } 197 | 198 | // example_class_notification_func :: proc "c" (instance: gd.ExtensionClassInstancePtr, what: i32) { 199 | // context = gd.godot_context() 200 | // } 201 | 202 | // example_class_to_string :: proc "c" (instance: gd.ExtensionClassInstancePtr, is_valid: ^bool, out: gd.StringPtr) { 203 | // context = gd.godot_context() 204 | // } 205 | 206 | // example_class_create :: proc "c" (user_data: rawptr) -> gd.ObjectPtr { 207 | // context = gd.godot_context() 208 | // return nil 209 | // } 210 | 211 | // example_class_free :: proc "c" (user_data: rawptr, instance: gd.ExtensionClassInstancePtr) { 212 | // context = gd.godot_context() 213 | // } 214 | 215 | // class_db_get_virtual_func :: proc "c" (user_data: rawptr, name: gd.StringNamePtr) -> gd.ExtensionClassCallVirtual { 216 | // context = gd.godot_context() 217 | // return nil 218 | // } 219 | -------------------------------------------------------------------------------- /templates/bindgen_view_variant.temple.twig: -------------------------------------------------------------------------------- 1 | package godot 2 | 3 | {% for name, import_ in this.imports %} 4 | import {{ name }} "{{ import_.path }}" 5 | {% end %} 6 | 7 | {% for enum_ in this.enums %} 8 | {% embed "bindgen_view_enum.temple.twig" with enum_ %} 9 | {% end %} 10 | 11 | {% if len(this.constructors) + len(this.extern_constructors) > 0 %} 12 | new_{{ this.snake_name }} :: proc { 13 | {% for constructor in this.constructors %} 14 | {{ constructor.name }}, 15 | {% end %} 16 | {% for extern_constructor in this.extern_constructors %} 17 | {{ extern_constructor }}, 18 | {% end %} 19 | } 20 | {% end %} 21 | 22 | {% for constructor in this.constructors %} 23 | {{ constructor.name }} :: proc "contextless" ( 24 | {% for arg in constructor.args %} 25 | {{ arg.name }}_: {{ arg.type }}, 26 | {% end %} 27 | ) -> (ret: {{ this.name }}) { 28 | @(static) __ptr: __bindgen_gde.PtrConstructor 29 | if __ptr == nil { 30 | __ptr = __bindgen_gde.variant_get_ptr_constructor(.{{ this.name }}, {{ int(constructor.index) }}) 31 | } 32 | {% for arg in constructor.args %} 33 | {{ arg.name }}_ := {{ arg.name }}_ 34 | {% end %} 35 | args := []__bindgen_gde.TypePtr { 36 | {% for arg in constructor.args %} 37 | &{{ arg.name }}_, 38 | {% end %} 39 | } 40 | __ptr(&ret, raw_data(args)) 41 | return 42 | } 43 | {% end %} 44 | 45 | {% if destructor_name, has_destructor := this.destructor.(string); has_destructor %} 46 | {{ destructor_name }} :: proc "contextless" (self: {{ this.name }}) { 47 | @(static) __ptr: __bindgen_gde.PtrDestructor 48 | if __ptr == nil { 49 | __ptr = __bindgen_gde.variant_get_ptr_destructor(.{{ this.name }}) 50 | } 51 | 52 | self := self 53 | __ptr(&self) 54 | } 55 | {% end %} 56 | 57 | // members 58 | {% for member in this.members %} 59 | {{ this.snake_name }}_set_{{ member.name }} :: proc "contextless" (self: ^{{ this.name }}, value: {{ member.type }}) { 60 | @(static) __ptr: __bindgen_gde.PtrSetter 61 | if __ptr == nil { 62 | _gde_name := new_string_name_cstring("{{ member.name }}", true) 63 | __ptr = __bindgen_gde.variant_get_ptr_setter(.{{ this.name }}, &_gde_name) 64 | } 65 | 66 | value := value 67 | __ptr(self, &value) 68 | } 69 | 70 | {{ this.snake_name }}_get_{{ member.name }} :: proc "contextless" (self: ^{{ this.name }}) -> (ret: {{ member.type }}) { 71 | @(static) __ptr: __bindgen_gde.PtrGetter 72 | if __ptr == nil { 73 | _gde_name := new_string_name_cstring("{{ member.name }}", true) 74 | __ptr = __bindgen_gde.variant_get_ptr_getter(.{{ this.name }}, &_gde_name) 75 | } 76 | 77 | __ptr(self, &ret) 78 | return 79 | } 80 | {% end %} 81 | 82 | {% for method in this.static_methods %} 83 | {% if return_type, has_return_type := method.return_type.(string); has_return_type %} 84 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 85 | {% for arg in method.args %} 86 | {{ arg.name }}_: {{ arg.type }}, 87 | {% end %} 88 | ) -> (ret: {{ return_type }}) { 89 | @(static) __ptr: __bindgen_gde.PtrBuiltInMethod 90 | if __ptr == nil { 91 | _gde_name := new_string_name_cstring("{{ method.name }}", true) 92 | __ptr = __bindgen_gde.variant_get_ptr_builtin_method(.{{ this.name }}, &_gde_name, {{ i64(method.hash) }}) 93 | } 94 | {% for arg in method.args %} 95 | {{ arg.name }}_ := {{ arg.name }}_ 96 | {% end %} 97 | args := []__bindgen_gde.TypePtr { 98 | {% for arg in method.args %} 99 | &{{ arg.name }}_, 100 | {% end %} 101 | } 102 | __ptr(nil, raw_data(args), &ret, len(args)) 103 | return 104 | } 105 | {% else %} 106 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 107 | {% for arg in method.args %} 108 | {{ arg.name }}_: {{ arg.type }}, 109 | {% end %} 110 | ) { 111 | @(static) __ptr: __bindgen_gde.PtrBuiltInMethod 112 | if __ptr == nil { 113 | _gde_name := new_string_name_cstring("{{ method.name }}", true) 114 | __ptr = __bindgen_gde.variant_get_ptr_builtin_method(.{{ this.name }}, &_gde_name, {{ i64(method.hash) }}) 115 | } 116 | {% for arg in method.args %} 117 | {{ arg.name }}_ := {{ arg.name }}_ 118 | {% end %} 119 | args := []__bindgen_gde.TypePtr { 120 | {% for arg in method.args %} 121 | &{{ arg.name }}_, 122 | {% end %} 123 | } 124 | __ptr(nil, raw_data(args), nil, len(args)) 125 | } 126 | {% end %} 127 | {% end %} 128 | 129 | {% for method in this.instance_methods %} 130 | {% if return_type, has_return_type := method.return_type.(string); has_return_type %} 131 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 132 | self: ^{{ this.name }}, 133 | {% for arg in method.args %} 134 | {{ arg.name }}_: {{ arg.type }}, 135 | {% end %} 136 | ) -> (ret: {{ return_type }}) { 137 | @(static) __ptr: __bindgen_gde.PtrBuiltInMethod 138 | if __ptr == nil { 139 | _gde_name := new_string_name_cstring("{{ method.name }}", true) 140 | __ptr = __bindgen_gde.variant_get_ptr_builtin_method(.{{ this.name }}, &_gde_name, {{ i64(method.hash) }}) 141 | } 142 | {% for arg in method.args %} 143 | {{ arg.name }}_ := {{ arg.name }}_ 144 | {% end %} 145 | args := []__bindgen_gde.TypePtr { 146 | {% for arg in method.args %} 147 | &{{ arg.name }}_, 148 | {% end %} 149 | } 150 | __ptr(self, raw_data(args), &ret, len(args)) 151 | return 152 | } 153 | {% else %} 154 | {{ this.snake_name }}_{{ method.name }} :: proc "contextless" ( 155 | self: ^{{ this.name }}, 156 | {% for arg in method.args %} 157 | {{ arg.name }}_: {{ arg.type }}, 158 | {% end %} 159 | ) { 160 | @(static) __ptr: __bindgen_gde.PtrBuiltInMethod 161 | if __ptr == nil { 162 | _gde_name := new_string_name_cstring("{{ method.name }}", true) 163 | __ptr = __bindgen_gde.variant_get_ptr_builtin_method(.{{ this.name }}, &_gde_name, {{ i64(method.hash) }}) 164 | } 165 | {% for arg in method.args %} 166 | {{ arg.name }}_ := {{ arg.name }}_ 167 | {% end %} 168 | args := []__bindgen_gde.TypePtr { 169 | {% for arg in method.args %} 170 | &{{ arg.name }}_, 171 | {% end %} 172 | } 173 | __ptr(self, raw_data(args), nil, len(args)) 174 | return 175 | } 176 | {% end %} 177 | {% end %} 178 | 179 | {% for operator in this.operators %} 180 | {% for overload in operator.overloads %} 181 | {% if right_type, has_right_type := overload.right_type.(string); has_right_type %} 182 | {{ overload.proc_name }} :: proc "contextless" (self: {{ this.name }}, other: {{ right_type }}) -> (ret: {{ overload.return_type }}) { 183 | @(static) __ptr: __bindgen_gde.PtrOperatorEvaluator 184 | if __ptr == nil { 185 | __ptr = __bindgen_gde.variant_get_ptr_operator_evaluator(.{{ operator.variant_name }}, .{{ this.name }}, .{{ overload.right_variant_type }}) 186 | } 187 | 188 | self := self 189 | other := other 190 | __ptr(&self, &other, &ret) 191 | return 192 | } 193 | {% else %} 194 | {{ overload.proc_name }} :: proc "contextless" (self: {{ this.name }}) -> (ret: {{ overload.return_type }}) { 195 | @(static) __ptr: __bindgen_gde.PtrOperatorEvaluator 196 | if __ptr == nil { 197 | __ptr = __bindgen_gde.variant_get_ptr_operator_evaluator(.{{ operator.variant_name }}, .{{ this.name }}, .Nil) 198 | } 199 | 200 | self := self 201 | __ptr(&self, nil, &ret) 202 | return 203 | } 204 | {% end %} 205 | {% end %} 206 | 207 | {{ operator.proc_name }} :: proc { 208 | {% for overload in operator.overloads %} 209 | {{ overload.proc_name }}, 210 | {% end %} 211 | } 212 | {% end %} 213 | 214 | -------------------------------------------------------------------------------- /bindgen/graph/api.odin: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import "../names" 4 | 5 | Api :: struct { 6 | version: ApiVersion `json:"header"`, 7 | builtin_sizes: []ApiBuiltinClassSizes `json:"builtin_class_sizes"`, 8 | builtin_offsets: []ApiBuiltinClassMemberOffsets `json:"builtin_class_member_offsets"`, 9 | // global_constants is empty, so it is not parsed in this model 10 | enums: []ApiEnum `json:"global_enums"`, 11 | util_functions: []ApiUtilityFunction `json:"utility_functions"`, 12 | builtin_classes: []ApiBuiltinClass `json:"builtin_classes"`, 13 | classes: []ApiClass `json:"classes"`, 14 | singletons: []ApiSingleton `json:"singletons"`, 15 | native_structs: []ApiNativeStructure `json:"native_structures"`, 16 | } 17 | 18 | ApiVersion :: struct { 19 | major: uint `json:"version_major"`, 20 | minor: uint `json:"version_minor"`, 21 | patch: uint `json:"version_patch"`, 22 | status: string `json:"version_status"`, 23 | build: string `json:"version_build"`, 24 | full_name: string `json:"version_full_name"`, 25 | } 26 | 27 | ApiBuiltinClassSizes :: struct { 28 | configuration: string `json:"build_configuration"`, 29 | sizes: []ApiTypeSize `json:"sizes"`, 30 | } 31 | 32 | ApiTypeSize :: struct { 33 | name: names.Godot_Name, 34 | size: uint, 35 | } 36 | 37 | ApiBuiltinClassMemberOffsets :: struct { 38 | configuration: string `json:"build_configuration"`, 39 | classes: []ApiMemberOffsetClass `json:"classes"`, 40 | } 41 | 42 | ApiMemberOffsetClass :: struct { 43 | name: names.Godot_Name `json:"name"`, 44 | members: []ApiMemberOffset `json:"members"`, 45 | } 46 | 47 | ApiMemberOffset :: struct { 48 | member: string `json:"member"`, 49 | offset: uint `json:"offset"`, 50 | meta: string `json:"meta"`, 51 | } 52 | 53 | ApiEnum :: struct { 54 | name: names.Godot_Name `json:"name"`, 55 | is_bitfield: bool `json:"is_bitfield"`, 56 | values: []struct { 57 | name: names.Const_Name `json:"name"`, 58 | value: int `json:"value"`, 59 | } `json:"values"`, 60 | } 61 | 62 | ApiUtilityFunction :: struct { 63 | name: string `json:"name"`, 64 | return_type: Maybe(string) `json:"return_type"`, 65 | category: string `json:"category"`, 66 | is_vararg: bool `json:"is_vararg"`, 67 | hash: i64 `json:"hash"`, 68 | arguments: []ApiFunctionArgument `json:"arguments"`, 69 | } 70 | 71 | ApiFunctionArgument :: struct { 72 | name: string `json:"name"`, 73 | type: string `json:"type"`, 74 | default_value: Maybe(string) `json:"default_value"`, 75 | } 76 | 77 | ApiBuiltinClass :: struct { 78 | name: names.Godot_Name `json:"name"`, 79 | has_destructor: bool `json:"has_destructor"`, 80 | indexing_return_type: Maybe(string) `json:"indexing_return_type"`, 81 | is_keyed: bool `json:"is_keyed"`, 82 | constants: []ApiConstant `json:"constants"`, 83 | constructors: []ApiClassConstructor `json:"constructors"`, 84 | enums: []ApiEnum `json:"enums"`, 85 | members: []ApiClassMember `json:"members"`, 86 | methods: []ApiBuiltinClassMethod `json:"methods"`, 87 | operators: []ApiClassOperator `json:"operators"`, 88 | } 89 | 90 | ApiClassOperator :: struct { 91 | name: string `json:"name"`, 92 | right_type: Maybe(string) `json:"right_type"`, 93 | return_type: string `json:"return_type"`, 94 | } 95 | 96 | ApiBuiltinClassMethod :: struct { 97 | name: string `json:"name"`, 98 | return_type: Maybe(string) `json:"return_type"`, 99 | is_vararg: bool `json:"is_vararg"`, 100 | is_const: bool `json:"is_const"`, 101 | is_static: bool `json:"is_static"`, 102 | hash: i64 `json:"hash"`, 103 | arguments: []ApiFunctionArgument `json:"arguments"`, 104 | } 105 | 106 | ApiClassConstructor :: struct { 107 | index: u64 `json:"index"`, 108 | arguments: []ApiFunctionArgument `json:"arguments"`, 109 | } 110 | 111 | ApiClassMember :: struct { 112 | name: string `json:"name"`, 113 | type: string `json:"name"`, 114 | } 115 | 116 | ApiClass :: struct { 117 | name: names.Godot_Name `json:"name"`, 118 | is_refcounted: bool `json:"is_refcounted"`, 119 | is_instantiable: bool `json:"is_instantiable"`, 120 | inherits: Maybe(names.Godot_Name) `json:"inherits"`, 121 | api_type: string `json:"api_type"`, 122 | enums: []ApiEnum `json:"enums"`, 123 | constants: []ApiConstant `json:"constants"`, 124 | methods: []ApiClassMethod `json:"methods"`, 125 | signals: []ApiClassSignal `json:"signals"`, 126 | operators: []ApiClassOperator `json:"operators"`, 127 | properties: []ApiClassProperty `json:"properties"`, 128 | } 129 | 130 | ApiConstant :: struct { 131 | name: names.Const_Name `json:"name"`, 132 | type: Maybe(string) `json:"type"`, 133 | value: union { 134 | int, 135 | string, 136 | } `json:"value"`, 137 | } 138 | 139 | ApiClassSignal :: struct { 140 | name: string `json:"name"`, 141 | arguments: []ApiClassSignalArgument `json:"arguments"`, 142 | } 143 | 144 | ApiClassSignalArgument :: struct { 145 | name: string `json:"name"`, 146 | type: string `json:"type"`, 147 | } 148 | 149 | ApiClassProperty :: struct { 150 | type: string `json:"type"`, 151 | name: string `json:"name"`, 152 | setter: Maybe(string) `json:"setter"`, 153 | getter: string `json:"getter"`, 154 | } 155 | 156 | ApiClassMethod :: struct { 157 | name: string `json:"name"`, 158 | is_vararg: bool `json:"is_vararg"`, 159 | is_const: bool `json:"is_const"`, 160 | is_static: bool `json:"is_static"`, 161 | is_virtual: bool `json:"is_virtual"`, 162 | hash: i64 `json:"hash"`, 163 | hash_compatibility: []i64 `json:"hash_compatibility"`, 164 | return_value: Maybe(ApiClassMethodReturnValue) `json:"return_value"`, 165 | arguments: []ApiClassMethodArguments `json:"arguments"`, 166 | } 167 | 168 | ApiClassMethodReturnValue :: struct { 169 | type: string `json:"type"`, 170 | meta: Maybe(string) `json:"meta"`, 171 | } 172 | 173 | ApiClassMethodArguments :: struct { 174 | name: string `json:"name"`, 175 | type: string `json:"type"`, 176 | meta: Maybe(string) `json:"meta"`, 177 | default_value: Maybe(string) `json:"default_value"`, 178 | } 179 | 180 | ApiSingleton :: struct { 181 | name: names.Godot_Name `json:"name"`, 182 | type: names.Godot_Name `json:"type"`, 183 | } 184 | 185 | ApiNativeStructure :: struct { 186 | name: names.Godot_Name `json:"name"`, 187 | format: string `json:"format"`, 188 | } 189 | 190 | /* 191 | Copyright 2025 Dresses Digital 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | */ 205 | -------------------------------------------------------------------------------- /bindgen/names/names.odin: -------------------------------------------------------------------------------- 1 | package names 2 | 3 | import "base:intrinsics" 4 | import "core:fmt" 5 | import "core:strings" 6 | import "core:unicode" 7 | import "core:unicode/utf8" 8 | 9 | // ACRONYMPascalCase 10 | Godot_Name :: distinct string 11 | 12 | // Acronym_Ada_Case 13 | Odin_Name :: distinct string 14 | 15 | // CONST_CASE 16 | Const_Name :: distinct string 17 | 18 | // snake_case 19 | Snake_Name :: distinct string 20 | 21 | Name_Case :: union { 22 | Godot_Name, 23 | Odin_Name, 24 | Const_Name, 25 | Snake_Name, 26 | } 27 | 28 | clone_string :: proc(name: $N) -> string where intrinsics.type_is_variant_of(Name_Case, N) { 29 | return strings.clone(cast(string)name) 30 | } 31 | 32 | to_odin :: proc { 33 | godot_to_odin, 34 | const_to_odin, 35 | snake_to_odin, 36 | } 37 | 38 | to_snake :: proc { 39 | odin_to_snake, 40 | godot_to_snake, 41 | } 42 | 43 | to_const :: proc { 44 | godot_to_const, 45 | odin_to_const, 46 | } 47 | 48 | is_upper_or_number :: proc(r: rune) -> bool { 49 | return unicode.is_number(r) || unicode.is_upper(r) 50 | } 51 | 52 | is_lower_or_number :: proc(r: rune) -> bool { 53 | return unicode.is_number(r) || unicode.is_lower(r) 54 | } 55 | 56 | // godot uses ACRONYMPascalCase, but we use AcronymPascalCase 57 | // return string must be freed 58 | godot_to_odin :: proc(name: Godot_Name) -> (s: Odin_Name) { 59 | assert(len(name) > 0) 60 | 61 | sb := strings.builder_make() 62 | defer strings.builder_destroy(&sb) 63 | 64 | runes := utf8.string_to_runes(cast(string)name) 65 | defer delete(runes) 66 | 67 | fmt.sbprint(&sb, unicode.to_upper(runes[0])) 68 | for i := 1; i < len(runes) - 1; i += 1 { 69 | r := runes[i] 70 | previous := runes[i - 1] 71 | next := runes[i + 1] 72 | 73 | if r == '.' { 74 | fmt.sbprint(&sb, '_') 75 | continue 76 | } 77 | 78 | // if r is uppercase, there are some transformations: 79 | // AAA -> Aaa 80 | // AA1 -> Aa1 81 | // AAa -> A_Aa 82 | // aAA -> a_Aa 83 | // aA1 -> a_A1 84 | // aAa -> a_Aa 85 | // 1AA -> 1aa 86 | // 1A1 -> 1a1 87 | // 1Aa -> 1aa 88 | 89 | // if r is lowercase, then there is no transformation to r: 90 | // AaA -> Aa_A 91 | // Aaa -> Aaa 92 | // Aa1 -> Aa1 93 | // aaA -> aa_A 94 | // aa1 -> aa1 95 | // aaa -> aaa 96 | // 1aA -> 1a_A 97 | // 1a1 -> 1a1 98 | // 1aa -> 1aa 99 | 100 | // if r is a number, then there is no transformation to r: 101 | // A1A -> A1a 102 | // A1a -> A1a 103 | // A11 -> A11 104 | // a1A -> a1a 105 | // a11 -> a11 106 | // a1a -> a1a 107 | // 11A -> 11a 108 | // 111 -> 111 109 | // 11a -> 11a 110 | if unicode.is_upper(r) { 111 | if is_upper_or_number(previous) && is_upper_or_number(next) { 112 | fmt.sbprint(&sb, unicode.to_lower(r)) 113 | continue 114 | } 115 | 116 | // only add _ prefix if not already prefixed with _ due to dot 117 | if previous != '.' { 118 | fmt.sbprint(&sb, '_') 119 | } 120 | } 121 | 122 | fmt.sbprint(&sb, r) 123 | } 124 | 125 | // always push the last rune as lower 126 | if len(runes) > 1 { 127 | fmt.sbprint(&sb, unicode.to_lower(runes[len(runes) - 1])) 128 | } 129 | 130 | s = cast(Odin_Name)strings.clone(strings.to_string(sb)) 131 | return 132 | } 133 | 134 | godot_to_snake :: proc(name: Godot_Name) -> (s: Snake_Name) { 135 | // lol (: 136 | odin_name := godot_to_odin(name) 137 | defer delete(cast(string)odin_name) 138 | s = odin_to_snake(odin_name) 139 | return 140 | } 141 | 142 | odin_to_snake :: proc(name: Odin_Name) -> (s: Snake_Name) { 143 | assert(len(name) > 0) 144 | return cast(Snake_Name)strings.to_lower(cast(string)name) 145 | } 146 | 147 | // ACROYNM0ACRONYMNormalWord to ACRONYM0ACRONYM_NORMAL_WORD 148 | godot_to_const :: proc(name: Godot_Name) -> (s: Const_Name) { 149 | assert(len(name) > 0) 150 | 151 | sb := strings.builder_make() 152 | defer strings.builder_destroy(&sb) 153 | 154 | runes := utf8.string_to_runes(cast(string)name) 155 | defer delete(runes) 156 | 157 | in_acronym := false 158 | 159 | for i := 0; i < len(runes) - 1; i += 1 { 160 | r := runes[i] 161 | peek := runes[i + 1] 162 | if i > 0 { 163 | if !unicode.is_number(r) && unicode.is_upper(r) && unicode.is_lower(peek) { 164 | fmt.sbprint(&sb, '_') 165 | fmt.sbprint(&sb, r) 166 | in_acronym = false 167 | continue 168 | } 169 | 170 | if !in_acronym && unicode.is_upper(peek) { 171 | if unicode.is_number(r) { 172 | fmt.sbprint(&sb, r) 173 | in_acronym = true 174 | continue 175 | } else if unicode.is_upper(r) { 176 | fmt.sbprint(&sb, '_') 177 | fmt.sbprint(&sb, r) 178 | in_acronym = true 179 | continue 180 | } 181 | } 182 | } 183 | 184 | fmt.sbprint(&sb, unicode.to_upper(r)) 185 | } 186 | 187 | fmt.sbprint(&sb, unicode.to_upper(runes[len(runes) - 1])) 188 | s = cast(Const_Name)strings.clone(strings.to_string(sb)) 189 | return 190 | } 191 | 192 | // Ada_Case to CONST_CASE 193 | // Acroynm3dAcronym_More_Words becomes ACRONYM3DACRONYM_MORE_WORDS 194 | odin_to_const :: proc(name: Odin_Name) -> (s: Const_Name) { 195 | assert(len(name) > 0) 196 | return cast(Const_Name)strings.to_upper(cast(string)name) 197 | } 198 | 199 | // CONST_CASE to Ada_Case 200 | // ACRONYM3DACRONYM_MORE_WORDS becomes Acroynm3dAcronym_More_Words 201 | const_to_odin :: proc(name: Const_Name) -> (s: Odin_Name) { 202 | assert(len(name) > 0) 203 | 204 | sb := strings.builder_make() 205 | defer strings.builder_destroy(&sb) 206 | 207 | runes := utf8.string_to_runes(cast(string)name) 208 | defer delete(runes) 209 | 210 | if unicode.is_number(runes[0]) { 211 | fmt.sbprint(&sb, "_") 212 | } 213 | 214 | fmt.sbprint(&sb, runes[0]) 215 | for i := 1; i < len(runes); i += 1 { 216 | r := runes[i] 217 | 218 | previous := runes[i - 1] 219 | if previous == '_' { 220 | fmt.sbprint(&sb, r) 221 | continue 222 | } 223 | 224 | fmt.sbprint(&sb, unicode.to_lower(r)) 225 | } 226 | 227 | s = cast(Odin_Name)strings.clone(strings.to_string(sb)) 228 | return 229 | } 230 | 231 | snake_to_odin :: proc(name: Snake_Name) -> (s: Odin_Name) { 232 | assert(len(name) > 0) 233 | 234 | sb := strings.builder_make() 235 | defer strings.builder_destroy(&sb) 236 | 237 | runes := utf8.string_to_runes(cast(string)name) 238 | defer delete(runes) 239 | 240 | fmt.sbprint(&sb, unicode.to_upper(runes[0])) 241 | for i := 1; i < len(runes); i += 1 { 242 | r := runes[i] 243 | previous := runes[i - 1] 244 | if r == '.' { 245 | fmt.sbprint(&sb, '_') 246 | continue 247 | } 248 | 249 | // we're at a new word, print upper case 250 | if previous == '_' && r != '_' { 251 | fmt.sbprint(&sb, unicode.to_upper(r)) 252 | continue 253 | } 254 | 255 | fmt.sbprint(&sb, r) 256 | } 257 | 258 | s = cast(Odin_Name)strings.clone(strings.to_string(sb)) 259 | return 260 | } 261 | 262 | /* 263 | Copyright 2025 Dresses Digital 264 | 265 | Licensed under the Apache License, Version 2.0 (the "License"); 266 | you may not use this file except in compliance with the License. 267 | You may obtain a copy of the License at 268 | 269 | http://www.apache.org/licenses/LICENSE-2.0 270 | 271 | Unless required by applicable law or agreed to in writing, software 272 | distributed under the License is distributed on an "AS IS" BASIS, 273 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 274 | See the License for the specific language governing permissions and 275 | limitations under the License. 276 | */ 277 | --------------------------------------------------------------------------------