Add back support for Godot 4.3

This commit is contained in:
David Snopek
2025-12-17 21:13:06 -06:00
parent 62b4a46e72
commit ce18d99cb0
21 changed files with 304554 additions and 24 deletions

View File

@ -51,6 +51,16 @@ jobs:
godot-test-versions: "4.4-stable 4.5-stable"
cache-name: linux-x86_64-godot44
- name: 🐧 Linux (GCC) for Godot 4.3
os: ubuntu-22.04
platform: linux
artifact-name: godot-cpp-linux-glibc2.27-x86_64-release-godot43
artifact-path: bin/libgodot-cpp.linux.template_release.x86_64.a
run-tests: true
api-version: 4.3
godot-test-versions: "4.3-stable 4.4-stable 4.5-stable"
cache-name: linux-x86_64-godot43
- name: 🏁 Windows (x86_64, MSVC)
os: windows-2022
platform: windows

View File

@ -1135,9 +1135,11 @@ def generate_builtin_class_header(builtin_api, size, used_classes, fully_used_cl
if class_name == "Dictionary":
result.append("\tconst Variant &operator[](const Variant &p_key) const;")
result.append("\tVariant &operator[](const Variant &p_key);")
result.append("#if GODOT_VERSION_MINOR >= 4")
result.append(
"\tvoid set_typed(uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script);"
)
result.append("#endif")
result.append("};")
@ -1888,7 +1890,9 @@ def generate_engine_class_header(class_api, used_classes, fully_used_classes, us
# condition returns false (in such cases it can't compile due to ambiguity).
f"\t\tif constexpr (!std::is_same_v<decltype(&B::{method_name}), decltype(&T::{method_name})>) {{"
)
result.append(f"\t\t\tBIND_VIRTUAL_METHOD(T, {method_name}, {method['hash']});")
# If using an `extension_api.json` from Godot 4.3 or earlier, there will be no hash for virtual functions.
method_hash = method.get("hash", 0)
result.append(f"\t\t\tBIND_VIRTUAL_METHOD(T, {method_name}, {method_hash});")
result.append("\t\t}")
result.append("\t}")

View File

@ -119,7 +119,7 @@ function(godotcpp_options)
CACHE STRING
"The Godot API version to target (ex \"4.5\") using one of the included API JSON files"
)
set_property(CACHE GODOTCPP_API_VERSION PROPERTY STRINGS ";4.4;4.5;4.6")
set_property(CACHE GODOTCPP_API_VERSION PROPERTY STRINGS ";4.3;4.4;4.5;4.6")
set(GODOTCPP_GDEXTENSION_DIR
"gdextension"
CACHE PATH

File diff suppressed because it is too large Load Diff

View File

@ -110,6 +110,7 @@ protected:
::godot::List<::godot::PropertyInfo> plist_owned;
void _postinitialize();
virtual void _notificationv(int32_t p_what, bool p_reversed = false) {}
Wrapped(const StringName &p_godot_class);
Wrapped(GodotObject *p_godot_object);
@ -404,6 +405,11 @@ public:
_gde_binding_reference_callback, \
}; \
\
protected: \
virtual void _notificationv(int32_t p_what, bool p_reversed = false) override { \
m_class::notification_bind(this, p_what, p_reversed); \
} \
\
private:
// Don't use this for your classes, use GDCLASS() instead.

View File

