Browse Source

Merge pull request #1227 from dsnopek/4.1-cherrypicks-2

Cherry-picks for the godot-cpp 4.1 branch - 2nd batch
David Snopek 1 year ago
parent
commit
bc980b59ff

+ 26 - 17
README.md

@@ -2,11 +2,15 @@
 
 
 > **Warning**
 > **Warning**
 >
 >
-> This repository's `master` branch is only usable with the latest version of
-> Godot's ([GDExtension](https://godotengine.org/article/introducing-gd-extensions))
-> API (Godot 4.1 and later).
+> This repository's `master` branch is only usable with
+> [GDExtension](https://godotengine.org/article/introducing-gd-extensions)
+> from Godot's `master` branch.
 >
 >
-> For users of Godot 4.0.x, switch to the [`4.0`](https://github.com/godotengine/godot-cpp/tree/4.0) branch.
+> For users of stable branches, switch to the branch matching your target Godot version:
+> - [`4.0`](https://github.com/godotengine/godot-cpp/tree/4.0)
+> - [`4.1`](https://github.com/godotengine/godot-cpp/tree/4.1)
+>
+> Or check out the Git tag matching your Godot version (e.g. `godot-4.1.1-stable`).
 >
 >
 > For GDNative users (Godot 3.x), switch to the [`3.x`](https://github.com/godotengine/godot-cpp/tree/3.x)
 > For GDNative users (Godot 3.x), switch to the [`3.x`](https://github.com/godotengine/godot-cpp/tree/3.x)
 > or the [`3.5`](https://github.com/godotengine/godot-cpp/tree/3.5) branch.
 > or the [`3.5`](https://github.com/godotengine/godot-cpp/tree/3.5) branch.
@@ -52,9 +56,10 @@ first-party `godot-cpp` extension.
 
 
 Some compatibility breakage is to be expected as GDExtension and `godot-cpp`
 Some compatibility breakage is to be expected as GDExtension and `godot-cpp`
 get more used, documented, and critical issues get resolved. See the
 get more used, documented, and critical issues get resolved. See the
-[issue tracker](https://github.com/godotengine/godot/issues) for a list of known
-issues, and be sure to provide feedback on issues and PRs which affect your use
-of this extension.
+[Godot issue tracker](https://github.com/godotengine/godot/issues?q=is%3Aissue+is%3Aopen+label%3Atopic%3Agdextension)
+and the [godot-cpp issue tracker](https://github.com/godotengine/godot/issues)
+for a list of known issues, and be sure to provide feedback on issues and PRs
+which affect your use of this extension.
 
 
 ## Contributing
 ## Contributing
 
 
@@ -76,22 +81,22 @@ just like before.
 
 
 To use the shared lib in your Godot project you'll need a `.gdextension`
 To use the shared lib in your Godot project you'll need a `.gdextension`
 file, which replaces what was the `.gdnlib` before.
 file, which replaces what was the `.gdnlib` before.
-Follow [the example](test/demo/example.gdextension):
+See [example.gdextension](test/project/example.gdextension) used in the test project:
 
 
 ```ini
 ```ini
 [configuration]
 [configuration]
 
 
 entry_symbol = "example_library_init"
 entry_symbol = "example_library_init"
-compatibility_minimum = 4.1
+compatibility_minimum = "4.1"
 
 
 [libraries]
 [libraries]
 
 
-macos.debug = "bin/libgdexample.macos.debug.framework"
-macos.release = "bin/libgdexample.macos.release.framework"
-windows.debug.x86_64 = "bin/libgdexample.windows.debug.x86_64.dll"
-windows.release.x86_64 = "bin/libgdexample.windows.release.x86_64.dll"
-linux.debug.x86_64 = "bin/libgdexample.linux.debug.x86_64.so"
-linux.release.x86_64 = "bin/libgdexample.linux.release.x86_64.so"
+macos.debug = "res://bin/libgdexample.macos.debug.framework"
+macos.release = "res://bin/libgdexample.macos.release.framework"
+windows.debug.x86_64 = "res://bin/libgdexample.windows.debug.x86_64.dll"
+windows.release.x86_64 = "res://bin/libgdexample.windows.release.x86_64.dll"
+linux.debug.x86_64 = "res://bin/libgdexample.linux.debug.x86_64.so"
+linux.release.x86_64 = "res://bin/libgdexample.linux.release.x86_64.so"
 # Repeat for other architectures to support arm64, rv64, etc.
 # Repeat for other architectures to support arm64, rv64, etc.
 ```
 ```
 
 
@@ -129,6 +134,10 @@ void initialize_example_module(ModuleInitializationLevel p_level) {
 
 
 Any node and resource you register will be available in the corresponding `Create...` dialog. Any class will be available to scripting as well.
 Any node and resource you register will be available in the corresponding `Create...` dialog. Any class will be available to scripting as well.
 
 
-## Included example
+## Examples and templates
+
+See the [godot-cpp-template](https://github.com/godotengine/godot-cpp-template) project for a
+generic reusable template.
 
 
-Check the project in the `test` folder for an example on how to use and register different things.
+Or checkout the code for the [Summator example](https://github.com/paddy-exe/GDExtensionSummator)
+as shown in the [official documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/gdextension/gdextension_cpp_example.html).

+ 6 - 276
SConstruct

@@ -7,54 +7,9 @@ import subprocess
 from binding_generator import scons_generate_bindings, scons_emit_files
 from binding_generator import scons_generate_bindings, scons_emit_files
 from SCons.Errors import UserError
 from SCons.Errors import UserError
 
 
-EnsureSConsVersion(4, 0)
-
-
-def add_sources(sources, dir, extension):
-    for f in os.listdir(dir):
-        if f.endswith("." + extension):
-            sources.append(dir + "/" + f)
-
-
-def normalize_path(val):
-    return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val)
-
-
-def validate_file(key, val, env):
-    if not os.path.isfile(normalize_path(val)):
-        raise UserError("'%s' is not a file: %s" % (key, val))
-
-
-def validate_dir(key, val, env):
-    if not os.path.isdir(normalize_path(val)):
-        raise UserError("'%s' is not a directory: %s" % (key, val))
-
-
-def validate_parent_dir(key, val, env):
-    if not os.path.isdir(normalize_path(os.path.dirname(val))):
-        raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val)))
-
-
-def get_gdextension_dir(env):
-    return normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath))
-
-
-def get_api_file(env):
-    return normalize_path(env.get("custom_api_file", os.path.join(get_gdextension_dir(env), "extension_api.json")))
 
 
+EnsureSConsVersion(4, 0)
 
 
-# Try to detect the host platform automatically.
-# This is used if no `platform` argument is passed
-if sys.platform.startswith("linux"):
-    default_platform = "linux"
-elif sys.platform == "darwin":
-    default_platform = "macos"
-elif sys.platform == "win32" or sys.platform == "msys":
-    default_platform = "windows"
-elif ARGUMENTS.get("platform", ""):
-    default_platform = ARGUMENTS.get("platform")
-else:
-    raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
 
 
 try:
 try:
     Import("env")
     Import("env")
@@ -65,24 +20,6 @@ except:
 
 
 env.PrependENVPath("PATH", os.getenv("PATH"))
 env.PrependENVPath("PATH", os.getenv("PATH"))
 
 
-# Default num_jobs to local cpu count if not user specified.
-# SCons has a peculiarity where user-specified options won't be overridden
-# by SetOption, so we can rely on this to know if we should use our default.
-initial_num_jobs = env.GetOption("num_jobs")
-altered_num_jobs = initial_num_jobs + 1
-env.SetOption("num_jobs", altered_num_jobs)
-if env.GetOption("num_jobs") == altered_num_jobs:
-    cpu_count = os.cpu_count()
-    if cpu_count is None:
-        print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.")
-    else:
-        safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1
-        print(
-            "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument."
-            % (cpu_count, safer_cpu_count)
-        )
-        env.SetOption("num_jobs", safer_cpu_count)
-
 # Custom options and profile flags.
 # Custom options and profile flags.
 customs = ["custom.py"]
 customs = ["custom.py"]
 profile = ARGUMENTS.get("profile", "")
 profile = ARGUMENTS.get("profile", "")
@@ -92,159 +29,11 @@ if profile:
     elif os.path.isfile(profile + ".py"):
     elif os.path.isfile(profile + ".py"):
         customs.append(profile + ".py")
         customs.append(profile + ".py")
 opts = Variables(customs, ARGUMENTS)
 opts = Variables(customs, ARGUMENTS)
-
-platforms = ("linux", "macos", "windows", "android", "ios", "javascript")
-opts.Add(
-    EnumVariable(
-        key="platform",
-        help="Target platform",
-        default=env.get("platform", default_platform),
-        allowed_values=platforms,
-        ignorecase=2,
-    )
-)
-
-# Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates).
-# Godot release templates are only compatible with "template_release" builds.
-# For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds.
-opts.Add(
-    EnumVariable(
-        key="target",
-        help="Compilation target",
-        default=env.get("target", "template_debug"),
-        allowed_values=("editor", "template_release", "template_debug"),
-    )
-)
-opts.Add(
-    PathVariable(
-        key="gdextension_dir",
-        help="Path to a custom directory containing GDExtension interface header and API JSON file",
-        default=env.get("gdextension_dir", None),
-        validator=validate_dir,
-    )
-)
-opts.Add(
-    PathVariable(
-        key="custom_api_file",
-        help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)",
-        default=env.get("custom_api_file", None),
-        validator=validate_file,
-    )
-)
-opts.Add(
-    BoolVariable(
-        key="generate_bindings",
-        help="Force GDExtension API bindings generation. Auto-detected by default.",
-        default=env.get("generate_bindings", False),
-    )
-)
-opts.Add(
-    BoolVariable(
-        key="generate_template_get_node",
-        help="Generate a template version of the Node class's get_node.",
-        default=env.get("generate_template_get_node", True),
-    )
-)
-
-opts.Add(BoolVariable(key="build_library", help="Build the godot-cpp library.", default=env.get("build_library", True)))
-opts.Add(
-    EnumVariable(
-        key="precision",
-        help="Set the floating-point precision level",
-        default=env.get("precision", "single"),
-        allowed_values=("single", "double"),
-    )
-)
-
-# compiledb
-opts.Add(
-    BoolVariable(
-        key="compiledb",
-        help="Generate compilation DB (`compile_commands.json`) for external tools",
-        default=env.get("compiledb", False),
-    )
-)
-opts.Add(
-    PathVariable(
-        key="compiledb_file",
-        help="Path to a custom `compile_commands.json` file",
-        default=env.get("compiledb_file", "compile_commands.json"),
-        validator=validate_parent_dir,
-    )
-)
-
-# Add platform options
-tools = {}
-for pl in platforms:
-    tool = Tool(pl, toolpath=["tools"])
-    if hasattr(tool, "options"):
-        tool.options(opts)
-    tools[pl] = tool
-
-# CPU architecture options.
-architecture_array = ["", "universal", "x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64", "wasm32"]
-architecture_aliases = {
-    "x64": "x86_64",
-    "amd64": "x86_64",
-    "armv7": "arm32",
-    "armv8": "arm64",
-    "arm64v8": "arm64",
-    "aarch64": "arm64",
-    "rv": "rv64",
-    "riscv": "rv64",
-    "riscv64": "rv64",
-    "ppcle": "ppc32",
-    "ppc": "ppc32",
-    "ppc64le": "ppc64",
-}
-opts.Add(
-    EnumVariable(
-        key="arch",
-        help="CPU architecture",
-        default=env.get("arch", ""),
-        allowed_values=architecture_array,
-        map=architecture_aliases,
-    )
-)
-
-# Targets flags tool (optimizations, debug symbols)
-target_tool = Tool("targets", toolpath=["tools"])
-target_tool.options(opts)
-
+cpp_tool = Tool("godotcpp", toolpath=["tools"])
+cpp_tool.options(opts, env)
 opts.Update(env)
 opts.Update(env)
-Help(opts.GenerateHelpText(env))
-
-# Process CPU architecture argument.
-if env["arch"] == "":
-    # No architecture specified. Default to arm64 if building for Android,
-    # universal if building for macOS or iOS, wasm32 if building for web,
-    # otherwise default to the host architecture.
-    if env["platform"] in ["macos", "ios"]:
-        env["arch"] = "universal"
-    elif env["platform"] == "android":
-        env["arch"] = "arm64"
-    elif env["platform"] == "javascript":
-        env["arch"] = "wasm32"
-    else:
-        host_machine = platform.machine().lower()
-        if host_machine in architecture_array:
-            env["arch"] = host_machine
-        elif host_machine in architecture_aliases.keys():
-            env["arch"] = architecture_aliases[host_machine]
-        elif "86" in host_machine:
-            # Catches x86, i386, i486, i586, i686, etc.
-            env["arch"] = "x86_32"
-        else:
-            print("Unsupported CPU architecture: " + host_machine)
-            Exit()
 
 
-tool = Tool(env["platform"], toolpath=["tools"])
-
-if tool is None or not tool.exists(env):
-    raise ValueError("Required toolchain not found for platform " + env["platform"])
-
-tool.generate(env)
-target_tool.generate(env)
+Help(opts.GenerateHelpText(env))
 
 
 # Detect and print a warning listing unknown SCons variables to ease troubleshooting.
 # Detect and print a warning listing unknown SCons variables to ease troubleshooting.
 unknown = opts.UnknownVariables()
 unknown = opts.UnknownVariables()
@@ -253,71 +42,12 @@ if unknown:
     for item in unknown.items():
     for item in unknown.items():
         print("    " + item[0] + "=" + item[1])
         print("    " + item[0] + "=" + item[1])
 
 
-print("Building for architecture " + env["arch"] + " on platform " + env["platform"])
-
-# Require C++17
-if env.get("is_msvc", False):
-    env.Append(CXXFLAGS=["/std:c++17"])
-else:
-    env.Append(CXXFLAGS=["-std=c++17"])
-
-if env["precision"] == "double":
-    env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"])
-
-# compile_commands.json
-if env.get("compiledb", False):
-    env.Tool("compilation_db")
-    env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"])))
-
-# Generate bindings
-env.Append(BUILDERS={"GenerateBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)})
-
-bindings = env.GenerateBindings(
-    env.Dir("."),
-    [get_api_file(env), os.path.join(get_gdextension_dir(env), "gdextension_interface.h"), "binding_generator.py"],
-)
-
 scons_cache_path = os.environ.get("SCONS_CACHE")
 scons_cache_path = os.environ.get("SCONS_CACHE")
 if scons_cache_path is not None:
 if scons_cache_path is not None:
     CacheDir(scons_cache_path)
     CacheDir(scons_cache_path)
     Decider("MD5")
     Decider("MD5")
 
 
-# Forces bindings regeneration.
-if env["generate_bindings"]:
-    AlwaysBuild(bindings)
-    NoCache(bindings)
-
-# Includes
-env.Append(CPPPATH=[[env.Dir(d) for d in [get_gdextension_dir(env), "include", os.path.join("gen", "include")]]])
-
-# Sources to compile
-sources = []
-add_sources(sources, "src", "cpp")
-add_sources(sources, "src/classes", "cpp")
-add_sources(sources, "src/core", "cpp")
-add_sources(sources, "src/variant", "cpp")
-sources.extend([f for f in bindings if str(f).endswith(".cpp")])
-
-suffix = ".{}.{}".format(env["platform"], env["target"])
-if env.dev_build:
-    suffix += ".dev"
-if env["precision"] == "double":
-    suffix += ".double"
-suffix += "." + env["arch"]
-if env["ios_simulator"]:
-    suffix += ".simulator"
-
-# Expose it when included from another project
-env["suffix"] = suffix
-
-library = None
-env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
-library_name = "libgodot-cpp{}{}".format(suffix, env["LIBSUFFIX"])
-
-if env["build_library"]:
-    library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
-    Default(library)
+cpp_tool.generate(env)
+library = env.GodotCPP()
 
 
-env.Append(LIBPATH=[env.Dir("bin")])
-env.Append(LIBS=library_name)
 Return("env")
 Return("env")

+ 31 - 0
binding_generator.py

@@ -123,6 +123,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False):
             include_gen_folder / "variant" / "variant_size.hpp",
             include_gen_folder / "variant" / "variant_size.hpp",
             include_gen_folder / "classes" / "global_constants.hpp",
             include_gen_folder / "classes" / "global_constants.hpp",
             include_gen_folder / "classes" / "global_constants_binds.hpp",
             include_gen_folder / "classes" / "global_constants_binds.hpp",
+            include_gen_folder / "core" / "version.hpp",
         ]:
         ]:
             files.append(str(path.as_posix()))
             files.append(str(path.as_posix()))
     if sources:
     if sources:
@@ -173,6 +174,7 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision=
     print("Built-in type config: " + real_t + "_" + bits)
     print("Built-in type config: " + real_t + "_" + bits)
 
 
     generate_global_constants(api, target_dir)
     generate_global_constants(api, target_dir)
+    generate_version_header(api, target_dir)
     generate_global_constant_binds(api, target_dir)
     generate_global_constant_binds(api, target_dir)
     generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
     generate_builtin_bindings(api, target_dir, real_t + "_" + bits)
     generate_engine_classes_bindings(api, target_dir, use_template_get_node)
     generate_engine_classes_bindings(api, target_dir, use_template_get_node)
@@ -1666,6 +1668,35 @@ def generate_global_constants(api, output_dir):
         header_file.write("\n".join(header))
         header_file.write("\n".join(header))
 
 
 
 
+def generate_version_header(api, output_dir):
+    header = []
+    header_filename = "version.hpp"
+    add_header(header_filename, header)
+
+    include_gen_folder = Path(output_dir) / "include" / "godot_cpp" / "core"
+    include_gen_folder.mkdir(parents=True, exist_ok=True)
+
+    header_file_path = include_gen_folder / header_filename
+
+    header_guard = "GODOT_CPP_VERSION_HPP"
+    header.append(f"#ifndef {header_guard}")
+    header.append(f"#define {header_guard}")
+    header.append("")
+
+    header.append(f"#define GODOT_VERSION_MAJOR {api['header']['version_major']}")
+    header.append(f"#define GODOT_VERSION_MINOR {api['header']['version_minor']}")
+    header.append(f"#define GODOT_VERSION_PATCH {api['header']['version_patch']}")
+    header.append(f"#define GODOT_VERSION_STATUS \"{api['header']['version_status']}\"")
+    header.append(f"#define GODOT_VERSION_BUILD \"{api['header']['version_build']}\"")
+
+    header.append("")
+    header.append(f"#endif // {header_guard}")
+    header.append("")
+
+    with header_file_path.open("w+", encoding="utf-8") as header_file:
+        header_file.write("\n".join(header))
+
+
 def generate_global_constant_binds(api, output_dir):
 def generate_global_constant_binds(api, output_dir):
     include_gen_folder = Path(output_dir) / "include" / "godot_cpp" / "classes"
     include_gen_folder = Path(output_dir) / "include" / "godot_cpp" / "classes"
     source_gen_folder = Path(output_dir) / "src" / "classes"
     source_gen_folder = Path(output_dir) / "src" / "classes"

+ 6 - 5
include/godot_cpp/templates/cowdata.hpp

@@ -39,6 +39,7 @@
 
 
 #include <cstring>
 #include <cstring>
 #include <new>
 #include <new>
+#include <type_traits>
 
 
 namespace godot {
 namespace godot {
 
 
@@ -210,9 +211,9 @@ void CowData<T>::_unref(void *p_data) {
 	if (refc->decrement() > 0) {
 	if (refc->decrement() > 0) {
 		return; // still in use
 		return; // still in use
 	}
 	}
-	// clean up
 
 
-	if (!__has_trivial_destructor(T)) {
+	// clean up
+	if (!std::is_trivially_destructible<T>::value) {
 		uint32_t *count = _get_size();
 		uint32_t *count = _get_size();
 		T *data = (T *)(count + 1);
 		T *data = (T *)(count + 1);
 
 
@@ -247,7 +248,7 @@ uint32_t CowData<T>::_copy_on_write() {
 		T *_data = (T *)(mem_new);
 		T *_data = (T *)(mem_new);
 
 
 		// initialize new elements
 		// initialize new elements
-		if (__has_trivial_copy(T)) {
+		if (std::is_trivially_copyable<T>::value) {
 			memcpy(mem_new, _ptr, current_size * sizeof(T));
 			memcpy(mem_new, _ptr, current_size * sizeof(T));
 
 
 		} else {
 		} else {
@@ -310,7 +311,7 @@ Error CowData<T>::resize(int p_size) {
 
 
 		// construct the newly created elements
 		// construct the newly created elements
 
 
-		if (!__has_trivial_constructor(T)) {
+		if (!std::is_trivially_constructible<T>::value) {
 			T *elems = _get_data();
 			T *elems = _get_data();
 
 
 			for (int i = *_get_size(); i < p_size; i++) {
 			for (int i = *_get_size(); i < p_size; i++) {
@@ -321,7 +322,7 @@ Error CowData<T>::resize(int p_size) {
 		*_get_size() = p_size;
 		*_get_size() = p_size;
 
 
 	} else if (p_size < current_size) {
 	} else if (p_size < current_size) {
-		if (!__has_trivial_destructor(T)) {
+		if (!std::is_trivially_destructible<T>::value) {
 			// deinitialize no longer needed elements
 			// deinitialize no longer needed elements
 			for (uint32_t i = p_size; i < *_get_size(); i++) {
 			for (uint32_t i = p_size; i < *_get_size(); i++) {
 				T *t = &_get_data()[i];
 				T *t = &_get_data()[i];

+ 8 - 0
include/godot_cpp/variant/vector3.hpp

@@ -78,6 +78,14 @@ struct _NO_DISCARD_ Vector3 {
 		return x < y ? (y < z ? Vector3::AXIS_Z : Vector3::AXIS_Y) : (x < z ? Vector3::AXIS_Z : Vector3::AXIS_X);
 		return x < y ? (y < z ? Vector3::AXIS_Z : Vector3::AXIS_Y) : (x < z ? Vector3::AXIS_Z : Vector3::AXIS_X);
 	}
 	}
 
 
+	Vector3 min(const Vector3 &p_vector3) const {
+		return Vector3(MIN(x, p_vector3.x), MIN(y, p_vector3.y), MIN(z, p_vector3.z));
+	}
+
+	Vector3 max(const Vector3 &p_vector3) const {
+		return Vector3(MAX(x, p_vector3.x), MAX(y, p_vector3.y), MAX(z, p_vector3.z));
+	}
+
 	_FORCE_INLINE_ real_t length() const;
 	_FORCE_INLINE_ real_t length() const;
 	_FORCE_INLINE_ real_t length_squared() const;
 	_FORCE_INLINE_ real_t length_squared() const;
 
 

+ 8 - 0
include/godot_cpp/variant/vector3i.hpp

@@ -71,6 +71,14 @@ struct _NO_DISCARD_ Vector3i {
 	Vector3i::Axis min_axis_index() const;
 	Vector3i::Axis min_axis_index() const;
 	Vector3i::Axis max_axis_index() const;
 	Vector3i::Axis max_axis_index() const;
 
 
+	Vector3i min(const Vector3i &p_vector3i) const {
+		return Vector3i(MIN(x, p_vector3i.x), MIN(y, p_vector3i.y), MIN(z, p_vector3i.z));
+	}
+
+	Vector3i max(const Vector3i &p_vector3i) const {
+		return Vector3i(MAX(x, p_vector3i.x), MAX(y, p_vector3i.y), MAX(z, p_vector3i.z));
+	}
+
 	_FORCE_INLINE_ int64_t length_squared() const;
 	_FORCE_INLINE_ int64_t length_squared() const;
 	_FORCE_INLINE_ double length() const;
 	_FORCE_INLINE_ double length() const;
 
 

+ 8 - 0
include/godot_cpp/variant/vector4.hpp

@@ -70,6 +70,14 @@ struct _NO_DISCARD_ Vector4 {
 	Vector4::Axis min_axis_index() const;
 	Vector4::Axis min_axis_index() const;
 	Vector4::Axis max_axis_index() const;
 	Vector4::Axis max_axis_index() const;
 
 
+	Vector4 min(const Vector4 &p_vector4) const {
+		return Vector4(MIN(x, p_vector4.x), MIN(y, p_vector4.y), MIN(z, p_vector4.z), MIN(w, p_vector4.w));
+	}
+
+	Vector4 max(const Vector4 &p_vector4) const {
+		return Vector4(MAX(x, p_vector4.x), MAX(y, p_vector4.y), MAX(z, p_vector4.z), MAX(w, p_vector4.w));
+	}
+
 	_FORCE_INLINE_ real_t length_squared() const;
 	_FORCE_INLINE_ real_t length_squared() const;
 	bool is_equal_approx(const Vector4 &p_vec4) const;
 	bool is_equal_approx(const Vector4 &p_vec4) const;
 	bool is_zero_approx() const;
 	bool is_zero_approx() const;

+ 8 - 0
include/godot_cpp/variant/vector4i.hpp

@@ -73,6 +73,14 @@ struct _NO_DISCARD_ Vector4i {
 	Vector4i::Axis min_axis_index() const;
 	Vector4i::Axis min_axis_index() const;
 	Vector4i::Axis max_axis_index() const;
 	Vector4i::Axis max_axis_index() const;
 
 
+	Vector4i min(const Vector4i &p_vector4i) const {
+		return Vector4i(MIN(x, p_vector4i.x), MIN(y, p_vector4i.y), MIN(z, p_vector4i.z), MIN(w, p_vector4i.w));
+	}
+
+	Vector4i max(const Vector4i &p_vector4i) const {
+		return Vector4i(MAX(x, p_vector4i.x), MAX(y, p_vector4i.y), MAX(z, p_vector4i.z), MAX(w, p_vector4i.w));
+	}
+
 	_FORCE_INLINE_ int64_t length_squared() const;
 	_FORCE_INLINE_ int64_t length_squared() const;
 	_FORCE_INLINE_ double length() const;
 	_FORCE_INLINE_ double length() const;
 
 

+ 1 - 1
test/project/example.gdextension

@@ -1,7 +1,7 @@
 [configuration]
 [configuration]
 
 
 entry_symbol = "example_library_init"
 entry_symbol = "example_library_init"
-compatibility_minimum = 4.1
+compatibility_minimum = "4.1"
 
 
 [libraries]
 [libraries]
 
 

+ 43 - 0
test/project/main.gd

@@ -94,6 +94,49 @@ func _ready():
 	example.group_subgroup_custom_position = Vector2(50, 50)
 	example.group_subgroup_custom_position = Vector2(50, 50)
 	assert_equal(example.group_subgroup_custom_position, Vector2(50, 50))
 	assert_equal(example.group_subgroup_custom_position, Vector2(50, 50))
 
 
+	# Test Object::cast_to<>() and that correct wrappers are being used.
+	var control = Control.new()
+	var sprite = Sprite2D.new()
+	var example_ref = ExampleRef.new()
+
+	assert_equal(example.test_object_cast_to_node(control), true)
+	assert_equal(example.test_object_cast_to_control(control), true)
+	assert_equal(example.test_object_cast_to_example(control), false)
+
+	assert_equal(example.test_object_cast_to_node(example), true)
+	assert_equal(example.test_object_cast_to_control(example), true)
+	assert_equal(example.test_object_cast_to_example(example), true)
+
+	assert_equal(example.test_object_cast_to_node(sprite), true)
+	assert_equal(example.test_object_cast_to_control(sprite), false)
+	assert_equal(example.test_object_cast_to_example(sprite), false)
+
+	assert_equal(example.test_object_cast_to_node(example_ref), false)
+	assert_equal(example.test_object_cast_to_control(example_ref), false)
+	assert_equal(example.test_object_cast_to_example(example_ref), false)
+
+	control.queue_free()
+	sprite.queue_free()
+
+	# Test conversions to and from Variant.
+	assert_equal(example.test_variant_vector2i_conversion(Vector2i(1, 1)), Vector2i(1, 1))
+	assert_equal(example.test_variant_vector2i_conversion(Vector2(1.0, 1.0)), Vector2i(1, 1))
+	assert_equal(example.test_variant_int_conversion(10), 10)
+	assert_equal(example.test_variant_int_conversion(10.0), 10)
+	assert_equal(example.test_variant_float_conversion(10.0), 10.0)
+	assert_equal(example.test_variant_float_conversion(10), 10.0)
+
+	# Test that ptrcalls from GDExtension to the engine are correctly encoding Object and RefCounted.
+	var new_node = Node.new()
+	example.test_add_child(new_node)
+	assert_equal(new_node.get_parent(), example)
+
+	var new_tileset = TileSet.new()
+	var new_tilemap = TileMap.new()
+	example.test_set_tileset(new_tilemap, new_tileset)
+	assert_equal(new_tilemap.tile_set, new_tileset)
+	new_tilemap.queue_free()
+
 	# Constants.
 	# Constants.
 	assert_equal(Example.FIRST, 0)
 	assert_equal(Example.FIRST, 0)
 	assert_equal(Example.ANSWER_TO_EVERYTHING, 42)
 	assert_equal(Example.ANSWER_TO_EVERYTHING, 42)

+ 1 - 1
test/project/project.godot

@@ -12,7 +12,7 @@ config_version=5
 
 
 config/name="GDExtension Test Project"
 config/name="GDExtension Test Project"
 run/main_scene="res://main.tscn"
 run/main_scene="res://main.tscn"
-config/features=PackedStringArray("4.1")
+config/features=PackedStringArray("4.2")
 config/icon="res://icon.png"
 config/icon="res://icon.png"
 
 
 [native_extensions]
 [native_extensions]

+ 43 - 0
test/src/example.cpp

@@ -141,6 +141,17 @@ void Example::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("test_string_is_fourty_two"), &Example::test_string_is_fourty_two);
 	ClassDB::bind_method(D_METHOD("test_string_is_fourty_two"), &Example::test_string_is_fourty_two);
 	ClassDB::bind_method(D_METHOD("test_vector_ops"), &Example::test_vector_ops);
 	ClassDB::bind_method(D_METHOD("test_vector_ops"), &Example::test_vector_ops);
 
 
+	ClassDB::bind_method(D_METHOD("test_object_cast_to_node", "object"), &Example::test_object_cast_to_node);
+	ClassDB::bind_method(D_METHOD("test_object_cast_to_control", "object"), &Example::test_object_cast_to_control);
+	ClassDB::bind_method(D_METHOD("test_object_cast_to_example", "object"), &Example::test_object_cast_to_example);
+
+	ClassDB::bind_method(D_METHOD("test_variant_vector2i_conversion", "variant"), &Example::test_variant_vector2i_conversion);
+	ClassDB::bind_method(D_METHOD("test_variant_int_conversion", "variant"), &Example::test_variant_int_conversion);
+	ClassDB::bind_method(D_METHOD("test_variant_float_conversion", "variant"), &Example::test_variant_float_conversion);
+
+	ClassDB::bind_method(D_METHOD("test_add_child", "node"), &Example::test_add_child);
+	ClassDB::bind_method(D_METHOD("test_set_tileset", "tilemap", "tileset"), &Example::test_set_tileset);
+
 	ClassDB::bind_method(D_METHOD("test_bitfield", "flags"), &Example::test_bitfield);
 	ClassDB::bind_method(D_METHOD("test_bitfield", "flags"), &Example::test_bitfield);
 
 
 	ClassDB::bind_method(D_METHOD("test_rpc", "value"), &Example::test_rpc);
 	ClassDB::bind_method(D_METHOD("test_rpc", "value"), &Example::test_rpc);
@@ -348,6 +359,38 @@ Example *Example::test_node_argument(Example *p_node) const {
 	return p_node;
 	return p_node;
 }
 }
 
 
+bool Example::test_object_cast_to_node(Object *p_object) const {
+	return Object::cast_to<Node>(p_object) != nullptr;
+}
+
+bool Example::test_object_cast_to_control(Object *p_object) const {
+	return Object::cast_to<Control>(p_object) != nullptr;
+}
+
+bool Example::test_object_cast_to_example(Object *p_object) const {
+	return Object::cast_to<Example>(p_object) != nullptr;
+}
+
+Vector2i Example::test_variant_vector2i_conversion(const Variant &p_variant) const {
+	return p_variant;
+}
+
+int Example::test_variant_int_conversion(const Variant &p_variant) const {
+	return p_variant;
+}
+
+float Example::test_variant_float_conversion(const Variant &p_variant) const {
+	return p_variant;
+}
+
+void Example::test_add_child(Node *p_node) {
+	add_child(p_node);
+}
+
+void Example::test_set_tileset(TileMap *p_tilemap, const Ref<TileSet> &p_tileset) const {
+	p_tilemap->set_tileset(p_tileset);
+}
+
 BitField<Example::Flags> Example::test_bitfield(BitField<Flags> flags) {
 BitField<Example::Flags> Example::test_bitfield(BitField<Flags> flags) {
 	return flags;
 	return flags;
 }
 }

+ 13 - 0
test/src/example.h

@@ -18,6 +18,8 @@
 #include <godot_cpp/classes/global_constants.hpp>
 #include <godot_cpp/classes/global_constants.hpp>
 #include <godot_cpp/classes/image.hpp>
 #include <godot_cpp/classes/image.hpp>
 #include <godot_cpp/classes/input_event_key.hpp>
 #include <godot_cpp/classes/input_event_key.hpp>
+#include <godot_cpp/classes/tile_map.hpp>
+#include <godot_cpp/classes/tile_set.hpp>
 #include <godot_cpp/classes/viewport.hpp>
 #include <godot_cpp/classes/viewport.hpp>
 
 
 #include <godot_cpp/core/binder_common.hpp>
 #include <godot_cpp/core/binder_common.hpp>
@@ -120,6 +122,17 @@ public:
 	bool test_string_is_fourty_two(const String &p_str) const;
 	bool test_string_is_fourty_two(const String &p_str) const;
 	int test_vector_ops() const;
 	int test_vector_ops() const;
 
 
+	bool test_object_cast_to_node(Object *p_object) const;
+	bool test_object_cast_to_control(Object *p_object) const;
+	bool test_object_cast_to_example(Object *p_object) const;
+
+	Vector2i test_variant_vector2i_conversion(const Variant &p_variant) const;
+	int test_variant_int_conversion(const Variant &p_variant) const;
+	float test_variant_float_conversion(const Variant &p_variant) const;
+
+	void test_add_child(Node *p_node);
+	void test_set_tileset(TileMap *p_tilemap, const Ref<TileSet> &p_tileset) const;
+
 	BitField<Flags> test_bitfield(BitField<Flags> flags);
 	BitField<Flags> test_bitfield(BitField<Flags> flags);
 
 
 	// RPC
 	// RPC

+ 309 - 0
tools/godotcpp.py

@@ -0,0 +1,309 @@
+import os, sys, platform
+
+from SCons.Variables import EnumVariable, PathVariable, BoolVariable
+from SCons.Tool import Tool
+from SCons.Builder import Builder
+
+from binding_generator import scons_generate_bindings, scons_emit_files
+
+
+def add_sources(sources, dir, extension):
+    for f in os.listdir(dir):
+        if f.endswith("." + extension):
+            sources.append(dir + "/" + f)
+
+
+def normalize_path(val, env):
+    return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val)
+
+
+def validate_file(key, val, env):
+    if not os.path.isfile(normalize_path(val, env)):
+        raise UserError("'%s' is not a file: %s" % (key, val))
+
+
+def validate_dir(key, val, env):
+    if not os.path.isdir(normalize_path(val, env)):
+        raise UserError("'%s' is not a directory: %s" % (key, val))
+
+
+def validate_parent_dir(key, val, env):
+    if not os.path.isdir(normalize_path(os.path.dirname(val), env)):
+        raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val)))
+
+
+platforms = ("linux", "macos", "windows", "android", "ios", "javascript")
+
+# CPU architecture options.
+architecture_array = [
+    "",
+    "universal",
+    "x86_32",
+    "x86_64",
+    "arm32",
+    "arm64",
+    "rv64",
+    "ppc32",
+    "ppc64",
+    "wasm32",
+]
+architecture_aliases = {
+    "x64": "x86_64",
+    "amd64": "x86_64",
+    "armv7": "arm32",
+    "armv8": "arm64",
+    "arm64v8": "arm64",
+    "aarch64": "arm64",
+    "rv": "rv64",
+    "riscv": "rv64",
+    "riscv64": "rv64",
+    "ppcle": "ppc32",
+    "ppc": "ppc32",
+    "ppc64le": "ppc64",
+}
+
+
+def exists(env):
+    return True
+
+
+def options(opts, env):
+    # Try to detect the host platform automatically.
+    # This is used if no `platform` argument is passed
+    if sys.platform.startswith("linux"):
+        default_platform = "linux"
+    elif sys.platform == "darwin":
+        default_platform = "macos"
+    elif sys.platform == "win32" or sys.platform == "msys":
+        default_platform = "windows"
+    elif ARGUMENTS.get("platform", ""):
+        default_platform = ARGUMENTS.get("platform")
+    else:
+        raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
+
+    opts.Add(
+        EnumVariable(
+            key="platform",
+            help="Target platform",
+            default=env.get("platform", default_platform),
+            allowed_values=platforms,
+            ignorecase=2,
+        )
+    )
+
+    # Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates).
+    # Godot release templates are only compatible with "template_release" builds.
+    # For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds.
+    opts.Add(
+        EnumVariable(
+            key="target",
+            help="Compilation target",
+            default=env.get("target", "template_debug"),
+            allowed_values=("editor", "template_release", "template_debug"),
+        )
+    )
+    opts.Add(
+        PathVariable(
+            key="gdextension_dir",
+            help="Path to a custom directory containing GDExtension interface header and API JSON file",
+            default=env.get("gdextension_dir", None),
+            validator=validate_dir,
+        )
+    )
+    opts.Add(
+        PathVariable(
+            key="custom_api_file",
+            help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)",
+            default=env.get("custom_api_file", None),
+            validator=validate_file,
+        )
+    )
+    opts.Add(
+        BoolVariable(
+            key="generate_bindings",
+            help="Force GDExtension API bindings generation. Auto-detected by default.",
+            default=env.get("generate_bindings", False),
+        )
+    )
+    opts.Add(
+        BoolVariable(
+            key="generate_template_get_node",
+            help="Generate a template version of the Node class's get_node.",
+            default=env.get("generate_template_get_node", True),
+        )
+    )
+    opts.Add(
+        BoolVariable(
+            key="build_library",
+            help="Build the godot-cpp library.",
+            default=env.get("build_library", True),
+        )
+    )
+    opts.Add(
+        EnumVariable(
+            key="precision",
+            help="Set the floating-point precision level",
+            default=env.get("precision", "single"),
+            allowed_values=("single", "double"),
+        )
+    )
+    opts.Add(
+        EnumVariable(
+            key="arch",
+            help="CPU architecture",
+            default=env.get("arch", ""),
+            allowed_values=architecture_array,
+            map=architecture_aliases,
+        )
+    )
+
+    # compiledb
+    opts.Add(
+        BoolVariable(
+            key="compiledb",
+            help="Generate compilation DB (`compile_commands.json`) for external tools",
+            default=env.get("compiledb", False),
+        )
+    )
+    opts.Add(
+        PathVariable(
+            key="compiledb_file",
+            help="Path to a custom `compile_commands.json` file",
+            default=env.get("compiledb_file", "compile_commands.json"),
+            validator=validate_parent_dir,
+        )
+    )
+
+    # Add platform options
+    for pl in platforms:
+        tool = Tool(pl, toolpath=["tools"])
+        if hasattr(tool, "options"):
+            tool.options(opts)
+
+    # Targets flags tool (optimizations, debug symbols)
+    target_tool = Tool("targets", toolpath=["tools"])
+    target_tool.options(opts)
+
+
+def generate(env):
+    # Default num_jobs to local cpu count if not user specified.
+    # SCons has a peculiarity where user-specified options won't be overridden
+    # by SetOption, so we can rely on this to know if we should use our default.
+    initial_num_jobs = env.GetOption("num_jobs")
+    altered_num_jobs = initial_num_jobs + 1
+    env.SetOption("num_jobs", altered_num_jobs)
+    if env.GetOption("num_jobs") == altered_num_jobs:
+        cpu_count = os.cpu_count()
+        if cpu_count is None:
+            print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.")
+        else:
+            safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1
+            print(
+                "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument."
+                % (cpu_count, safer_cpu_count)
+            )
+            env.SetOption("num_jobs", safer_cpu_count)
+
+    # Process CPU architecture argument.
+    if env["arch"] == "":
+        # No architecture specified. Default to arm64 if building for Android,
+        # universal if building for macOS or iOS, wasm32 if building for web,
+        # otherwise default to the host architecture.
+        if env["platform"] in ["macos", "ios"]:
+            env["arch"] = "universal"
+        elif env["platform"] == "android":
+            env["arch"] = "arm64"
+        elif env["platform"] == "javascript":
+            env["arch"] = "wasm32"
+        else:
+            host_machine = platform.machine().lower()
+            if host_machine in architecture_array:
+                env["arch"] = host_machine
+            elif host_machine in architecture_aliases.keys():
+                env["arch"] = architecture_aliases[host_machine]
+            elif "86" in host_machine:
+                # Catches x86, i386, i486, i586, i686, etc.
+                env["arch"] = "x86_32"
+            else:
+                print("Unsupported CPU architecture: " + host_machine)
+                Exit()
+
+    print("Building for architecture " + env["arch"] + " on platform " + env["platform"])
+
+    tool = Tool(env["platform"], toolpath=["tools"])
+
+    if tool is None or not tool.exists(env):
+        raise ValueError("Required toolchain not found for platform " + env["platform"])
+
+    tool.generate(env)
+    target_tool = Tool("targets", toolpath=["tools"])
+    target_tool.generate(env)
+
+    # Require C++17
+    if env.get("is_msvc", False):
+        env.Append(CXXFLAGS=["/std:c++17"])
+    else:
+        env.Append(CXXFLAGS=["-std=c++17"])
+
+    if env["precision"] == "double":
+        env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"])
+
+    # Suffix
+    suffix = ".{}.{}".format(env["platform"], env["target"])
+    if env.dev_build:
+        suffix += ".dev"
+    if env["precision"] == "double":
+        suffix += ".double"
+    suffix += "." + env["arch"]
+    if env["ios_simulator"]:
+        suffix += ".simulator"
+
+    env["suffix"] = suffix  # Exposed when included from another project
+    env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
+
+    # compile_commands.json
+    if env.get("compiledb", False):
+        env.Tool("compilation_db")
+        env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env)))
+
+    # Builders
+    env.Append(BUILDERS={"GodotCPPBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)})
+    env.AddMethod(_godot_cpp, "GodotCPP")
+
+
+def _godot_cpp(env):
+    api_file = normalize_path(env.get("custom_api_file", env.File("gdextension/extension_api.json").abspath), env)
+    extension_dir = normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath), env)
+    bindings = env.GodotCPPBindings(
+        env.Dir("."),
+        [
+            api_file,
+            os.path.join(extension_dir, "gdextension_interface.h"),
+            "binding_generator.py",
+        ],
+    )
+    # Forces bindings regeneration.
+    if env["generate_bindings"]:
+        AlwaysBuild(bindings)
+        NoCache(bindings)
+
+    # Sources to compile
+    sources = []
+    add_sources(sources, "src", "cpp")
+    add_sources(sources, "src/classes", "cpp")
+    add_sources(sources, "src/core", "cpp")
+    add_sources(sources, "src/variant", "cpp")
+    sources.extend([f for f in bindings if str(f).endswith(".cpp")])
+
+    # Includes
+    env.AppendUnique(CPPPATH=[env.Dir(d) for d in [extension_dir, "include", "gen/include"]])
+
+    library = None
+    library_name = "libgodot-cpp" + env["suffix"] + env["LIBSUFFIX"]
+
+    if env["build_library"]:
+        library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
+        env.Default(library)
+
+    env.AppendUnique(LIBS=[env.File("bin/%s" % library_name)])
+    return library

+ 61 - 9
tools/targets.py

@@ -1,10 +1,14 @@
 import os
 import os
+import subprocess
 import sys
 import sys
 from SCons.Script import ARGUMENTS
 from SCons.Script import ARGUMENTS
 from SCons.Variables import *
 from SCons.Variables import *
 from SCons.Variables.BoolVariable import _text2bool
 from SCons.Variables.BoolVariable import _text2bool
 
 
 
 
+# Helper methods
+
+
 def get_cmdline_bool(option, default):
 def get_cmdline_bool(option, default):
     """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line,
     """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line,
     and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings.
     and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings.
@@ -16,6 +20,24 @@ def get_cmdline_bool(option, default):
         return default
         return default
 
 
 
 
+def using_clang(env):
+    return "clang" in os.path.basename(env["CC"])
+
+
+def is_vanilla_clang(env):
+    if not using_clang(env):
+        return False
+    try:
+        version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8")
+    except (subprocess.CalledProcessError, OSError):
+        print("Couldn't parse CXX environment variable to infer compiler version.")
+        return False
+    return not version.startswith("Apple")
+
+
+# Main tool definition
+
+
 def options(opts):
 def options(opts):
     opts.Add(
     opts.Add(
         EnumVariable(
         EnumVariable(
@@ -34,19 +56,21 @@ def exists(env):
 
 
 
 
 def generate(env):
 def generate(env):
-    env.dev_build = env["dev_build"]
-    env.debug_features = env["target"] in ["editor", "template_debug"]
-    env.editor_build = env["target"] == "editor"
+    # Configuration of build targets:
+    # - Editor or template
+    # - Debug features (DEBUG_ENABLED code)
+    # - Dev only code (DEV_ENABLED code)
+    # - Optimization level
+    # - Debug symbols for crash traces / debuggers
 
 
-    if env.editor_build:
-        env.AppendUnique(CPPDEFINES=["TOOLS_ENABLED"])
+    # Keep this configuration in sync with SConstruct in upstream Godot.
 
 
-    if env.debug_features:
-        env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED", "DEBUG_METHODS_ENABLED"])
+    env.editor_build = env["target"] == "editor"
+    env.dev_build = env["dev_build"]
+    env.debug_features = env["target"] in ["editor", "template_debug"]
 
 
     if env.dev_build:
     if env.dev_build:
         opt_level = "none"
         opt_level = "none"
-        env.AppendUnique(CPPDEFINES=["DEV_ENABLED"])
     elif env.debug_features:
     elif env.debug_features:
         opt_level = "speed_trace"
         opt_level = "speed_trace"
     else:  # Release
     else:  # Release
@@ -55,6 +79,26 @@ def generate(env):
     env["optimize"] = ARGUMENTS.get("optimize", opt_level)
     env["optimize"] = ARGUMENTS.get("optimize", opt_level)
     env["debug_symbols"] = get_cmdline_bool("debug_symbols", env.dev_build)
     env["debug_symbols"] = get_cmdline_bool("debug_symbols", env.dev_build)
 
 
+    if env.editor_build:
+        env.Append(CPPDEFINES=["TOOLS_ENABLED"])
+
+    if env.debug_features:
+        # DEBUG_ENABLED enables debugging *features* and debug-only code, which is intended
+        # to give *users* extra debugging information for their game development.
+        env.Append(CPPDEFINES=["DEBUG_ENABLED"])
+        # In upstream Godot this is added in typedefs.h when DEBUG_ENABLED is set.
+        env.Append(CPPDEFINES=["DEBUG_METHODS_ENABLED"])
+
+    if env.dev_build:
+        # DEV_ENABLED enables *engine developer* code which should only be compiled for those
+        # working on the engine itself.
+        env.Append(CPPDEFINES=["DEV_ENABLED"])
+    else:
+        # Disable assert() for production targets (only used in thirdparty code).
+        env.Append(CPPDEFINES=["NDEBUG"])
+
+    # Set optimize and debug_symbols flags.
+    # "custom" means do nothing and let users set their own optimization flags.
     if env.get("is_msvc", False):
     if env.get("is_msvc", False):
         if env["debug_symbols"]:
         if env["debug_symbols"]:
             env.Append(CCFLAGS=["/Zi", "/FS"])
             env.Append(CCFLAGS=["/Zi", "/FS"])
@@ -71,13 +115,21 @@ def generate(env):
             env.Append(LINKFLAGS=["/OPT:REF"])
             env.Append(LINKFLAGS=["/OPT:REF"])
         elif env["optimize"] == "debug" or env["optimize"] == "none":
         elif env["optimize"] == "debug" or env["optimize"] == "none":
             env.Append(CCFLAGS=["/Od"])
             env.Append(CCFLAGS=["/Od"])
-
     else:
     else:
         if env["debug_symbols"]:
         if env["debug_symbols"]:
+            # Adding dwarf-4 explicitly makes stacktraces work with clang builds,
+            # otherwise addr2line doesn't understand them.
+            env.Append(CCFLAGS=["-gdwarf-4"])
             if env.dev_build:
             if env.dev_build:
                 env.Append(CCFLAGS=["-g3"])
                 env.Append(CCFLAGS=["-g3"])
             else:
             else:
                 env.Append(CCFLAGS=["-g2"])
                 env.Append(CCFLAGS=["-g2"])
+        else:
+            if using_clang(env) and not is_vanilla_clang(env):
+                # Apple Clang, its linker doesn't like -s.
+                env.Append(LINKFLAGS=["-Wl,-S", "-Wl,-x", "-Wl,-dead_strip"])
+            else:
+                env.Append(LINKFLAGS=["-s"])
 
 
         if env["optimize"] == "speed":
         if env["optimize"] == "speed":
             env.Append(CCFLAGS=["-O3"])
             env.Append(CCFLAGS=["-O3"])