├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── derive ├── Cargo.toml └── src │ ├── attribute_ops.rs │ ├── enums.rs │ ├── impl_attribute.rs │ ├── lib.rs │ └── type_paths.rs ├── download_godot_dev.nu ├── license_header.nu ├── rust-script ├── Cargo.toml ├── build.rs ├── src │ ├── apply.rs │ ├── editor_ui_hacks.rs │ ├── interface.rs │ ├── interface │ │ ├── export.rs │ │ └── signals.rs │ ├── lib.rs │ ├── runtime │ │ ├── call_context.rs │ │ ├── downgrade_self.rs │ │ ├── editor.rs │ │ ├── metadata.rs │ │ ├── mod.rs │ │ ├── resource_loader.rs │ │ ├── resource_saver.rs │ │ ├── rust_script.rs │ │ ├── rust_script_instance.rs │ │ └── rust_script_language.rs │ └── static_script_registry.rs └── tests │ ├── macro_test.rs │ └── script_derive.rs ├── rust-toolchain.toml └── tests-scripts-lib ├── Cargo.toml └── src └── lib.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | permissions: 13 | security-events: "write" 14 | 15 | jobs: 16 | build: 17 | runs-on: "ubuntu-22.04" 18 | env: 19 | RUSTFLAGS: "-D warnings" 20 | strategy: 21 | matrix: 22 | api_version: ["4-1", "4-2", "4-3", "4-4"] 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | - name: Setup Cache 28 | uses: Swatinem/rust-cache@v2 29 | with: 30 | cache-on-failure: true 31 | - name: Install Tools 32 | run: cargo install clippy-sarif sarif-fmt 33 | - name: Build Debug 34 | run: | 35 | set -o pipefail 36 | cargo build --workspace --features "godot/api-${{ matrix.api_version}}" --all-features --message-format json | clippy-sarif | tee rust-build-results.sarif | sarif-fmt 37 | - name: Build Release 38 | run: | 39 | set -o pipefail 40 | cargo build --release --workspace --features "godot/api-${{ matrix.api_version}}" --all-features --message-format json | clippy-sarif | tee rust-build-results.sarif | sarif-fmt 41 | - name: Upload Results 42 | uses: github/codeql-action/upload-sarif@v2 43 | if: ${{ always() }} 44 | with: 45 | sarif_file: rust-build-results.sarif 46 | wait-for-processing: true 47 | 48 | clippy: 49 | runs-on: "ubuntu-22.04" 50 | strategy: 51 | matrix: 52 | api_version: ["4-1", "4-2", "4-3", "4-4"] 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v3 57 | - name: Setup Cache 58 | uses: Swatinem/rust-cache@v2 59 | with: 60 | cache-on-failure: true 61 | - name: Install Tools 62 | run: cargo install clippy-sarif sarif-fmt 63 | - name: Checks 64 | run: | 65 | set -o pipefail 66 | cargo clippy --message-format json --workspace --all-features --features "godot/api-${{ matrix.api_version}}" -- -D warnings | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 67 | - name: Upload Results 68 | uses: github/codeql-action/upload-sarif@v3 69 | if: ${{ always() }} 70 | with: 71 | sarif_file: rust-clippy-results.sarif 72 | wait-for-processing: true 73 | 74 | tests: 75 | runs-on: "ubuntu-22.04" 76 | strategy: 77 | matrix: 78 | api_version: ["4-1", "4-2", "4-3", "4-4", "custom"] 79 | 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v3 83 | 84 | - name: Setup Cache 85 | uses: Swatinem/rust-cache@v2 86 | with: 87 | cache-on-failure: true 88 | 89 | - name: Install ENV 90 | run: | 91 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 92 | brew install nushell 93 | 94 | - name: Download Godot Prerelease 95 | id: prerelease_setup 96 | if: ${{ matrix.api_version == 'custom' }} 97 | run: | 98 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 99 | brew install llvm 100 | GODOT4_BIN="$(./download_godot_dev.nu)" 101 | echo "godot4_bin=$GODOT4_BIN" >> "$GITHUB_OUTPUT" 102 | 103 | - name: Tests 104 | env: 105 | LLVM_PATH: "/home/linuxbrew/.linuxbrew/opt/llvm/bin" 106 | GODOT4_BIN: ${{ steps.prerelease_setup.outputs.godot4_bin }} 107 | run: | 108 | cargo test --features "godot/api-${{ matrix.api_version}}" 109 | 110 | license: 111 | runs-on: "ubuntu-22.04" 112 | 113 | steps: 114 | - name: Checkout 115 | uses: actions/checkout@v3 116 | 117 | - name: Install ENV 118 | run: | 119 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 120 | brew install nushell 121 | 122 | - name: Check License Headers 123 | run: | 124 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 125 | ./license_header.nu 126 | git diff 127 | test $(git diff | wc -l) -eq 0 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "cfg-if" 16 | version = "1.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 19 | 20 | [[package]] 21 | name = "const-str" 22 | version = "0.5.6" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "aca749d3d3f5b87a0d6100509879f9cf486ab510803a4a4e1001da1ff61c2bd6" 25 | 26 | [[package]] 27 | name = "darling" 28 | version = "0.20.10" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 31 | dependencies = [ 32 | "darling_core", 33 | "darling_macro", 34 | ] 35 | 36 | [[package]] 37 | name = "darling_core" 38 | version = "0.20.10" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 41 | dependencies = [ 42 | "fnv", 43 | "ident_case", 44 | "proc-macro2", 45 | "quote", 46 | "strsim", 47 | "syn", 48 | ] 49 | 50 | [[package]] 51 | name = "darling_macro" 52 | version = "0.20.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 55 | dependencies = [ 56 | "darling_core", 57 | "quote", 58 | "syn", 59 | ] 60 | 61 | [[package]] 62 | name = "either" 63 | version = "1.9.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 66 | 67 | [[package]] 68 | name = "fnv" 69 | version = "1.0.7" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 72 | 73 | [[package]] 74 | name = "gdextension-api" 75 | version = "0.2.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "2ec0a03c8f9c91e3d8eb7ca56dea81c7248c03826dd3f545f33cd22ef275d4d1" 78 | 79 | [[package]] 80 | name = "getrandom" 81 | version = "0.2.10" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 84 | dependencies = [ 85 | "cfg-if", 86 | "libc", 87 | "wasi", 88 | ] 89 | 90 | [[package]] 91 | name = "glam" 92 | version = "0.30.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "6b46b9ca4690308844c644e7c634d68792467260e051c8543e0c7871662b3ba7" 95 | 96 | [[package]] 97 | name = "godot" 98 | version = "0.3.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "8be80775e8e898b3eb778324edb8184abe69b8ce1f8c9ddefdb3ca3c29dbf807" 101 | dependencies = [ 102 | "godot-core", 103 | "godot-macros", 104 | ] 105 | 106 | [[package]] 107 | name = "godot-bindings" 108 | version = "0.3.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "ffae5f2ac74e199b620c9423a63d07d1d256727ae10b8fd5f4dd1b7d0ad6e53a" 111 | dependencies = [ 112 | "gdextension-api", 113 | ] 114 | 115 | [[package]] 116 | name = "godot-cell" 117 | version = "0.3.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "359a01b262bb181fddcfabc529da221ca5045b264a07546ed1976efd0feefc06" 120 | 121 | [[package]] 122 | name = "godot-codegen" 123 | version = "0.3.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "7df7b4edf35977ab5f448fd99087725307ac6e6b88c957a61b02ec556a0fedf8" 126 | dependencies = [ 127 | "godot-bindings", 128 | "heck", 129 | "nanoserde", 130 | "proc-macro2", 131 | "quote", 132 | "regex", 133 | ] 134 | 135 | [[package]] 136 | name = "godot-core" 137 | version = "0.3.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "21b55f9a4003ff887a8729820c0c8b4db84cb17d623e77a847d31fc69db1cf33" 140 | dependencies = [ 141 | "glam", 142 | "godot-bindings", 143 | "godot-cell", 144 | "godot-codegen", 145 | "godot-ffi", 146 | ] 147 | 148 | [[package]] 149 | name = "godot-ffi" 150 | version = "0.3.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "d5b1d3fffcbd583091476f1ae0c1482e5413f2d97ce30829198bae6cd4db468e" 153 | dependencies = [ 154 | "godot-bindings", 155 | "godot-codegen", 156 | "godot-macros", 157 | "libc", 158 | ] 159 | 160 | [[package]] 161 | name = "godot-macros" 162 | version = "0.3.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "d0c19bc6f536e07249efc37cdda01ccd72aa0a6c05cd135f540c5cf5abbdaae1" 165 | dependencies = [ 166 | "godot-bindings", 167 | "libc", 168 | "proc-macro2", 169 | "quote", 170 | "venial", 171 | ] 172 | 173 | [[package]] 174 | name = "godot-rust-script" 175 | version = "0.1.0" 176 | dependencies = [ 177 | "const-str", 178 | "godot", 179 | "godot-bindings", 180 | "godot-cell", 181 | "godot-rust-script", 182 | "godot-rust-script-derive", 183 | "itertools", 184 | "once_cell", 185 | "rand", 186 | "tests-scripts-lib", 187 | "thiserror", 188 | ] 189 | 190 | [[package]] 191 | name = "godot-rust-script-derive" 192 | version = "0.1.0" 193 | dependencies = [ 194 | "darling", 195 | "itertools", 196 | "proc-macro2", 197 | "quote", 198 | "syn", 199 | ] 200 | 201 | [[package]] 202 | name = "heck" 203 | version = "0.5.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 206 | 207 | [[package]] 208 | name = "ident_case" 209 | version = "1.0.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 212 | 213 | [[package]] 214 | name = "itertools" 215 | version = "0.10.5" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 218 | dependencies = [ 219 | "either", 220 | ] 221 | 222 | [[package]] 223 | name = "libc" 224 | version = "0.2.172" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 227 | 228 | [[package]] 229 | name = "memchr" 230 | version = "2.6.4" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 233 | 234 | [[package]] 235 | name = "nanoserde" 236 | version = "0.2.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "a36fb3a748a4c9736ed7aeb5f2dfc99665247f1ce306abbddb2bf0ba2ac530a4" 239 | dependencies = [ 240 | "nanoserde-derive", 241 | ] 242 | 243 | [[package]] 244 | name = "nanoserde-derive" 245 | version = "0.2.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "a846cbc04412cf509efcd8f3694b114fc700a035fb5a37f21517f9fb019f1ebc" 248 | 249 | [[package]] 250 | name = "once_cell" 251 | version = "1.19.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 254 | 255 | [[package]] 256 | name = "ppv-lite86" 257 | version = "0.2.17" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 260 | 261 | [[package]] 262 | name = "proc-macro2" 263 | version = "1.0.86" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 266 | dependencies = [ 267 | "unicode-ident", 268 | ] 269 | 270 | [[package]] 271 | name = "quote" 272 | version = "1.0.40" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 275 | dependencies = [ 276 | "proc-macro2", 277 | ] 278 | 279 | [[package]] 280 | name = "rand" 281 | version = "0.8.5" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 284 | dependencies = [ 285 | "libc", 286 | "rand_chacha", 287 | "rand_core", 288 | ] 289 | 290 | [[package]] 291 | name = "rand_chacha" 292 | version = "0.3.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 295 | dependencies = [ 296 | "ppv-lite86", 297 | "rand_core", 298 | ] 299 | 300 | [[package]] 301 | name = "rand_core" 302 | version = "0.6.4" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 305 | dependencies = [ 306 | "getrandom", 307 | ] 308 | 309 | [[package]] 310 | name = "regex" 311 | version = "1.11.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 314 | dependencies = [ 315 | "aho-corasick", 316 | "memchr", 317 | "regex-automata", 318 | "regex-syntax", 319 | ] 320 | 321 | [[package]] 322 | name = "regex-automata" 323 | version = "0.4.9" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 326 | dependencies = [ 327 | "aho-corasick", 328 | "memchr", 329 | "regex-syntax", 330 | ] 331 | 332 | [[package]] 333 | name = "regex-syntax" 334 | version = "0.8.5" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 337 | 338 | [[package]] 339 | name = "strsim" 340 | version = "0.11.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 343 | 344 | [[package]] 345 | name = "syn" 346 | version = "2.0.75" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" 349 | dependencies = [ 350 | "proc-macro2", 351 | "quote", 352 | "unicode-ident", 353 | ] 354 | 355 | [[package]] 356 | name = "tests-scripts-lib" 357 | version = "0.1.0" 358 | dependencies = [ 359 | "godot-rust-script", 360 | ] 361 | 362 | [[package]] 363 | name = "thiserror" 364 | version = "1.0.63" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 367 | dependencies = [ 368 | "thiserror-impl", 369 | ] 370 | 371 | [[package]] 372 | name = "thiserror-impl" 373 | version = "1.0.63" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 376 | dependencies = [ 377 | "proc-macro2", 378 | "quote", 379 | "syn", 380 | ] 381 | 382 | [[package]] 383 | name = "unicode-ident" 384 | version = "1.0.12" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 387 | 388 | [[package]] 389 | name = "venial" 390 | version = "0.6.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "9a42528baceab6c7784446df2a10f4185078c39bf73dc614f154353f1a6b1229" 393 | dependencies = [ 394 | "proc-macro2", 395 | "quote", 396 | ] 397 | 398 | [[package]] 399 | name = "wasi" 400 | version = "0.11.0+wasi-snapshot-preview1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 403 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "rust-script", 5 | "derive", 6 | ] 7 | 8 | [workspace.package] 9 | version = "0.1.0" 10 | edition = "2021" 11 | 12 | [workspace.dependencies] 13 | godot = { version = "0.3", features = ["experimental-threads"] } 14 | godot-cell = "0.3" 15 | godot-bindings = "0.3" 16 | itertools = "0.10" 17 | rand = "0.8" 18 | darling = { version = "0.20" } 19 | proc-macro2 = "1.0" 20 | quote = "1" 21 | syn = "2" 22 | const-str = "0.5" 23 | thiserror = "1" 24 | 25 | godot-rust-script-derive = { path = "derive" } 26 | tests-scripts-lib = { path = "tests-scripts-lib" } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Rust Script 2 | An implementation of the rust programing language as a scripting language for the godot 4.x engine based on [godot-rust/gdext](https://github.com/godot-rust/gdext). 3 | 4 | # Important Notice 5 | 6 | **godot-rust-script is still experimental and undergoes breaking changes from time to time.** 7 | 8 | # Why? 9 | 10 | The question of why this project exists might arise, and it's a good question. The [godot-rust/gdext](https://github.com/godot-rust/gdext) 11 | project already implements excellent bindings with the engine and provides a good developer experience. If you are just looking to write code 12 | for your Godot project in rust, you most likely are already well served with gdext and definitely do not **need** this library. 13 | 14 | ## When would you want to use `godot-rust-script`? 15 | 16 | GDExtension works by allowing dynamic libraries to define their own Godot classes, which inherit directly from an existing class. These 17 | classes inherit all the functionality of their base classes. Nothing more, nothing less. Scripts, on the other hand, offer a bit more 18 | flexibility. While they also define a base class, this is more like a minimally required interface. Scripts are attached to an instance of 19 | an existing class. As long as the instance inherits the required base class, the script is compatible with it. This makes the scripts somewhat 20 | more flexible and provides more compossibility than using plain class-based inheritance. It is up to you to decide if you need this 21 | additional flexibility. 22 | 23 | # Setup 24 | 25 | To use `godot-rust-script` first follow the basic setup instructions for `gdext`. 26 | 27 | ## Add Dependency 28 | 29 | The project has to be added as a cargo dependency. At the moment, it is not available on crates.io since it is still under heavy development. 30 | This library currently re-exports the `godot` crate, but adding the `godot` dependency as well is recommended, as this most likely will change in the future. 31 | 32 | ```toml 33 | [lib] 34 | crate-type = ["cdylib"] 35 | 36 | [dependencies] 37 | godot-rust-script = { git = "https://github.com/TitanNano/godot-rust-script.git", branch = "master" } 38 | ``` 39 | 40 | ## Bootstrap Script Runtime 41 | 42 | The script runtime has to be registered with the engine, as Godot does not know how scripts written in rust should be executed or even that 43 | it's available as a scripting language. 44 | 45 | For this, a manual implementation of the `godot::init::ExtensionLibrary` trait is required. Initializing and deinitalizing the runtime can then 46 | be achieved via two macro calls. The `init!(...)` macro requires the name / path to a module in your library, which represents the root module 47 | of all available scripts. 48 | 49 | ```rs 50 | struct Lib; 51 | 52 | #[gdextension] 53 | unsafe impl ExtensionLibrary for Lib { 54 | fn on_level_init(level: InitLevel) { 55 | match level { 56 | InitLevel::Core => (), 57 | InitLevel::Servers => (), 58 | InitLevel::Scene => godot_rust_script::init!(scripts), 59 | InitLevel::Editor => (), 60 | } 61 | } 62 | 63 | fn on_level_deinit(level: InitLevel) { 64 | match level { 65 | InitLevel::Editor => (), 66 | InitLevel::Scene => godot_rust_script::deinit!(), 67 | InitLevel::Servers => (), 68 | InitLevel::Core => (), 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ## Define Scripts Root 75 | 76 | Rust scripts require a root module. All rust modules under this module will be considered as potential scripts. 77 | 78 | ```rs 79 | mod example_script; 80 | 81 | godot_rust_script::define_script_root!(); 82 | ``` 83 | 84 | ## Write the first Script 85 | 86 | Godots script system is file-based, which means each of your rust scripts has to go into its own module file. Rust script then uses the name 87 | of your module file (e.g., `player_controller.rs`) to identify the script class / struct inside of it (e.g., `PlayerController`). 88 | 89 | Currently, all scripts are defined as global classes in the engine. This means each script must have a unique name. 90 | 91 | Scripts are then composed of a `struct` definition and an `impl` block. Public functions inside the impl block will be made available to 92 | other scripting languages and the engine, so they must use Godot compatible types. The same applies to struct fields. 93 | Struct fields can additionally be exported via the `#[export]` attribute, so they show up in the editor inspector. 94 | 95 | ```rs 96 | use godot_rust_script::{ 97 | godot::prelude::{godot_print, Gd, GodotString, Node3D, Object}, 98 | godot_script_impl, GodotScript, 99 | }; 100 | 101 | #[derive(Debug, GodotScript)] 102 | struct ExampleScript { 103 | #[export] 104 | pub flag: bool, 105 | pub path: GodotString, 106 | property: Option>, 107 | base: Gd, 108 | } 109 | 110 | #[godot_script_impl] 111 | impl ExampleScript { 112 | pub fn perform_action(&self, value: i32) -> bool { 113 | value > 0 114 | } 115 | 116 | pub fn _process(&mut self, delta: f64) { 117 | godot_print!( 118 | "example script doing process stuff: {}, {}", 119 | delta, 120 | self.base 121 | ); 122 | } 123 | } 124 | ``` 125 | 126 | # FAQ 127 | 128 | ## Can I write / edit scripts in the godot editor? 129 | No, it's currently neither supported nor planned. There are numerous good Rust editors and IDEs, so supporting the language inside 130 | the Godot code editor is not a goal of this project. 131 | 132 | ## Can I compile my scripts from inside the godot editor? 133 | This is currently not supported. 134 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "godot-rust-script-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | darling.workspace = true 11 | proc-macro2.workspace = true 12 | quote.workspace = true 13 | syn.workspace = true 14 | itertools.workspace = true 15 | -------------------------------------------------------------------------------- /derive/src/attribute_ops.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use darling::ast::Data; 8 | use darling::util::{self, SpannedValue, WithOriginal}; 9 | use darling::{FromAttributes, FromDeriveInput, FromField, FromMeta}; 10 | use proc_macro2::{Span, TokenStream}; 11 | use quote::{quote, quote_spanned}; 12 | use syn::spanned::Spanned; 13 | use syn::{LitStr, Meta, Type}; 14 | 15 | use crate::type_paths::godot_types; 16 | 17 | #[derive(FromAttributes, Debug)] 18 | #[darling(attributes(export))] 19 | pub struct FieldExportOps { 20 | color_no_alpha: Option>, 21 | dir: Option>, 22 | exp_easing: Option>, 23 | file: Option>, 24 | enum_options: Option>, 25 | flags: Option>, 26 | global_dir: Option>, 27 | global_file: Option>, 28 | multiline: Option>, 29 | node_path: Option>, 30 | placeholder: Option>, 31 | range: Option>, 32 | #[darling(rename = "ty")] 33 | custom_type: Option>, 34 | } 35 | 36 | impl FieldExportOps { 37 | pub fn hint(&self, ty: &Type) -> Result<(TokenStream, TokenStream), TokenStream> { 38 | let godot_types = godot_types(); 39 | let property_hints = quote!(#godot_types::global::PropertyHint); 40 | 41 | let mut result: Option<(&str, TokenStream, TokenStream)> = None; 42 | 43 | if let Some(color_no_alpha) = self.color_no_alpha.as_ref() { 44 | result = Some(( 45 | "color_no_alpha", 46 | quote_spanned!(color_no_alpha.original.span() => #property_hints::COLOR_NO_ALPHA), 47 | quote_spanned!(color_no_alpha.original.span() => String::new()), 48 | )); 49 | } 50 | 51 | if let Some(dir) = self.dir.as_ref() { 52 | let field = "dir"; 53 | 54 | if let Some((active_field, _, _)) = result { 55 | return Self::error(dir.original.span(), active_field, field); 56 | } 57 | 58 | result = Some(( 59 | field, 60 | quote_spanned!(dir.original.span() => Some(#property_hints::DIR)), 61 | quote_spanned!(dir.original.span() => Some(String::new())), 62 | )); 63 | } 64 | 65 | if let Some(exp_list) = self.exp_easing.as_ref() { 66 | let field = "exp_easing"; 67 | 68 | if let Some((active_field, _, _)) = result { 69 | return Self::error(exp_list.original.span(), active_field, field); 70 | } 71 | 72 | let parsed_params = exp_list 73 | .parsed 74 | .elems 75 | .iter() 76 | .map(ExpEasingOpts::from_expr) 77 | .collect::, _>>() 78 | .map_err(|err| err.write_errors())?; 79 | 80 | let serialized_params = parsed_params 81 | .into_iter() 82 | .map(|item| match item { 83 | ExpEasingOpts::Attenuation => "atenuation", 84 | ExpEasingOpts::PositiveOnly => "positive_only", 85 | }) 86 | .collect::>() 87 | .join(","); 88 | 89 | result = Some(( 90 | field, 91 | quote_spanned!(exp_list.original.span() => Some(#property_hints::EXP_EASING)), 92 | quote_spanned!(exp_list.original.span() => Some(String::from(#serialized_params))), 93 | )); 94 | } 95 | 96 | if let Some(list) = self.file.as_ref() { 97 | let field = "file"; 98 | 99 | if let Some((active_field, _, _)) = result { 100 | return Self::error(list.original.span(), active_field, field); 101 | } 102 | 103 | let filters = list 104 | .parsed 105 | .elems 106 | .iter() 107 | .map(String::from_expr) 108 | .collect::, _>>() 109 | .map_err(|err| err.write_errors())? 110 | .join(","); 111 | 112 | result = Some(( 113 | field, 114 | quote_spanned!(list.original.span() => Some(#property_hints::FILE)), 115 | quote_spanned!(list.original.span() => Some(String::from(#filters))), 116 | )); 117 | } 118 | 119 | if let Some(list) = self.enum_options.as_ref() { 120 | let field = "enum_options"; 121 | 122 | if let Some((active_field, _, _)) = result { 123 | return Self::error(list.original.span(), active_field, field); 124 | } 125 | 126 | let flags = list 127 | .parsed 128 | .elems 129 | .iter() 130 | .map(String::from_expr) 131 | .collect::, _>>() 132 | .map_err(|err| err.write_errors())? 133 | .join(","); 134 | 135 | result = Some(( 136 | field, 137 | quote_spanned!(list.original.span() => Some(#property_hints::ENUM)), 138 | quote_spanned!(list.original.span() => Some(String::from(#flags))), 139 | )); 140 | } 141 | 142 | if let Some(list) = self.flags.as_ref() { 143 | let field = "flags"; 144 | 145 | if let Some((active_field, _, _)) = result { 146 | return Self::error(list.original.span(), active_field, field); 147 | } 148 | 149 | let flags = list 150 | .parsed 151 | .elems 152 | .iter() 153 | .map(String::from_expr) 154 | .collect::, _>>() 155 | .map_err(|err| err.write_errors())? 156 | .join(","); 157 | 158 | result = Some(( 159 | field, 160 | quote_spanned!(list.original.span() => Some(#property_hints::FLAGS)), 161 | quote_spanned!(list.original.span() => Some(String::from(#flags))), 162 | )); 163 | } 164 | 165 | if let Some(global_dir) = self.global_dir.as_ref() { 166 | let field = "global_dir"; 167 | 168 | if let Some((active_field, _, _)) = result { 169 | return Self::error(global_dir.original.span(), active_field, field); 170 | } 171 | 172 | result = Some(( 173 | field, 174 | quote_spanned!(global_dir.original.span() => Some(#property_hints::GLOBAL_DIR)), 175 | quote_spanned!(global_dir.original.span() => Some(String::new())), 176 | )); 177 | } 178 | 179 | if let Some(global_file) = self.global_file.as_ref() { 180 | let field = "global_file"; 181 | 182 | if let Some((active_field, _, _)) = result { 183 | return Self::error(global_file.original.span(), active_field, field); 184 | } 185 | 186 | result = Some(( 187 | field, 188 | quote_spanned!(global_file.original.span() => Some(#property_hints::GLOBAL_FILE)), 189 | quote_spanned!(global_file.original.span() => Some(String::new())), 190 | )); 191 | } 192 | 193 | if let Some(multiline) = self.multiline.as_ref() { 194 | let field = "multiline"; 195 | 196 | if let Some((active_field, _, _)) = result { 197 | return Self::error(multiline.original.span(), active_field, field); 198 | } 199 | 200 | result = Some(( 201 | field, 202 | quote_spanned!(multiline.original.span() => Some(#property_hints::MULTILINE)), 203 | quote_spanned!(multiline.original.span() => Some(String::new())), 204 | )); 205 | } 206 | 207 | if let Some(list) = self.node_path.as_ref() { 208 | let field = "node_path"; 209 | 210 | if let Some((active_field, _, _)) = result { 211 | return Self::error(list.original.span(), active_field, field); 212 | } 213 | 214 | let types = list 215 | .parsed 216 | .elems 217 | .iter() 218 | .map(String::from_expr) 219 | .collect::, _>>() 220 | .map_err(|err| err.write_errors())? 221 | .join(","); 222 | 223 | result = Some(( 224 | field, 225 | quote_spanned!(list.original.span() => Some(#property_hints::NODE_PATH_VALID_TYPES)), 226 | quote_spanned!(list.original.span() => Some(String::from(#types))), 227 | )); 228 | } 229 | 230 | if let Some(text) = self.placeholder.as_ref() { 231 | let field = "placeholder"; 232 | 233 | if let Some((active_field, _, _)) = result { 234 | return Self::error(text.original.span(), active_field, field); 235 | } 236 | 237 | let content = &text.parsed; 238 | 239 | result = Some(( 240 | field, 241 | quote_spanned!(text.original.span() => Some(#property_hints::PLACEHOLDER_TEXT)), 242 | quote_spanned!(text.original.span() => Some(String::from(#content))), 243 | )); 244 | } 245 | 246 | if let Some(ops) = self.range.as_ref() { 247 | let field = "range"; 248 | 249 | if let Some((active_field, _, _)) = result { 250 | return Self::error(ops.original.span(), active_field, field); 251 | } 252 | 253 | let step = ops.parsed.step.unwrap_or(1.0); 254 | let hint_string = format!("{},{},{}", ops.parsed.min, ops.parsed.max, step); 255 | 256 | result = Some(( 257 | field, 258 | quote_spanned!(ops.original.span() => Some(#property_hints::RANGE)), 259 | quote_spanned!(ops.original.span() => Some(String::from(#hint_string))), 260 | )); 261 | } 262 | 263 | if let Some(attr_ty) = self.custom_type.as_ref() { 264 | let field = "ty"; 265 | 266 | if let Some((active_field, _, _)) = result { 267 | return Self::error(attr_ty.original.span(), active_field, field); 268 | } 269 | 270 | let attr_ty_raw = &attr_ty.parsed; 271 | 272 | let hint = quote_spanned!(ty.span() => None); 273 | let hint_string = 274 | quote_spanned!(attr_ty.original.span() => Some(String::from(#attr_ty_raw))); 275 | 276 | result = Some((field, hint, hint_string)); 277 | } 278 | 279 | let (hint, hint_string) = result 280 | .map(|(_, tokens, hint_string)| (tokens, hint_string)) 281 | .unwrap_or_else(|| (quote!(None), quote!(None))); 282 | 283 | let default_hint = quote_spanned!(ty.span() => <#ty as ::godot_rust_script::GodotScriptExport>::hint(#hint)); 284 | let default_hint_string = quote_spanned!(ty.span() => <#ty as ::godot_rust_script::GodotScriptExport>::hint_string(#hint, #hint_string)); 285 | 286 | Ok((default_hint, default_hint_string)) 287 | } 288 | 289 | fn error( 290 | span: Span, 291 | active_field: &str, 292 | field: &str, 293 | ) -> Result<(TokenStream, TokenStream), TokenStream> { 294 | let err = syn::Error::new( 295 | span, 296 | format!("{} is not compatible with {}", field, active_field), 297 | ) 298 | .into_compile_error(); 299 | 300 | Err(err) 301 | } 302 | } 303 | 304 | #[derive(FromMeta, Debug)] 305 | struct ExportRangeOps { 306 | min: f64, 307 | max: f64, 308 | step: Option, 309 | } 310 | 311 | #[derive(FromMeta, Debug)] 312 | enum ExpEasingOpts { 313 | Attenuation, 314 | PositiveOnly, 315 | } 316 | 317 | #[derive(FromField, Debug)] 318 | #[darling(forward_attrs(export, prop, doc, signal))] 319 | pub struct FieldOpts { 320 | pub ident: Option, 321 | pub attrs: Vec, 322 | pub vis: syn::Visibility, 323 | pub ty: syn::Type, 324 | } 325 | 326 | #[derive(FromDeriveInput, Debug)] 327 | #[darling(supports(struct_any), attributes(script), forward_attrs(doc))] 328 | pub struct GodotScriptOpts { 329 | pub ident: syn::Ident, 330 | pub data: Data>, 331 | pub base: Option, 332 | pub attrs: Vec, 333 | } 334 | 335 | #[derive(FromAttributes, Debug)] 336 | #[darling(attributes(prop))] 337 | pub struct PropertyOpts { 338 | pub get: Option, 339 | pub set: Option, 340 | } 341 | -------------------------------------------------------------------------------- /derive/src/enums.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use darling::{ 8 | ast::Data, 9 | util::{Ignored, WithOriginal}, 10 | FromDeriveInput, FromVariant, 11 | }; 12 | use itertools::Itertools; 13 | use proc_macro2::TokenStream; 14 | use quote::{quote, quote_spanned}; 15 | use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Ident, Meta, Visibility}; 16 | 17 | use crate::type_paths::{convert_error_ty, godot_types, property_hints}; 18 | 19 | #[derive(FromDeriveInput)] 20 | #[darling(supports(enum_unit), attributes(script_enum))] 21 | struct EnumDeriveInput { 22 | vis: Visibility, 23 | ident: Ident, 24 | export: Option>, 25 | data: Data, 26 | } 27 | 28 | #[derive(FromVariant)] 29 | struct EnumVariant { 30 | ident: Ident, 31 | } 32 | 33 | pub fn script_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 34 | let godot_types = godot_types(); 35 | let convert_error = convert_error_ty(); 36 | let property_hints = property_hints(); 37 | 38 | let input = parse_macro_input!(input as DeriveInput); 39 | let input = EnumDeriveInput::from_derive_input(&input).unwrap(); 40 | 41 | let enum_ident = input.ident; 42 | let enum_as_try_from = quote_spanned! {enum_ident.span()=> <#enum_ident as TryFrom>}; 43 | let enum_from_self = quote_spanned! {enum_ident.span()=> >}; 44 | let enum_error_ident = Ident::new(&format!("{}Error", enum_ident), enum_ident.span()); 45 | let enum_visibility = input.vis; 46 | 47 | let variants = input.data.take_enum().unwrap(); 48 | 49 | let (from_variants, into_variants, hint_strings): (TokenStream, TokenStream, Vec<_>) = variants 50 | .iter() 51 | .enumerate() 52 | .map(|(index, variant)| { 53 | let variant_ident = &variant.ident; 54 | let index = index as u8; 55 | 56 | ( 57 | quote_spanned! {variant_ident.span()=> #enum_ident::#variant_ident => #index,}, 58 | quote_spanned! {variant_ident.span()=> #index => Ok(#enum_ident::#variant_ident),}, 59 | format!("{variant_ident}:{index}"), 60 | ) 61 | }) 62 | .multiunzip(); 63 | let enum_property_hint_str = hint_strings.join(","); 64 | 65 | let derive_export = input.export.map(|export| { 66 | quote_spanned! {export.original.span()=> 67 | impl ::godot_rust_script::GodotScriptExport for #enum_ident { 68 | fn hint(custom: Option<#property_hints>) -> #property_hints { 69 | if let Some(custom) = custom { 70 | return custom; 71 | } 72 | 73 | #property_hints::ENUM 74 | } 75 | 76 | fn hint_string(_custom_hint: Option<#property_hints>, custom_string: Option) -> String { 77 | if let Some(custom_string) = custom_string { 78 | return custom_string; 79 | } 80 | 81 | String::from(#enum_property_hint_str) 82 | } 83 | } 84 | } 85 | }); 86 | 87 | let derived = quote! { 88 | impl #godot_types::meta::FromGodot for #enum_ident { 89 | fn try_from_godot(via: Self::Via) -> Result { 90 | #enum_as_try_from::try_from(via) 91 | .map_err(|err| #convert_error::with_error_value(err, via)) 92 | } 93 | } 94 | 95 | impl #godot_types::meta::ToGodot for #enum_ident { 96 | type ToVia<'a> = Self::Via; 97 | 98 | fn to_godot(&self) -> Self::Via { 99 | #enum_from_self::from(self) 100 | } 101 | } 102 | 103 | impl #godot_types::meta::GodotConvert for #enum_ident { 104 | type Via = u8; 105 | } 106 | 107 | impl GodotScriptEnum for #enum_ident {} 108 | 109 | impl From<&#enum_ident> for u8 { 110 | fn from(value: &#enum_ident) -> Self { 111 | match value { 112 | #from_variants 113 | } 114 | } 115 | } 116 | 117 | #[derive(Debug)] 118 | #enum_visibility struct #enum_error_ident(u8); 119 | 120 | impl ::std::fmt::Display for #enum_error_ident { 121 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 122 | write!(f, "Enum value {} is out of range.", self.0) 123 | } 124 | } 125 | 126 | impl ::std::error::Error for #enum_error_ident {} 127 | 128 | impl TryFrom for #enum_ident { 129 | type Error = #enum_error_ident; 130 | 131 | fn try_from(value: u8) -> ::std::result::Result { 132 | match value { 133 | #into_variants 134 | _ => Err(#enum_error_ident(value)), 135 | } 136 | } 137 | } 138 | 139 | #derive_export 140 | }; 141 | 142 | derived.into() 143 | } 144 | -------------------------------------------------------------------------------- /derive/src/impl_attribute.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use proc_macro2::TokenStream; 8 | use quote::{quote, quote_spanned, ToTokens}; 9 | use syn::{ 10 | parse2, parse_macro_input, spanned::Spanned, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, 11 | PatIdent, PatType, ReturnType, Token, Type, Visibility, 12 | }; 13 | 14 | use crate::{ 15 | extract_ident_from_type, is_context_type, rust_to_variant_type, 16 | type_paths::{godot_types, property_hints, string_name_ty, variant_ty}, 17 | }; 18 | 19 | pub fn godot_script_impl( 20 | _args: proc_macro::TokenStream, 21 | body: proc_macro::TokenStream, 22 | ) -> proc_macro::TokenStream { 23 | let body = parse_macro_input!(body as ItemImpl); 24 | 25 | let godot_types = godot_types(); 26 | let string_name_ty = string_name_ty(); 27 | let variant_ty = variant_ty(); 28 | let call_error_ty = quote!(#godot_types::sys::GDExtensionCallErrorType); 29 | let property_hints = property_hints(); 30 | 31 | let current_type = &body.self_ty; 32 | 33 | let result: Result, _> = body 34 | .items 35 | .iter() 36 | .filter_map(|item| match item { 37 | ImplItem::Fn(fnc) => Some(fnc), 38 | _ => None, 39 | }) 40 | .filter(|fnc| matches!(fnc.vis, syn::Visibility::Public(_))) 41 | .map(|fnc| { 42 | let fn_name = &fnc.sig.ident; 43 | let fn_name_str = fn_name.to_string(); 44 | let fn_return_ty_rust = match &fnc.sig.output { 45 | ty @ ReturnType::Default => syn::parse2::(quote_spanned!(ty.span() => ())).map_err(|err| err.into_compile_error())?, 46 | ReturnType::Type(_, ty) => (**ty).to_owned(), 47 | }; 48 | let fn_return_ty = rust_to_variant_type(&fn_return_ty_rust)?; 49 | let is_static = !fnc.sig.inputs.iter().any(|arg| matches!(arg, FnArg::Receiver(_))); 50 | 51 | let args: Vec<(TokenStream, TokenStream)> = fnc.sig.inputs 52 | .iter() 53 | .filter_map(|arg| match arg { 54 | syn::FnArg::Typed(arg) => Some(arg), 55 | syn::FnArg::Receiver(_) => None 56 | }) 57 | .enumerate() 58 | .map(|(index, arg)| { 59 | let arg_name = arg.pat.as_ref(); 60 | let arg_rust_type = arg.ty.as_ref(); 61 | let arg_type = rust_to_variant_type(arg.ty.as_ref()).unwrap(); 62 | 63 | if is_context_type(arg.ty.as_ref()) { 64 | ( 65 | quote!(), 66 | 67 | quote_spanned!(arg.span() => ctx,) 68 | ) 69 | } else { 70 | ( 71 | quote_spanned! { 72 | arg.span() => 73 | ::godot_rust_script::private_export::RustScriptPropDesc { 74 | name: stringify!(#arg_name), 75 | ty: #arg_type, 76 | class_name: <<#arg_rust_type as #godot_types::meta::GodotConvert>::Via as #godot_types::meta::GodotType>::class_name(), 77 | exported: false, 78 | hint: #property_hints::NONE, 79 | hint_string: String::new(), 80 | description: "", 81 | }, 82 | }, 83 | 84 | quote_spanned! { 85 | arg.span() => 86 | #godot_types::prelude::FromGodot::try_from_variant( 87 | args.get(#index).ok_or(#godot_types::sys::GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS)? 88 | ).map_err(|err| { 89 | #godot_types::global::godot_error!("failed to convert variant for argument {} of {}: {}", stringify!(#arg_name), #fn_name_str, err); 90 | #godot_types::sys::GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT 91 | })?, 92 | } 93 | ) 94 | } 95 | }) 96 | .collect(); 97 | 98 | let arg_count = args.len(); 99 | 100 | let (args_meta, args): (TokenStream, TokenStream) = args.into_iter().unzip(); 101 | 102 | 103 | let dispatch = quote_spanned! { 104 | fnc.span() => 105 | #fn_name_str => { 106 | if args.len() > #arg_count { 107 | return Err(#godot_types::sys::GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS); 108 | } 109 | 110 | Ok(#godot_types::prelude::ToGodot::to_variant(&self.#fn_name(#args))) 111 | }, 112 | }; 113 | 114 | let method_flag = if is_static { 115 | quote!(#godot_types::global::MethodFlags::STATIC) 116 | } else { 117 | quote!(#godot_types::global::MethodFlags::NORMAL) 118 | }; 119 | 120 | let description = fnc.attrs.iter() 121 | .filter(|attr| attr.path().is_ident("doc")) 122 | .map(|attr| attr.meta.require_name_value().unwrap().value.to_token_stream()) 123 | .reduce(|mut acc, ident| { 124 | acc.extend(quote!(, "\n", )); 125 | acc.extend(ident); 126 | acc 127 | }); 128 | 129 | let metadata = quote_spanned! { 130 | fnc.span() => 131 | ::godot_rust_script::private_export::RustScriptMethodDesc { 132 | name: #fn_name_str, 133 | arguments: Box::new([#args_meta]), 134 | return_type: ::godot_rust_script::private_export::RustScriptPropDesc { 135 | name: #fn_name_str, 136 | ty: #fn_return_ty, 137 | class_name: <<#fn_return_ty_rust as #godot_types::meta::GodotConvert>::Via as #godot_types::meta::GodotType>::class_name(), 138 | exported: false, 139 | hint: #property_hints::NONE, 140 | hint_string: String::new(), 141 | description: "", 142 | }, 143 | flags: #method_flag, 144 | description: concat!(#description), 145 | }, 146 | }; 147 | 148 | Ok((dispatch, metadata)) 149 | }) 150 | .collect(); 151 | 152 | let (method_dispatch, method_metadata): (TokenStream, TokenStream) = match result { 153 | Ok(r) => r.into_iter().unzip(), 154 | Err(err) => return err, 155 | }; 156 | 157 | let trait_impl = quote_spanned! { 158 | current_type.span() => 159 | impl ::godot_rust_script::GodotScriptImpl for #current_type { 160 | type ImplBase = ::Base; 161 | 162 | #[allow(unused_variables)] 163 | fn call_fn(&mut self, name: #string_name_ty, args: &[&#variant_ty], ctx: ::godot_rust_script::Context) -> ::std::result::Result<#variant_ty, #call_error_ty> { 164 | match name.to_string().as_str() { 165 | #method_dispatch 166 | 167 | _ => Err(#godot_types::sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD), 168 | } 169 | } 170 | } 171 | }; 172 | 173 | let metadata = quote! { 174 | ::godot_rust_script::register_script_methods!( 175 | #current_type, 176 | vec![ 177 | #method_metadata 178 | ] 179 | ); 180 | }; 181 | 182 | let pub_interface = generate_public_interface(&body); 183 | 184 | quote! { 185 | #body 186 | 187 | #trait_impl 188 | 189 | #pub_interface 190 | 191 | #metadata 192 | } 193 | .into() 194 | } 195 | 196 | fn sanitize_trait_fn_arg(arg: FnArg) -> FnArg { 197 | match arg { 198 | FnArg::Receiver(mut rec) => { 199 | rec.mutability = Some(Token![mut](rec.span())); 200 | rec.ty = parse2(quote!(&mut Self)).unwrap(); 201 | 202 | FnArg::Receiver(rec) 203 | } 204 | FnArg::Typed(ty) => FnArg::Typed(PatType { 205 | attrs: ty.attrs, 206 | pat: match *ty.pat { 207 | syn::Pat::Const(_) 208 | | syn::Pat::Lit(_) 209 | | syn::Pat::Macro(_) 210 | | syn::Pat::Or(_) 211 | | syn::Pat::Paren(_) 212 | | syn::Pat::Path(_) 213 | | syn::Pat::Range(_) 214 | | syn::Pat::Reference(_) 215 | | syn::Pat::Rest(_) 216 | | syn::Pat::Slice(_) 217 | | syn::Pat::Struct(_) 218 | | syn::Pat::Tuple(_) 219 | | syn::Pat::TupleStruct(_) 220 | | syn::Pat::Type(_) 221 | | syn::Pat::Verbatim(_) 222 | | syn::Pat::Wild(_) => ty.pat, 223 | syn::Pat::Ident(ident_pat) => Box::new(syn::Pat::Ident(PatIdent { 224 | attrs: ident_pat.attrs, 225 | by_ref: None, 226 | mutability: None, 227 | ident: ident_pat.ident, 228 | subpat: None, 229 | })), 230 | _ => ty.pat, 231 | }, 232 | colon_token: ty.colon_token, 233 | ty: ty.ty, 234 | }), 235 | } 236 | } 237 | 238 | fn generate_public_interface(impl_body: &ItemImpl) -> TokenStream { 239 | let impl_target = impl_body.self_ty.as_ref(); 240 | let script_name = match extract_ident_from_type(impl_target) { 241 | Ok(target) => target, 242 | Err(err) => return err, 243 | }; 244 | 245 | let trait_name = Ident::new(&format!("I{}", script_name), script_name.span()); 246 | 247 | let functions: Vec<_> = impl_body 248 | .items 249 | .iter() 250 | .filter_map(|func| match func { 251 | ImplItem::Fn(func @ ImplItemFn{ vis: Visibility::Public(_), .. }) => Some(func), 252 | _ => None, 253 | }) 254 | .map(|func| { 255 | let mut sig = func.sig.clone(); 256 | 257 | sig.inputs = sig 258 | .inputs 259 | .into_iter() 260 | .filter(|arg| { 261 | !matches!(arg, FnArg::Typed(PatType { attrs: _, pat: _, colon_token: _, ty }) if matches!(ty.as_ref(), Type::Path(path) if path.path.segments.last().unwrap().ident == "Context")) 262 | }) 263 | .map(sanitize_trait_fn_arg) 264 | .collect(); 265 | sig 266 | }) 267 | .collect(); 268 | 269 | let function_defs: TokenStream = functions 270 | .iter() 271 | .map(|func| quote_spanned! { func.span() => #func; }) 272 | .collect(); 273 | let function_impls: TokenStream = functions 274 | .iter() 275 | .map(|func| { 276 | let func_name = func.ident.to_string(); 277 | let args: TokenStream = func 278 | .inputs 279 | .iter() 280 | .filter_map(|arg| match arg { 281 | FnArg::Receiver(_) => None, 282 | FnArg::Typed(arg) => Some(arg), 283 | }) 284 | .map(|arg| { 285 | let pat = arg.pat.clone(); 286 | 287 | quote_spanned! { pat.span() => 288 | ::godot::meta::ToGodot::to_variant(&#pat), 289 | } 290 | }) 291 | .collect(); 292 | 293 | quote_spanned! { func.span() => 294 | #func { 295 | (*self).call(#func_name, &[#args]).to() 296 | } 297 | } 298 | }) 299 | .collect(); 300 | 301 | quote! { 302 | #[automatically_derived] 303 | #[allow(dead_code)] 304 | pub trait #trait_name { 305 | #function_defs 306 | } 307 | 308 | #[automatically_derived] 309 | #[allow(dead_code)] 310 | impl #trait_name for ::godot_rust_script::RsRef<#impl_target> { 311 | #function_impls 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | mod attribute_ops; 8 | mod enums; 9 | mod impl_attribute; 10 | mod type_paths; 11 | 12 | use attribute_ops::{FieldOpts, GodotScriptOpts}; 13 | use darling::{util::SpannedValue, FromAttributes, FromDeriveInput}; 14 | use itertools::Itertools; 15 | use proc_macro2::TokenStream; 16 | use quote::{quote, quote_spanned, ToTokens}; 17 | use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Ident, Type}; 18 | use type_paths::{godot_types, property_hints, string_name_ty, variant_ty}; 19 | 20 | use crate::attribute_ops::{FieldExportOps, PropertyOpts}; 21 | 22 | #[proc_macro_derive(GodotScript, attributes(export, script, prop, signal))] 23 | pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 24 | let input = parse_macro_input!(input as DeriveInput); 25 | 26 | let opts = GodotScriptOpts::from_derive_input(&input).unwrap(); 27 | 28 | let godot_types = godot_types(); 29 | let variant_ty = variant_ty(); 30 | let string_name_ty = string_name_ty(); 31 | let call_error_ty = quote!(#godot_types::sys::GDExtensionCallErrorType); 32 | 33 | let base_class = opts 34 | .base 35 | .map(|ident| quote!(#ident)) 36 | .unwrap_or_else(|| quote!(::godot_rust_script::godot::prelude::RefCounted)); 37 | 38 | let script_type_ident = opts.ident; 39 | let class_name = script_type_ident.to_string(); 40 | let fields = opts.data.take_struct().unwrap().fields; 41 | 42 | let ( 43 | field_metadata, 44 | signal_metadata, 45 | get_fields_dispatch, 46 | set_fields_dispatch, 47 | export_field_state, 48 | ): ( 49 | TokenStream, 50 | TokenStream, 51 | TokenStream, 52 | TokenStream, 53 | TokenStream, 54 | ) = fields 55 | .iter() 56 | .map(|field| { 57 | let signal_attr = field 58 | .attrs 59 | .iter() 60 | .find(|attr| attr.path().is_ident("signal")); 61 | let export_attr = field 62 | .attrs 63 | .iter() 64 | .find(|attr| attr.path().is_ident("export")); 65 | 66 | let is_public = matches!(field.vis, syn::Visibility::Public(_)) 67 | || field.attrs.iter().any(|attr| attr.path().is_ident("prop")); 68 | let is_exported = export_attr.is_some(); 69 | let is_signal = signal_attr.is_some(); 70 | 71 | let field_metadata = match (is_public, is_exported, is_signal) { 72 | (false, false, _) | (true, false, true) => TokenStream::default(), 73 | (false, true, _) => { 74 | let err = compile_error("Only public fields can be exported!", export_attr); 75 | 76 | quote! {#err,} 77 | } 78 | (true, _, false) => { 79 | derive_field_metadata(field, is_exported).unwrap_or_else(|err| err) 80 | } 81 | (true, true, true) => { 82 | let err = compile_error("Signals can not be exported!", export_attr); 83 | 84 | quote! {#err,} 85 | } 86 | }; 87 | 88 | let get_field_dispatch = is_public.then(|| derive_get_field_dispatch(field)); 89 | let set_field_dispatch = 90 | (is_public && !is_signal).then(|| derive_set_field_dispatch(field)); 91 | let export_field_state = 92 | (is_public && !is_signal).then(|| derive_property_state_export(field)); 93 | 94 | let signal_metadata = match (is_public, is_signal) { 95 | (false, false) | (true, false) => TokenStream::default(), 96 | (true, true) => derive_signal_metadata(field), 97 | (false, true) => { 98 | let err = compile_error("Signals must be public!", signal_attr); 99 | 100 | quote! {#err,} 101 | } 102 | }; 103 | 104 | ( 105 | field_metadata, 106 | signal_metadata, 107 | get_field_dispatch.to_token_stream(), 108 | set_field_dispatch.to_token_stream(), 109 | export_field_state.to_token_stream(), 110 | ) 111 | }) 112 | .multiunzip(); 113 | 114 | let get_fields_impl = derive_get_fields(get_fields_dispatch); 115 | let set_fields_impl = derive_set_fields(set_fields_dispatch); 116 | let properties_state_impl = derive_property_states_export(export_field_state); 117 | let default_impl = derive_default_with_base(&fields); 118 | 119 | let description = opts 120 | .attrs 121 | .iter() 122 | .filter(|attr| attr.path().is_ident("doc")) 123 | .map(|attr| { 124 | attr.meta 125 | .require_name_value() 126 | .unwrap() 127 | .value 128 | .to_token_stream() 129 | }) 130 | .reduce(|mut acc, lit| { 131 | acc.extend(quote!(,"\n",)); 132 | acc.extend(lit); 133 | acc 134 | }); 135 | 136 | let output = quote! { 137 | impl ::godot_rust_script::GodotScript for #script_type_ident { 138 | type Base = #base_class; 139 | 140 | const CLASS_NAME: &'static str = #class_name; 141 | 142 | #get_fields_impl 143 | 144 | #set_fields_impl 145 | 146 | fn call(&mut self, name: #string_name_ty, args: &[&#variant_ty], ctx: ::godot_rust_script::Context) -> ::std::result::Result<#variant_ty, #call_error_ty> { 147 | ::godot_rust_script::GodotScriptImpl::call_fn(self, name, args, ctx) 148 | } 149 | 150 | fn to_string(&self) -> String { 151 | format!("{:?}", self) 152 | } 153 | 154 | #properties_state_impl 155 | 156 | #default_impl 157 | } 158 | 159 | ::godot_rust_script::register_script_class!( 160 | #script_type_ident, 161 | #base_class, 162 | concat!(#description), 163 | vec![ 164 | #field_metadata 165 | ], 166 | vec![ 167 | #signal_metadata 168 | ] 169 | ); 170 | 171 | }; 172 | 173 | output.into() 174 | } 175 | 176 | fn rust_to_variant_type(ty: &syn::Type) -> Result { 177 | use syn::Type as T; 178 | 179 | let godot_types = godot_types(); 180 | 181 | match ty { 182 | T::Path(path) => Ok(quote_spanned! { 183 | ty.span() => { 184 | use #godot_types::sys::GodotFfi; 185 | use #godot_types::meta::GodotType; 186 | 187 | <<#path as #godot_types::meta::GodotConvert>::Via as GodotType>::Ffi::VARIANT_TYPE 188 | } 189 | }), 190 | T::Verbatim(_) => Err(syn::Error::new( 191 | ty.span(), 192 | "not sure how to handle verbatim types yet!", 193 | ) 194 | .into_compile_error()), 195 | T::Tuple(tuple) => { 196 | if !tuple.elems.is_empty() { 197 | return Err(syn::Error::new( 198 | ty.span(), 199 | format!("\"{}\" is not a supported type", quote!(#tuple)), 200 | ) 201 | .into_compile_error()); 202 | } 203 | 204 | Ok(quote_spanned! { 205 | tuple.span() => { 206 | use #godot_types::sys::GodotFfi; 207 | use #godot_types::meta::GodotType; 208 | 209 | <<#tuple as #godot_types::meta::GodotConvert>::Via as GodotType>::Ffi::VARIANT_TYPE 210 | } 211 | }) 212 | } 213 | _ => Err(syn::Error::new( 214 | ty.span(), 215 | format!("\"{}\" is not a supported type", quote!(#ty)), 216 | ) 217 | .into_compile_error()), 218 | } 219 | } 220 | 221 | fn is_context_type(ty: &syn::Type) -> bool { 222 | let syn::Type::Path(path) = ty else { 223 | return false; 224 | }; 225 | 226 | path.path 227 | .segments 228 | .last() 229 | .map(|segment| segment.ident == "Context") 230 | .unwrap_or(false) 231 | } 232 | 233 | fn derive_default_with_base(field_opts: &[SpannedValue]) -> TokenStream { 234 | let godot_types = godot_types(); 235 | let fields: TokenStream = field_opts 236 | .iter() 237 | .filter_map(|field| match field.ident.as_ref() { 238 | Some(ident) if *ident == "base" => { 239 | Some(quote_spanned!(ident.span() => #ident: base.clone().cast(),)) 240 | }, 241 | 242 | Some(ident) if field.attrs.iter().any(|attr| attr.path().is_ident("signal")) => { 243 | Some(quote_spanned!(ident.span() => #ident: ::godot_rust_script::ScriptSignal::new(base.clone(), stringify!(#ident)),)) 244 | } 245 | 246 | Some(ident) => Some(quote_spanned!(ident.span() => #ident: Default::default(),)), 247 | None => None, 248 | }) 249 | .collect(); 250 | 251 | quote! { 252 | fn default_with_base(base: #godot_types::prelude::Gd<#godot_types::prelude::Object>) -> Self { 253 | Self { 254 | #fields 255 | } 256 | } 257 | } 258 | } 259 | 260 | fn derive_get_field_dispatch(field: &SpannedValue) -> TokenStream { 261 | let godot_types = godot_types(); 262 | 263 | let field_ident = field.ident.as_ref().unwrap(); 264 | let field_name = field_ident.to_string(); 265 | 266 | let opts = match PropertyOpts::from_attributes(&field.attrs) { 267 | Ok(opts) => opts, 268 | Err(err) => return err.write_errors(), 269 | }; 270 | 271 | let accessor = match opts.get { 272 | Some(getter) => quote_spanned!(getter.span()=> #getter(&self)), 273 | None => quote_spanned!(field_ident.span()=> self.#field_ident), 274 | }; 275 | 276 | quote_spanned! {field.ty.span()=> 277 | #[allow(clippy::needless_borrow)] 278 | #field_name => Some(#godot_types::prelude::ToGodot::to_variant(&#accessor)), 279 | } 280 | } 281 | 282 | fn derive_get_fields(get_field_dispatch: TokenStream) -> TokenStream { 283 | let string_name_ty = string_name_ty(); 284 | let variant_ty = variant_ty(); 285 | 286 | quote! { 287 | fn get(&self, name: #string_name_ty) -> ::std::option::Option<#variant_ty> { 288 | match name.to_string().as_str() { 289 | #get_field_dispatch 290 | 291 | _ => None, 292 | } 293 | } 294 | } 295 | } 296 | 297 | fn derive_set_field_dispatch(field: &SpannedValue) -> TokenStream { 298 | let godot_types = godot_types(); 299 | 300 | let field_ident = field.ident.as_ref().unwrap(); 301 | let field_name = field_ident.to_string(); 302 | 303 | let opts = match PropertyOpts::from_attributes(&field.attrs) { 304 | Ok(opts) => opts, 305 | Err(err) => return err.write_errors(), 306 | }; 307 | 308 | let variant_value = quote_spanned!(field.ty.span()=> #godot_types::prelude::FromGodot::try_from_variant(&value)); 309 | 310 | let assignment = match opts.set { 311 | Some(setter) => quote_spanned!(setter.span()=> #setter(self, local_value)), 312 | None => quote_spanned!(field.ty.span() => self.#field_ident = local_value), 313 | }; 314 | 315 | quote! { 316 | #field_name => { 317 | let local_value = match #variant_value { 318 | Ok(v) => v, 319 | Err(_) => return false, 320 | }; 321 | 322 | #assignment; 323 | true 324 | }, 325 | } 326 | } 327 | 328 | fn derive_set_fields(set_field_dispatch: TokenStream) -> TokenStream { 329 | let string_name_ty = string_name_ty(); 330 | let variant_ty = variant_ty(); 331 | 332 | quote! { 333 | fn set(&mut self, name: #string_name_ty, value: #variant_ty) -> bool { 334 | match name.to_string().as_str() { 335 | #set_field_dispatch 336 | 337 | _ => false, 338 | } 339 | } 340 | } 341 | } 342 | 343 | fn derive_property_state_export(field: &SpannedValue) -> TokenStream { 344 | let string_name_ty = string_name_ty(); 345 | 346 | let Some(ident) = field.ident.as_ref() else { 347 | return Default::default(); 348 | }; 349 | 350 | let field_name = ident.to_string(); 351 | let field_string_name = quote!(#string_name_ty::from(#field_name)); 352 | 353 | quote! { 354 | (#field_string_name, self.get(#field_string_name).unwrap()), 355 | } 356 | } 357 | 358 | fn derive_property_states_export(fetch_property_states: TokenStream) -> TokenStream { 359 | let string_name_ty = string_name_ty(); 360 | let variant_ty = variant_ty(); 361 | 362 | quote! { 363 | fn property_state(&self) -> ::std::collections::HashMap<#string_name_ty, #variant_ty> { 364 | ::std::collections::HashMap::from([ 365 | #fetch_property_states 366 | ]) 367 | } 368 | } 369 | } 370 | 371 | fn derive_field_metadata( 372 | field: &SpannedValue, 373 | is_exported: bool, 374 | ) -> Result { 375 | let godot_types = godot_types(); 376 | let property_hint_ty = property_hints(); 377 | let name = field 378 | .ident 379 | .as_ref() 380 | .map(|field| field.to_string()) 381 | .unwrap_or_default(); 382 | 383 | let rust_ty = &field.ty; 384 | let ty = rust_to_variant_type(&field.ty)?; 385 | 386 | let (hint, hint_string) = is_exported 387 | .then(|| { 388 | let ops = 389 | FieldExportOps::from_attributes(&field.attrs).map_err(|err| err.write_errors())?; 390 | 391 | ops.hint(&field.ty) 392 | }) 393 | .transpose()? 394 | .unwrap_or_else(|| { 395 | ( 396 | quote_spanned!(field.span()=> #property_hint_ty::NONE), 397 | quote_spanned!(field.span()=> String::new()), 398 | ) 399 | }); 400 | 401 | let description = get_field_description(field); 402 | let item = quote! { 403 | ::godot_rust_script::private_export::RustScriptPropDesc { 404 | name: #name, 405 | ty: #ty, 406 | class_name: <<#rust_ty as #godot_types::meta::GodotConvert>::Via as #godot_types::meta::GodotType>::class_name(), 407 | exported: #is_exported, 408 | hint: #hint, 409 | hint_string: #hint_string, 410 | description: concat!(#description), 411 | }, 412 | }; 413 | 414 | Ok(item) 415 | } 416 | 417 | fn get_field_description(field: &FieldOpts) -> Option { 418 | field 419 | .attrs 420 | .iter() 421 | .filter(|attr| attr.path().is_ident("doc")) 422 | .map(|attr| { 423 | attr.meta 424 | .require_name_value() 425 | .unwrap() 426 | .value 427 | .to_token_stream() 428 | }) 429 | .reduce(|mut acc, comment| { 430 | acc.extend(quote!(, "\n", )); 431 | acc.extend(comment); 432 | acc 433 | }) 434 | } 435 | 436 | fn derive_signal_metadata(field: &SpannedValue) -> TokenStream { 437 | let signal_name = field 438 | .ident 439 | .as_ref() 440 | .map(|ident| ident.to_string()) 441 | .unwrap_or_default(); 442 | let signal_description = get_field_description(field); 443 | let signal_type = &field.ty; 444 | 445 | quote! { 446 | ::godot_rust_script::private_export::RustScriptSignalDesc { 447 | name: #signal_name, 448 | arguments: <#signal_type as ::godot_rust_script::ScriptSignal>::argument_desc(), 449 | description: concat!(#signal_description), 450 | }, 451 | } 452 | } 453 | 454 | #[proc_macro_attribute] 455 | pub fn godot_script_impl( 456 | args: proc_macro::TokenStream, 457 | body: proc_macro::TokenStream, 458 | ) -> proc_macro::TokenStream { 459 | impl_attribute::godot_script_impl(args, body) 460 | } 461 | 462 | fn compile_error(message: &str, tokens: impl ToTokens) -> TokenStream { 463 | syn::Error::new_spanned(tokens, message).into_compile_error() 464 | } 465 | 466 | fn extract_ident_from_type(impl_target: &syn::Type) -> Result { 467 | match impl_target { 468 | Type::Array(_) => Err(compile_error("Arrays are not supported!", impl_target)), 469 | Type::BareFn(_) => Err(compile_error( 470 | "Bare functions are not supported!", 471 | impl_target, 472 | )), 473 | Type::Group(_) => Err(compile_error("Groups are not supported!", impl_target)), 474 | Type::ImplTrait(_) => Err(compile_error("Impl traits are not suppored!", impl_target)), 475 | Type::Infer(_) => Err(compile_error("Infer is not supported!", impl_target)), 476 | Type::Macro(_) => Err(compile_error("Macro types are not supported!", impl_target)), 477 | Type::Never(_) => Err(compile_error("Never type is not supported!", impl_target)), 478 | Type::Paren(_) => Err(compile_error("Unsupported type!", impl_target)), 479 | Type::Path(ref path) => Ok(path.path.segments.last().unwrap().ident.clone()), 480 | Type::Ptr(_) => Err(compile_error( 481 | "Pointer types are not supported!", 482 | impl_target, 483 | )), 484 | Type::Reference(_) => Err(compile_error("References are not supported!", impl_target)), 485 | Type::Slice(_) => Err(compile_error("Slices are not supported!", impl_target)), 486 | Type::TraitObject(_) => Err(compile_error( 487 | "Trait objects are not supported!", 488 | impl_target, 489 | )), 490 | Type::Tuple(_) => Err(compile_error("Tuples are not supported!", impl_target)), 491 | Type::Verbatim(_) => Err(compile_error("Verbatim is not supported!", impl_target)), 492 | _ => Err(compile_error("Unsupported type!", impl_target)), 493 | } 494 | } 495 | 496 | #[proc_macro_derive(GodotScriptEnum, attributes(script_enum))] 497 | pub fn script_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 498 | enums::script_enum_derive(input) 499 | } 500 | -------------------------------------------------------------------------------- /derive/src/type_paths.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use proc_macro2::TokenStream; 8 | use quote::quote; 9 | 10 | pub fn godot_types() -> TokenStream { 11 | quote!(::godot_rust_script::godot) 12 | } 13 | 14 | pub fn property_hints() -> TokenStream { 15 | let godot_types = godot_types(); 16 | 17 | quote!(#godot_types::global::PropertyHint) 18 | } 19 | 20 | pub fn variant_ty() -> TokenStream { 21 | let godot_types = godot_types(); 22 | 23 | quote!(#godot_types::prelude::Variant) 24 | } 25 | 26 | pub fn string_name_ty() -> TokenStream { 27 | let godot_types = godot_types(); 28 | 29 | quote!(#godot_types::prelude::StringName) 30 | } 31 | 32 | pub fn convert_error_ty() -> TokenStream { 33 | let godot_types = godot_types(); 34 | 35 | quote!(#godot_types::meta::error::ConvertError) 36 | } 37 | -------------------------------------------------------------------------------- /download_godot_dev.nu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | 3 | const GODOT_BUILDS = "godotengine/godot-builds" 4 | 5 | let tmp_dir = mktemp -d 6 | let godot_dev_dir = $"($tmp_dir)/godot_dev" 7 | let godot_dev_zip = $"($godot_dev_dir).zip" 8 | 9 | print -e $"fetching releases from ($GODOT_BUILDS)..." 10 | let asset = http get $"https://api.github.com/repos/($GODOT_BUILDS)/releases" 11 | | sort-by -nr tag_name 12 | | filter {|item| $item.tag_name | str starts-with "4." } 13 | | get 0.assets 14 | | filter {|item| $item.name | str contains "linux.x86_64" } 15 | | get 0 16 | 17 | print -e $"downloading prebuilt prerelease from ($asset.browser_download_url)..." 18 | http get $asset.browser_download_url 19 | | save $godot_dev_zip 20 | 21 | print -e "extracting zip archive..." 22 | unzip -q -d $godot_dev_dir $godot_dev_zip 23 | 24 | ls $godot_dev_dir | get 0.name | print 25 | -------------------------------------------------------------------------------- /license_header.nu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | 3 | let source_root = $env.FILE_PWD 4 | 5 | let license_notice = "/* 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | " | lines 11 | 12 | let license_length = $license_notice | length 13 | 14 | def read_file [path: string]: nothing -> list { 15 | open --raw $path | lines 16 | } 17 | 18 | def lines []: string -> list { 19 | split row "\n" 20 | } 21 | 22 | def main []: nothing -> nothing { 23 | for file in (glob $"($source_root)/**/*.rs") { 24 | let current_header = read_file $file | first $license_length 25 | 26 | if $current_header == $license_notice { 27 | continue 28 | } 29 | 30 | read_file $file | prepend $license_notice | str join "\n" | collect | save -f $file 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rust-script/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "godot-rust-script" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | [lib] 7 | 8 | [dependencies] 9 | godot.workspace = true 10 | godot-cell.workspace = true 11 | 12 | itertools = { workspace = true, optional = true } 13 | rand = { workspace = true, optional = true } 14 | 15 | godot-rust-script-derive = { workspace = true, optional = true } 16 | once_cell = "1.19.0" 17 | const-str.workspace = true 18 | thiserror.workspace = true 19 | 20 | [dev-dependencies] 21 | tests-scripts-lib = { path = "../tests-scripts-lib" } 22 | godot-rust-script = { path = "./", features = ["runtime"] } 23 | 24 | [build-dependencies] 25 | godot-bindings.workspace = true 26 | 27 | [features] 28 | default = ["runtime", "scripts"] 29 | runtime = ["dep:itertools", "dep:rand"] 30 | scripts = ["dep:godot-rust-script-derive"] 31 | -------------------------------------------------------------------------------- /rust-script/build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | fn main() { 8 | godot_bindings::emit_godot_version_cfg(); 9 | } -------------------------------------------------------------------------------- /rust-script/src/apply.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | pub trait Apply: Sized { 8 | fn apply(mut self, cb: F) -> Self { 9 | cb(&mut self); 10 | self 11 | } 12 | } 13 | 14 | impl Apply for T {} 15 | -------------------------------------------------------------------------------- /rust-script/src/editor_ui_hacks.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use godot::classes::{EditorInterface, Engine}; 8 | use godot::global::godot_warn; 9 | use godot::meta::ToGodot; 10 | use godot::prelude::GodotConvert; 11 | 12 | #[derive(Clone, Copy)] 13 | pub enum EditorToasterSeverity { 14 | Warning, 15 | } 16 | 17 | impl From for u8 { 18 | fn from(value: EditorToasterSeverity) -> Self { 19 | use EditorToasterSeverity::*; 20 | 21 | match value { 22 | Warning => 1, 23 | } 24 | } 25 | } 26 | 27 | impl GodotConvert for EditorToasterSeverity { 28 | type Via = u8; 29 | } 30 | 31 | impl ToGodot for EditorToasterSeverity { 32 | type ToVia<'v> = Self::Via; 33 | 34 | fn to_godot(&self) -> Self::ToVia<'static> { 35 | (*self).into() 36 | } 37 | } 38 | 39 | #[expect(dead_code)] 40 | pub fn show_editor_toast(message: &str, severity: EditorToasterSeverity) { 41 | if !Engine::singleton().is_editor_hint() { 42 | return; 43 | } 44 | 45 | #[cfg(before_api = "4.2")] 46 | let Some(base_control) = Engine::singleton() 47 | .get_singleton("EditorInterface") 48 | .and_then(|obj| obj.cast::().get_base_control()) 49 | else { 50 | godot_warn!("[godot-rust-script] unable to access editor UI!"); 51 | return; 52 | }; 53 | 54 | #[cfg(since_api = "4.2")] 55 | let Some(base_control) = EditorInterface::singleton().get_base_control() else { 56 | godot_warn!("[godot-rust-script] unable to access editor UI!"); 57 | return; 58 | }; 59 | 60 | let editor_toaser = base_control 61 | .find_children_ex("*") 62 | .type_("EditorToaster") 63 | .recursive(true) 64 | .owned(false) 65 | .done() 66 | .get(0); 67 | 68 | let Some(mut editor_toaser) = editor_toaser else { 69 | godot_warn!("[godot-rust-script] unable to access editor toast notifications!"); 70 | return; 71 | }; 72 | 73 | if !editor_toaser.has_method("_popup_str") { 74 | godot_warn!("[godot-rust-script] Internal toast notifications API no longer exists!"); 75 | return; 76 | } 77 | 78 | editor_toaser.call( 79 | "_popup_str", 80 | &[message.to_variant(), severity.to_variant(), "".to_variant()], 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /rust-script/src/interface.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | mod export; 8 | mod signals; 9 | 10 | use std::marker::PhantomData; 11 | use std::ops::{Deref, DerefMut}; 12 | use std::{collections::HashMap, fmt::Debug}; 13 | 14 | use godot::meta::{FromGodot, GodotConvert, ToGodot}; 15 | use godot::obj::Inherits; 16 | use godot::prelude::{ConvertError, Gd, Object, StringName, Variant}; 17 | 18 | pub use crate::runtime::Context; 19 | 20 | pub use export::GodotScriptExport; 21 | pub use signals::{ScriptSignal, Signal}; 22 | 23 | pub trait GodotScript: Debug + GodotScriptImpl { 24 | type Base: Inherits; 25 | 26 | const CLASS_NAME: &'static str; 27 | 28 | fn set(&mut self, name: StringName, value: Variant) -> bool; 29 | fn get(&self, name: StringName) -> Option; 30 | fn call( 31 | &mut self, 32 | method: StringName, 33 | args: &[&Variant], 34 | context: Context<'_, Self>, 35 | ) -> Result; 36 | 37 | fn to_string(&self) -> String; 38 | fn property_state(&self) -> HashMap; 39 | 40 | fn default_with_base(base: godot::prelude::Gd) -> Self; 41 | } 42 | 43 | pub trait GodotScriptImpl { 44 | type ImplBase: Inherits; 45 | 46 | fn call_fn( 47 | &mut self, 48 | name: StringName, 49 | args: &[&Variant], 50 | context: Context, 51 | ) -> Result; 52 | } 53 | 54 | #[derive(Debug)] 55 | pub struct RsRef { 56 | owner: Gd, 57 | script_ty: PhantomData, 58 | } 59 | 60 | impl RsRef { 61 | pub(crate) fn new + Inherits>(owner: Gd) -> Self { 62 | Self { 63 | owner: owner.upcast(), 64 | script_ty: PhantomData, 65 | } 66 | } 67 | 68 | fn validate_script>(owner: &Gd) -> Option { 69 | let script = owner 70 | .upcast_ref::() 71 | .get_script() 72 | .try_to::>>(); 73 | 74 | let Ok(script) = script else { 75 | return Some(GodotScriptCastError::NotRustScript); 76 | }; 77 | 78 | let Some(script) = script else { 79 | return Some(GodotScriptCastError::NoScriptAttached); 80 | }; 81 | 82 | let class_name = script.bind().str_class_name(); 83 | 84 | (class_name != T::CLASS_NAME).then(|| { 85 | GodotScriptCastError::ClassMismatch(T::CLASS_NAME, script.get_class().to_string()) 86 | }) 87 | } 88 | } 89 | 90 | impl Deref for RsRef { 91 | type Target = Gd; 92 | 93 | fn deref(&self) -> &Self::Target { 94 | &self.owner 95 | } 96 | } 97 | 98 | impl DerefMut for RsRef { 99 | fn deref_mut(&mut self) -> &mut Self::Target { 100 | &mut self.owner 101 | } 102 | } 103 | 104 | impl Clone for RsRef { 105 | fn clone(&self) -> Self { 106 | Self { 107 | owner: self.owner.clone(), 108 | script_ty: PhantomData, 109 | } 110 | } 111 | } 112 | 113 | impl GodotConvert for RsRef { 114 | type Via = Gd; 115 | } 116 | 117 | impl FromGodot for RsRef 118 | where 119 | T::Base: Inherits, 120 | { 121 | fn try_from_godot(via: Self::Via) -> Result { 122 | via.try_to_script().map_err(ConvertError::with_error) 123 | } 124 | } 125 | 126 | impl ToGodot for RsRef { 127 | type ToVia<'v> 128 | = Gd 129 | where 130 | Self: 'v; 131 | 132 | fn to_godot(&self) -> Self::ToVia<'_> { 133 | self.deref().clone() 134 | } 135 | } 136 | 137 | #[derive(thiserror::Error, Debug)] 138 | pub enum GodotScriptCastError { 139 | #[error("Object has no script attached!")] 140 | NoScriptAttached, 141 | 142 | #[error("Script attached to object is not a RustScript!")] 143 | NotRustScript, 144 | 145 | #[error( 146 | "Script attached to object does not match expected script class `{0}` but found `{1}`!" 147 | )] 148 | ClassMismatch(&'static str, String), 149 | } 150 | 151 | pub trait CastToScript { 152 | fn try_to_script(&self) -> Result, GodotScriptCastError>; 153 | fn try_into_script(self) -> Result, GodotScriptCastError>; 154 | fn to_script(&self) -> RsRef; 155 | fn into_script(self) -> RsRef; 156 | } 157 | 158 | impl + Inherits> CastToScript for Gd { 159 | fn try_to_script(&self) -> Result, GodotScriptCastError> { 160 | if let Some(err) = RsRef::::validate_script(self) { 161 | return Err(err); 162 | } 163 | 164 | Ok(RsRef::new(self.clone())) 165 | } 166 | 167 | fn try_into_script(self) -> Result, GodotScriptCastError> { 168 | if let Some(err) = RsRef::::validate_script(&self) { 169 | return Err(err); 170 | } 171 | 172 | Ok(RsRef::new(self)) 173 | } 174 | 175 | fn to_script(&self) -> RsRef { 176 | self.try_to_script().unwrap_or_else(|err| { 177 | panic!( 178 | "`{}` was assumed to have rust script `{}`, but this was not the case at runtime!\nError: {}", 179 | B::class_name(), 180 | T::CLASS_NAME, 181 | err, 182 | ); 183 | }) 184 | } 185 | 186 | fn into_script(self) -> RsRef { 187 | self.try_into_script().unwrap_or_else(|err| { 188 | panic!( 189 | "`{}` was assumed to have rust script `{}`, but this was not the case at runtime!\nError: {}", 190 | B::class_name(), 191 | T::CLASS_NAME, 192 | err 193 | ); 194 | }) 195 | } 196 | } 197 | 198 | #[macro_export] 199 | macro_rules! define_script_root { 200 | () => { 201 | #[no_mangle] 202 | pub fn __godot_rust_script_init( 203 | ) -> ::std::vec::Vec<$crate::private_export::RustScriptMetaData> { 204 | use $crate::godot::obj::EngineEnum; 205 | use $crate::private_export::*; 206 | 207 | let lock = $crate::private_export::SCRIPT_REGISTRY 208 | .lock() 209 | .expect("unable to aquire mutex lock"); 210 | 211 | $crate::private_export::assemble_metadata(lock.iter()) 212 | } 213 | 214 | pub const __GODOT_RUST_SCRIPT_SRC_ROOT: &str = $crate::private_export::concat!( 215 | env!("CARGO_MANIFEST_DIR"), 216 | "/src", 217 | $crate::private_export::replace!( 218 | $crate::private_export::unwrap!($crate::private_export::strip_prefix!( 219 | module_path!(), 220 | $crate::private_export::replace!(env!("CARGO_PKG_NAME"), "-", "_") 221 | )), 222 | "::", 223 | "/" 224 | ), 225 | ); 226 | }; 227 | } 228 | 229 | #[deprecated = "Has been renamed to define_script_root!()"] 230 | #[macro_export] 231 | macro_rules! setup_library { 232 | () => { 233 | ::godot_rust_script::define_script_root!(); 234 | }; 235 | } 236 | 237 | pub trait GodotScriptEnum: GodotConvert + FromGodot + ToGodot {} 238 | 239 | #[macro_export] 240 | macro_rules! init { 241 | ($scripts_module:tt) => { 242 | $crate::RustScriptExtensionLayer::initialize( 243 | $scripts_module::__godot_rust_script_init, 244 | $scripts_module::__GODOT_RUST_SCRIPT_SRC_ROOT, 245 | ) 246 | }; 247 | } 248 | 249 | #[macro_export] 250 | macro_rules! deinit { 251 | () => { 252 | $crate::RustScriptExtensionLayer::deinitialize() 253 | }; 254 | } 255 | -------------------------------------------------------------------------------- /rust-script/src/interface/export.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | #[cfg(since_api = "4.3")] 8 | use godot::builtin::PackedVector4Array; 9 | use godot::builtin::{ 10 | Aabb, Array, Basis, Callable, Color, Dictionary, GString, NodePath, PackedByteArray, 11 | PackedColorArray, PackedFloat32Array, PackedFloat64Array, PackedInt32Array, PackedInt64Array, 12 | PackedStringArray, PackedVector2Array, PackedVector3Array, Plane, Projection, Quaternion, 13 | Rect2, Rect2i, Rid, StringName, Transform2D, Transform3D, Vector2, Vector2i, Vector3, Vector3i, 14 | Vector4, Vector4i, 15 | }; 16 | use godot::classes::{Node, Resource}; 17 | use godot::global::PropertyHint; 18 | use godot::meta::{ArrayElement, FromGodot, GodotConvert, GodotType, ToGodot}; 19 | use godot::obj::{EngineEnum, Gd}; 20 | use godot::prelude::GodotClass; 21 | use godot::sys::GodotFfi; 22 | 23 | use super::{GodotScript, RsRef}; 24 | 25 | pub trait GodotScriptExport: GodotConvert + FromGodot + ToGodot { 26 | fn hint_string(custom_hint: Option, custom_string: Option) -> String; 27 | 28 | fn hint(custom: Option) -> PropertyHint; 29 | } 30 | 31 | impl GodotScriptExport for Gd { 32 | fn hint_string(_custom_hint: Option, custom_string: Option) -> String { 33 | if let Some(custom) = custom_string { 34 | return custom; 35 | } 36 | 37 | T::class_name().to_string() 38 | } 39 | 40 | fn hint(custom: Option) -> PropertyHint { 41 | if let Some(custom) = custom { 42 | return custom; 43 | } 44 | 45 | if T::inherits::() { 46 | PropertyHint::NODE_TYPE 47 | } else if T::inherits::() { 48 | PropertyHint::RESOURCE_TYPE 49 | } else { 50 | PropertyHint::NONE 51 | } 52 | } 53 | } 54 | 55 | impl GodotScriptExport for RsRef { 56 | fn hint_string(_custom_hint: Option, custom_string: Option) -> String { 57 | if let Some(custom) = custom_string { 58 | return custom; 59 | } 60 | 61 | T::CLASS_NAME.to_string() 62 | } 63 | 64 | fn hint(custom: Option) -> PropertyHint { 65 | if let Some(custom) = custom { 66 | return custom; 67 | } 68 | 69 | if T::Base::inherits::() { 70 | PropertyHint::NODE_TYPE 71 | } else if T::Base::inherits::() { 72 | PropertyHint::RESOURCE_TYPE 73 | } else { 74 | PropertyHint::NONE 75 | } 76 | } 77 | } 78 | 79 | impl GodotScriptExport for Option 80 | where 81 | for<'v> T: 'v, 82 | for<'v> <::ToVia<'v> as GodotType>::Ffi: godot::sys::GodotNullableFfi, 83 | for<'f> <::Via as GodotType>::ToFfi<'f>: godot::sys::GodotNullableFfi, 84 | <::Via as GodotType>::Ffi: godot::sys::GodotNullableFfi, 85 | for<'v, 'f> <::ToVia<'v> as GodotType>::ToFfi<'f>: godot::sys::GodotNullableFfi, 86 | { 87 | fn hint_string(custom_hint: Option, custom_string: Option) -> String { 88 | T::hint_string(custom_hint, custom_string) 89 | } 90 | 91 | fn hint(custom: Option) -> PropertyHint { 92 | T::hint(custom) 93 | } 94 | } 95 | 96 | impl GodotScriptExport for Array { 97 | fn hint_string(custom_hint: Option, custom_string: Option) -> String { 98 | let element_type = <::Ffi as GodotFfi>::VARIANT_TYPE.ord(); 99 | let element_hint = ::hint(custom_hint).ord(); 100 | let element_hint_string = ::hint_string(custom_hint, custom_string); 101 | 102 | format!("{}/{}:{}", element_type, element_hint, element_hint_string) 103 | } 104 | 105 | fn hint(custom: Option) -> PropertyHint { 106 | if let Some(custom) = custom { 107 | return custom; 108 | }; 109 | 110 | PropertyHint::ARRAY_TYPE 111 | } 112 | } 113 | 114 | macro_rules! default_export { 115 | ($ty:ty) => { 116 | impl GodotScriptExport for $ty { 117 | fn hint_string( 118 | _custom_hint: Option, 119 | custom_string: Option, 120 | ) -> String { 121 | if let Some(custom) = custom_string { 122 | return custom; 123 | } 124 | 125 | String::new() 126 | } 127 | 128 | fn hint(custom: Option) -> PropertyHint { 129 | if let Some(custom) = custom { 130 | return custom; 131 | } 132 | 133 | PropertyHint::NONE 134 | } 135 | } 136 | }; 137 | } 138 | 139 | // Bounding Boxes 140 | default_export!(Aabb); 141 | default_export!(Rect2); 142 | default_export!(Rect2i); 143 | 144 | // Matrices 145 | default_export!(Basis); 146 | default_export!(Transform2D); 147 | default_export!(Transform3D); 148 | default_export!(Projection); 149 | 150 | // Vectors 151 | default_export!(Vector2); 152 | default_export!(Vector2i); 153 | default_export!(Vector3); 154 | default_export!(Vector3i); 155 | default_export!(Vector4); 156 | default_export!(Vector4i); 157 | 158 | // Misc Math 159 | default_export!(Quaternion); 160 | default_export!(Plane); 161 | 162 | // Stringy Types 163 | default_export!(GString); 164 | default_export!(StringName); 165 | default_export!(NodePath); 166 | 167 | default_export!(Color); 168 | 169 | // Arrays 170 | default_export!(PackedByteArray); 171 | default_export!(PackedInt32Array); 172 | default_export!(PackedInt64Array); 173 | default_export!(PackedFloat32Array); 174 | default_export!(PackedFloat64Array); 175 | default_export!(PackedStringArray); 176 | default_export!(PackedVector2Array); 177 | default_export!(PackedVector3Array); 178 | #[cfg(since_api = "4.3")] 179 | default_export!(PackedVector4Array); 180 | default_export!(PackedColorArray); 181 | 182 | // Primitives 183 | default_export!(f64); 184 | default_export!(i64); 185 | default_export!(bool); 186 | default_export!(f32); 187 | 188 | default_export!(i32); 189 | default_export!(i16); 190 | default_export!(i8); 191 | default_export!(u32); 192 | default_export!(u16); 193 | default_export!(u8); 194 | default_export!(u64); 195 | 196 | default_export!(Callable); 197 | default_export!(godot::builtin::Signal); 198 | default_export!(Dictionary); 199 | 200 | default_export!(Rid); 201 | -------------------------------------------------------------------------------- /rust-script/src/interface/signals.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use std::marker::PhantomData; 8 | 9 | use godot::builtin::{ 10 | Callable, Dictionary, GString, NodePath, StringName, Variant, Vector2, Vector3, 11 | }; 12 | use godot::classes::Object; 13 | use godot::global::{Error, PropertyHint}; 14 | use godot::meta::{GodotConvert, GodotType, ToGodot}; 15 | use godot::obj::Gd; 16 | 17 | use crate::static_script_registry::RustScriptPropDesc; 18 | 19 | pub trait ScriptSignal { 20 | type Args: SignalArguments; 21 | 22 | fn new(host: Gd, name: &'static str) -> Self; 23 | 24 | fn emit(&self, args: Self::Args); 25 | 26 | fn connect(&mut self, callable: Callable) -> Result<(), Error>; 27 | 28 | fn argument_desc() -> Box<[RustScriptPropDesc]>; 29 | 30 | fn name(&self) -> &str; 31 | } 32 | 33 | pub trait SignalArguments { 34 | fn count() -> u8; 35 | 36 | fn to_variants(&self) -> Vec; 37 | 38 | fn argument_desc() -> Box<[RustScriptPropDesc]>; 39 | } 40 | 41 | impl SignalArguments for () { 42 | fn count() -> u8 { 43 | 0 44 | } 45 | 46 | fn to_variants(&self) -> Vec { 47 | vec![] 48 | } 49 | 50 | fn argument_desc() -> Box<[RustScriptPropDesc]> { 51 | Box::new([]) 52 | } 53 | } 54 | 55 | macro_rules! count_tts { 56 | (inner $sub:expr) => {1}; 57 | ($($tts:expr)+) => {$(count_tts!(inner $tts) + )+ 0}; 58 | } 59 | 60 | macro_rules! tuple_args { 61 | (impl $($arg: ident),+) => { 62 | impl<$($arg: ToGodot),+> SignalArguments for ($($arg,)+) { 63 | fn count() -> u8 { 64 | count_tts!($($arg)+) 65 | } 66 | 67 | fn to_variants(&self) -> Vec { 68 | #[allow(non_snake_case)] 69 | let ($($arg,)+) = self; 70 | 71 | vec![ 72 | $(ToGodot::to_variant($arg)),+ 73 | ] 74 | } 75 | 76 | fn argument_desc() -> Box<[RustScriptPropDesc]> { 77 | Box::new([ 78 | $(signal_argument_desc!("0", $arg)),+ 79 | ]) 80 | } 81 | } 82 | }; 83 | 84 | (chop $($arg: ident);* | $next: ident $(, $tail: ident)*) => { 85 | tuple_args!(impl $($arg,)* $next); 86 | 87 | 88 | tuple_args!(chop $($arg;)* $next | $($tail),*); 89 | }; 90 | 91 | (chop $($arg: ident);+ |) => {}; 92 | 93 | ($($arg: ident),+) => { 94 | tuple_args!(chop | $($arg),+); 95 | } 96 | } 97 | 98 | macro_rules! single_args { 99 | (impl $arg: ty) => { 100 | impl SignalArguments for $arg { 101 | fn count() -> u8 { 102 | 1 103 | } 104 | 105 | fn to_variants(&self) -> Vec { 106 | vec![self.to_variant()] 107 | } 108 | 109 | fn argument_desc() -> Box<[RustScriptPropDesc]> { 110 | Box::new([ 111 | signal_argument_desc!("0", $arg), 112 | ]) 113 | } 114 | } 115 | }; 116 | 117 | ($($arg: ty),+) => { 118 | $(single_args!(impl $arg);)+ 119 | }; 120 | } 121 | 122 | macro_rules! signal_argument_desc { 123 | ($name:literal, $type:ty) => { 124 | RustScriptPropDesc { 125 | name: $name, 126 | ty: <<<$type as GodotConvert>::Via as GodotType>::Ffi as godot::sys::GodotFfi>::VARIANT_TYPE, 127 | class_name: <<$type as GodotConvert>::Via as GodotType>::class_name(), 128 | exported: false, 129 | hint: PropertyHint::NONE, 130 | hint_string: String::new(), 131 | description: "", 132 | } 133 | }; 134 | } 135 | 136 | tuple_args!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10); 137 | single_args!( 138 | bool, u8, u16, u32, u64, i8, i16, i32, i64, f64, GString, StringName, NodePath, Vector2, 139 | Vector3, Dictionary 140 | ); 141 | 142 | #[derive(Debug)] 143 | pub struct Signal { 144 | host: Gd, 145 | name: &'static str, 146 | args: PhantomData, 147 | } 148 | 149 | impl ScriptSignal for Signal { 150 | type Args = T; 151 | 152 | fn new(host: Gd, name: &'static str) -> Self { 153 | Self { 154 | host, 155 | name, 156 | args: PhantomData, 157 | } 158 | } 159 | 160 | fn emit(&self, args: Self::Args) { 161 | self.host 162 | .clone() 163 | .emit_signal(self.name, &args.to_variants()); 164 | } 165 | 166 | fn connect(&mut self, callable: Callable) -> Result<(), Error> { 167 | match self.host.connect(self.name, &callable) { 168 | Error::OK => Ok(()), 169 | error => Err(error), 170 | } 171 | } 172 | 173 | fn argument_desc() -> Box<[RustScriptPropDesc]> { 174 | ::argument_desc() 175 | } 176 | 177 | fn name(&self) -> &str { 178 | self.name 179 | } 180 | } 181 | 182 | impl GodotConvert for Signal { 183 | type Via = godot::builtin::Signal; 184 | } 185 | 186 | impl ToGodot for Signal { 187 | type ToVia<'v> 188 | = Self::Via 189 | where 190 | Self: 'v; 191 | 192 | fn to_godot(&self) -> Self::Via { 193 | godot::builtin::Signal::from_object_signal(&self.host, self.name) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /rust-script/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | mod apply; 8 | 9 | mod editor_ui_hacks; 10 | mod interface; 11 | mod runtime; 12 | mod static_script_registry; 13 | 14 | pub use godot_rust_script_derive::{godot_script_impl, GodotScript, GodotScriptEnum}; 15 | pub use interface::*; 16 | pub use runtime::RustScriptExtensionLayer; 17 | 18 | #[doc(hidden)] 19 | pub mod private_export { 20 | pub use crate::static_script_registry::{ 21 | assemble_metadata, create_default_data_struct, RegistryItem, RustScriptEntry, 22 | RustScriptEntryMethods, RustScriptMetaData, RustScriptMethodDesc, RustScriptPropDesc, 23 | RustScriptSignalDesc, SCRIPT_REGISTRY, 24 | }; 25 | pub use const_str::{concat, replace, strip_prefix, unwrap}; 26 | pub use godot::sys::{plugin_add, plugin_registry}; 27 | } 28 | 29 | pub use godot; 30 | -------------------------------------------------------------------------------- /rust-script/src/runtime/call_context.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use std::ops::DerefMut; 8 | use std::{fmt::Debug, marker::PhantomData}; 9 | 10 | use godot::obj::{script::ScriptBaseMut, Gd}; 11 | use godot::prelude::GodotClass; 12 | use godot_cell::blocking::GdCell; 13 | 14 | use crate::interface::GodotScriptImpl; 15 | 16 | use super::rust_script_instance::{GodotScriptObject, RustScriptInstance}; 17 | 18 | pub struct Context<'a, Script: GodotScriptImpl + ?Sized> { 19 | cell: *const GdCell>, 20 | data_ptr: *mut Box, 21 | base: ScriptBaseMut<'a, RustScriptInstance>, 22 | base_type: PhantomData