@ -111,13 +111,21 @@ private:
static void _register_class(bool p_virtual = false, bool p_exposed = true, bool p_runtime = false);
template <typename T>
#if GODOT_VERSION_MINOR >= 4
static GDExtensionObjectPtr _create_instance_func(void *data, GDExtensionBool p_notify_postinitialize) {
#else
static GDExtensionObjectPtr _create_instance_func(void *data) {
#endif // GODOT_VERSION_MINOR >= 4
if constexpr (!std::is_abstract_v<T>) {
Wrapped::_set_construct_info<T>();
#if GODOT_VERSION_MINOR >= 4
T *new_object = new ("", "") T;
if (p_notify_postinitialize) {
new_object->_postinitialize();
}
#else
T *new_object = memnew(T);
#endif // GODOT_VERSION_MINOR >= 4
return new_object->_owner;
} else {
return nullptr;
@ -195,7 +203,12 @@ public:
static MethodBind *get_method(const StringName &p_class, const StringName &p_method);
#if GODOT_VERSION_MINOR >= 4
static GDExtensionClassCallVirtual get_virtual_func(void *p_userdata, GDExtensionConstStringNamePtr p_name, uint32_t p_hash);
#else
static GDExtensionClassCallVirtual get_virtual_func(void *p_userdata, GDExtensionConstStringNamePtr p_name);
#endif // GODOT_VERSION_MINOR >= 4
static const GDExtensionInstanceBindingCallbacks *get_instance_binding_callbacks(const StringName &p_class);
static void initialize(GDExtensionInitializationLevel p_level);
@ -244,14 +257,18 @@ void ClassDB::_register_class(bool p_virtual, bool p_exposed, bool p_runtime) {
// Register this class with Godot
#if GODOT_VERSION_MINOR >= 5
GDExtensionClassCreationInfo5 class_info = {
#else
#elif GODOT_VERSION_MINOR >= 4
GDExtensionClassCreationInfo4 class_info = {
#else
GDExtensionClassCreationInfo3 class_info = {
#endif
p_virtual, // GDExtensionBool is_virtual;
is_abstract, // GDExtensionBool is_abstract;
p_exposed, // GDExtensionBool is_exposed;
p_runtime, // GDExtensionBool is_runtime;
#if GODOT_VERSION_MINOR >= 4
nullptr, // GDExtensionConstStringPtr icon_path;
#endif // GODOT_VERSION_MINOR >= 4
T::set_bind, // GDExtensionClassSet set_func;
T::get_bind, // GDExtensionClassGet get_func;
T::has_get_property_list() ? T::get_property_list_bind : nullptr, // GDExtensionClassGetPropertyList get_property_list_func;
@ -269,13 +286,18 @@ void ClassDB::_register_class(bool p_virtual, bool p_exposed, bool p_runtime) {
&ClassDB::get_virtual_func, // GDExtensionClassGetVirtual get_virtual_func;
nullptr, // GDExtensionClassGetVirtualCallData get_virtual_call_data_func;
nullptr, // GDExtensionClassCallVirtualWithData call_virtual_func;
#if GODOT_VERSION_MINOR <= 3
nullptr, // GDExtensionClassGetRID get_rid;
#endif // GODOT_VERSION_MINOR <= 3
(void *)&T::get_class_static(), // void *class_userdata;
};
#if GODOT_VERSION_MINOR >= 5
::godot::gdextension_interface::classdb_register_extension_class5(::godot::gdextension_interface::library, cl.name._native_ptr(), cl.parent_name._native_ptr(), &class_info);
#else
#elif GODOT_VERSION_MINOR >= 4
::godot::gdextension_interface::classdb_register_extension_class4(::godot::gdextension_interface::library, cl.name._native_ptr(), cl.parent_name._native_ptr(), &class_info);
#else
::godot::gdextension_interface::classdb_register_extension_class3(::godot::gdextension_interface::library, cl.name._native_ptr(), cl.parent_name._native_ptr(), &class_info);
#endif
// call bind_methods etc. to register all members of the class

View File

@ -30,6 +30,10 @@
#pragma once
#include <godot_cpp/core/version.hpp>
#if GODOT_VERSION_MINOR >= 4
#include <godot_cpp/core/type_info.hpp>
#include <godot_cpp/templates/pair.hpp>
#include <godot_cpp/variant/dictionary.hpp>
@ -463,3 +467,7 @@ MAKE_TYPED_DICTIONARY_INFO(IPAddress, Variant::STRING)
#undef MAKE_TYPED_DICTIONARY_INFO_WITH_OBJECT
} // namespace godot
#else
#error "TypedDictionary is only supported when targeting Godot 4.4+"
#endif // GODOT_VERSION_MINOR >= 4

View File

@ -411,10 +411,11 @@ Array::ConstIterator Array::end() const {
Array::Array(std::initializer_list<Variant> p_init) :
Array() {
ERR_FAIL_COND(resize(p_init.size()) != 0);
Variant *variant_ptr = ptrw();
size_t i = 0;
for (const Variant &element : p_init) {
set(i++, element);
variant_ptr[i++] = element;
}
}

View File

@ -30,6 +30,10 @@
#pragma once
#include <godot_cpp/core/version.hpp>
#if GODOT_VERSION_MINOR >= 4
#include <gdextension_interface.h>
#include <godot_cpp/variant/variant.hpp>
@ -504,3 +508,5 @@ struct VariantDefaultInitializer {
};
} // namespace godot
#endif // GODOT_VERSION_MINOR >= 4

View File

@ -60,10 +60,17 @@ void Wrapped::_postinitialize() {
Wrapped::_constructing_mutex.unlock();
#endif
#if GODOT_VERSION_MINOR >= 4
Object *obj = dynamic_cast<Object *>(this);
if (obj) {
obj->notification(Object::NOTIFICATION_POSTINITIALIZE);
}
#else
// Only send NOTIFICATION_POSTINITIALIZE for extension classes.
if (_is_extension_class()) {
_notificationv(Object::NOTIFICATION_POSTINITIALIZE);
}
#endif // GODOT_VERSION_MINOR >= 4
}
Wrapped::Wrapped(const StringName &p_godot_class) {
@ -74,7 +81,11 @@ Wrapped::Wrapped(const StringName &p_godot_class) {
} else
#endif
{
#if GODOT_VERSION_MINOR >= 4
_owner = ::godot::gdextension_interface::classdb_construct_object2(reinterpret_cast<GDExtensionConstStringNamePtr>(p_godot_class._native_ptr()));
#else
_owner = ::godot::gdextension_interface::classdb_construct_object(reinterpret_cast<GDExtensionConstStringNamePtr>(p_godot_class._native_ptr()));
#endif
}
if (_constructing_extension_class_name) {

View File

@ -283,7 +283,12 @@ void ClassDB::bind_integer_constant(const StringName &p_class_name, const String
// Register it with Godot
::godot::gdextension_interface::classdb_register_extension_class_integer_constant(::godot::gdextension_interface::library, p_class_name._native_ptr(), p_enum_name._native_ptr(), p_constant_name._native_ptr(), p_constant_value, p_is_bitfield);
}
#if GODOT_VERSION_MINOR >= 4
GDExtensionClassCallVirtual ClassDB::get_virtual_func(void *p_userdata, GDExtensionConstStringNamePtr p_name, uint32_t p_hash) {
#else
GDExtensionClassCallVirtual ClassDB::get_virtual_func(void *p_userdata, GDExtensionConstStringNamePtr p_name) {
#endif // GODOT_VERSION_MINOR >= 4
// This is called by Godot the first time it calls a virtual function, and it caches the result, per object instance.
// Because of this, it can happen from different threads at once.
// It should be ok not using any mutex as long as we only READ data.
@ -299,7 +304,11 @@ GDExtensionClassCallVirtual ClassDB::get_virtual_func(void *p_userdata, GDExtens
while (type != nullptr) {
AHashMap<StringName, ClassInfo::VirtualMethod>::ConstIterator method_it = type->virtual_methods.find(*name);
#if GODOT_VERSION_MINOR >= 4
if (method_it != type->virtual_methods.end() && method_it->value.hash == p_hash) {
#else
if (method_it != type->virtual_methods.end()) {
#endif // GODOT_VERSION_MINOR >= 4
return method_it->value.func;
}

View File

@ -250,10 +250,12 @@ Variant &Dictionary::operator[](const Variant &p_key) {
return *var;
}
#if GODOT_VERSION_MINOR >= 4
void Dictionary::set_typed(uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script) {
// p_key_type/p_value_type are not Variant::Type so that header doesn't depend on <variant.hpp>.
::godot::gdextension_interface::dictionary_set_typed((GDExtensionTypePtr *)this, (GDExtensionVariantType)p_key_type, (GDExtensionConstStringNamePtr)&p_key_class_name, (GDExtensionConstVariantPtr)&p_key_script,
(GDExtensionVariantType)p_value_type, (GDExtensionConstStringNamePtr)&p_value_class_name, (GDExtensionConstVariantPtr)&p_value_script);
}
#endif // GODOT_VERSION_MINOR >= 4
} // namespace godot

View File

@ -50,7 +50,10 @@ void Variant::init_bindings() {
from_type_constructor[i] = ::godot::gdextension_interface::get_variant_from_type_constructor((GDExtensionVariantType)i);
to_type_constructor[i] = ::godot::gdextension_interface::get_variant_to_type_constructor((GDExtensionVariantType)i);
}
#if GODOT_VERSION_MINOR >= 4
VariantInternal::init_bindings();
#endif // GODOT_VERSION_MINOR >= 4
StringName::init_bindings();
String::init_bindings();
@ -450,7 +453,18 @@ Variant::operator ObjectID() const {
if (get_type() == Type::INT) {
return ObjectID(operator uint64_t());
} else if (get_type() == Type::OBJECT) {
#if GODOT_VERSION_MINOR >= 4
return ObjectID(::godot::gdextension_interface::variant_get_object_instance_id(_native_ptr()));
#else
// Note: This isn't safe, because the object may have already been freed, but it's the best we
// can do in Godot 4.3 and earlier.
Object *obj = operator Object *();
if (obj != nullptr) {
return ObjectID(obj->get_instance_id());
} else {
return ObjectID();
}
#endif // GODOT_VERSION_MINOR >= 4
} else {
return ObjectID();
}
@ -513,7 +527,12 @@ Variant::operator PackedVector4Array() const {
}
Object *Variant::get_validated_object() const {
#if GODOT_VERSION_MINOR >= 4
return ObjectDB::get_instance(operator ObjectID());
#else
// Note: This isn't actually validated, but we can't do any better in Godot 4.3 or earlier.
return operator Object *();
#endif // GODOT_VERSION_MINOR >= 4
}
Variant &Variant::operator=(const Variant &other) {

View File

@ -30,6 +30,8 @@
#include <godot_cpp/variant/variant_internal.hpp>
#if GODOT_VERSION_MINOR >= 4
namespace godot {
GDExtensionVariantGetInternalPtrFunc VariantInternal::get_internal_func[Variant::VARIANT_MAX]{};
@ -41,3 +43,5 @@ void VariantInternal::init_bindings() {
}
} // namespace godot
#endif // GODOT_VERSION_MINOR >= 4

View File

@ -9,6 +9,7 @@ class TestClass:
func _ready():
var example: Example = $Example
var godot_target_version := example.get_godot_target_version()
var godot_runtime_version := Engine.get_version_info()
# Timing of set instance binding.
assert_equal(example.is_object_binding_set_by_parent_constructor(), true)
@ -85,9 +86,10 @@ func _ready():
var array: Array[int] = [1, 2, 3]
assert_equal(example.test_tarray_arg(array), 6)
assert_equal(example.test_dictionary(), { "hello": "world", "foo": "bar" })
assert_equal(example.test_tdictionary(), { Vector2(1, 2): Vector2i(2, 3) })
var dictionary: Dictionary[String, int] = { "1": 1, "2": 2, "3": 3 }
assert_equal(example.test_tdictionary_arg(dictionary), 6)
if godot_target_version["minor"] >= 4:
var test = load("res://test_typed_dictionary.gd").new()
test.test_typed_dictionary(self, example)
example.callable_bind()
assert_equal(custom_signal_emitted, ["bound", 11])
@ -209,11 +211,12 @@ func _ready():
assert_equal(example.test_variant_float_conversion(10.0), 10.0)
assert_equal(example.test_variant_float_conversion(10), 10.0)
# Test checking if objects are valid.
var object_of_questionable_validity = Object.new()
assert_equal(example.test_object_is_valid(object_of_questionable_validity), true)
object_of_questionable_validity.free()
assert_equal(example.test_object_is_valid(object_of_questionable_validity), false)
if godot_target_version["minor"] >= 4:
# Test checking if objects are valid.
var object_of_questionable_validity = Object.new()
assert_equal(example.test_object_is_valid(object_of_questionable_validity), true)
object_of_questionable_validity.free()
assert_equal(example.test_object_is_valid(object_of_questionable_validity), false)
# Test that ptrcalls from GDExtension to the engine are correctly encoding Object and RefCounted.
var new_node = Node.new()
@ -274,8 +277,9 @@ func _ready():
# Test that we can access an engine singleton.
assert_equal(example.test_use_engine_singleton(), OS.get_name())
assert_equal(example.test_get_internal(1), 1)
assert_equal(example.test_get_internal(true), -1)
if godot_target_version["minor"] >= 4:
assert_equal(example.test_get_internal(1), 1)
assert_equal(example.test_get_internal(true), -1)
# Test that notifications happen on both parent and child classes.
var example_child = $ExampleChild
@ -299,9 +303,10 @@ func _ready():
assert_equal(internal_class.get_the_answer(), 42)
assert_equal(internal_class.get_class(), "ExampleInternal")
# Test a class with a unicode name.
var przykład = ExamplePrzykład.new()
assert_equal(przykład.get_the_word(), "słowo to przykład")
if godot_runtime_version["minor"] >= 4:
# Test a class with a unicode name.
var przykład = ClassDB.instantiate("ExamplePrzykład")
assert_equal(przykład.get_the_word(), "słowo to przykład")
exit_with_status()

View File

@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://dmx2xuigcpvt4"]
[ext_resource type="Script" uid="uid://bujp6xsb8pfqk" path="res://main.gd" id="1_qesh5"]
[ext_resource type="Script" uid="uid://1htvqeulgew6" path="res://example.gd" id="2_jju25"]
[ext_resource type="Script" path="res://main.gd" id="1_qesh5"]
[ext_resource type="Script" path="res://example.gd" id="2_jju25"]
[node name="Node" type="Node"]
script = ExtResource("1_qesh5")

View File

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

View File

@ -0,0 +1,6 @@
extends RefCounted
func test_typed_dictionary(test, example: Example):
test.assert_equal(example.test_tdictionary(), { Vector2(1, 2): Vector2i(2, 3) })
var dictionary: Dictionary[String, int] = { "1": 1, "2": 2, "3": 3 }
test.assert_equal(example.test_tdictionary_arg(dictionary), 6)

View File

@ -12,7 +12,6 @@
#include <godot_cpp/classes/multiplayer_api.hpp>
#include <godot_cpp/classes/multiplayer_peer.hpp>
#include <godot_cpp/classes/os.hpp>
#include <godot_cpp/variant/typed_dictionary.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
@ -209,8 +208,12 @@ void Example::_bind_methods() {
ClassDB::bind_method(D_METHOD("test_tarray_arg", "array"), &Example::test_tarray_arg);
ClassDB::bind_method(D_METHOD("test_tarray"), &Example::test_tarray);
ClassDB::bind_method(D_METHOD("test_dictionary"), &Example::test_dictionary);
#if GODOT_VERSION_MINOR >= 4
ClassDB::bind_method(D_METHOD("test_tdictionary_arg", "dictionary"), &Example::test_tdictionary_arg);
ClassDB::bind_method(D_METHOD("test_tdictionary"), &Example::test_tdictionary);
#endif // GODOT_VERSION_MINOR >= 4
ClassDB::bind_method(D_METHOD("test_node_argument"), &Example::test_node_argument);
ClassDB::bind_method(D_METHOD("test_string_ops"), &Example::test_string_ops);
ClassDB::bind_method(D_METHOD("test_str_utility"), &Example::test_str_utility);
@ -254,7 +257,9 @@ void Example::_bind_methods() {
ClassDB::bind_method(D_METHOD("callable_bind"), &Example::callable_bind);
ClassDB::bind_method(D_METHOD("test_post_initialize"), &Example::test_post_initialize);
#if GODOT_VERSION_MINOR >= 4
ClassDB::bind_method(D_METHOD("test_get_internal", "a"), &Example::test_get_internal);
#endif // GODOT_VERSION_MINOR >= 4
GDVIRTUAL_BIND(_do_something_virtual, "name", "value");
ClassDB::bind_method(D_METHOD("test_virtual_implemented_in_script"), &Example::test_virtual_implemented_in_script);
@ -569,6 +574,7 @@ Dictionary Example::test_dictionary() const {
return dict;
}
#if GODOT_VERSION_MINOR >= 4
int Example::test_tdictionary_arg(const TypedDictionary<String, int64_t> &p_dictionary) {
int sum = 0;
TypedArray<int64_t> values = p_dictionary.values();
@ -585,6 +591,7 @@ TypedDictionary<Vector2, Vector2i> Example::test_tdictionary() const {
return dict;
}
#endif // GODOT_VERSION_MINOR >= 4
Example *Example::test_node_argument(Example *p_node) const {
return p_node;
@ -767,6 +774,7 @@ Ref<RefCounted> Example::test_get_internal_class() const {
return it;
}
#if GODOT_VERSION_MINOR >= 4
int64_t Example::test_get_internal(const Variant &p_input) const {
if (p_input.get_type() != Variant::INT) {
return -1;
@ -774,6 +782,7 @@ int64_t Example::test_get_internal(const Variant &p_input) const {
return *VariantInternal::get_int(&p_input);
}
#endif // GODOT_VERSION_MINOR >= 4
void ExampleRuntime::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_prop_value", "value"), &ExampleRuntime::set_prop_value);

View File

@ -21,10 +21,13 @@
#include <godot_cpp/classes/tile_set.hpp>
#include <godot_cpp/classes/tween.hpp>
#include <godot_cpp/classes/viewport.hpp>
#include <godot_cpp/variant/typed_dictionary.hpp>
#include <godot_cpp/variant/variant.hpp>
#include <godot_cpp/variant/variant_internal.hpp>
#if GODOT_VERSION_MINOR >= 4
#include <godot_cpp/variant/typed_dictionary.hpp>
#endif // GODOT_VERSION_MINOR >= 4
#include <godot_cpp/core/binder_common.hpp>
#include <godot_cpp/core/gdvirtual.gen.inc>
@ -135,8 +138,12 @@ public:
int test_tarray_arg(const TypedArray<int64_t> &p_array);
TypedArray<Vector2> test_tarray() const;
Dictionary test_dictionary() const;
#if GODOT_VERSION_MINOR >= 4
int test_tdictionary_arg(const TypedDictionary<String, int64_t> &p_dictionary);
TypedDictionary<Vector2, Vector2i> test_tdictionary() const;
#endif // GODOT_VERSION_MINOR >= 4
Example *test_node_argument(Example *p_node) const;
String test_string_ops() const;
String test_str_utility() const;

View File

@ -176,7 +176,7 @@ def scons_generate_bindings(target, source, env):
return None
supported_api_versions = ["4.4", "4.5", "4.6"]
supported_api_versions = ["4.3", "4.4", "4.5", "4.6"]
platforms = ["linux", "macos", "windows", "android", "ios", "web"]