diff options
152 files changed, 3773 insertions, 2997 deletions
diff --git a/SConstruct b/SConstruct index 2c5b34854e..7009cb8fbe 100644 --- a/SConstruct +++ b/SConstruct @@ -182,8 +182,8 @@ opts.Add(BoolVariable("deprecated", "Enable compatibility code for deprecated an opts.Add(EnumVariable("float", "Floating-point precision", "32", ("32", "64"))) opts.Add(BoolVariable("minizip", "Enable ZIP archive support using minizip", True)) opts.Add(BoolVariable("xaudio2", "Enable the XAudio2 audio driver", False)) -opts.Add(BoolVariable("vulkan", "Enable the vulkan video driver", True)) -opts.Add(BoolVariable("opengl3", "Enable the OpenGL/GLES3 video driver", True)) +opts.Add(BoolVariable("vulkan", "Enable the vulkan rendering driver", True)) +opts.Add(BoolVariable("opengl3", "Enable the OpenGL/GLES3 rendering driver", True)) opts.Add(BoolVariable("openxr", "Enable the OpenXR driver", True)) opts.Add(BoolVariable("use_volk", "Use the volk library to load the Vulkan loader dynamically", True)) opts.Add("custom_modules", "A list of comma-separated directory paths containing custom modules to build.", "") diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 7496ba1979..87b36f7a21 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -334,6 +334,10 @@ String OS::get_version() const { return ::OS::get_singleton()->get_version(); } +Vector<String> OS::get_video_adapter_driver_info() const { + return ::OS::get_singleton()->get_video_adapter_driver_info(); +} + Vector<String> OS::get_cmdline_args() { List<String> cmdline = ::OS::get_singleton()->get_cmdline_args(); Vector<String> cmdlinev; @@ -549,6 +553,8 @@ void OS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_cmdline_args"), &OS::get_cmdline_args); ClassDB::bind_method(D_METHOD("get_cmdline_user_args"), &OS::get_cmdline_user_args); + ClassDB::bind_method(D_METHOD("get_video_adapter_driver_info"), &OS::get_video_adapter_driver_info); + ClassDB::bind_method(D_METHOD("set_restart_on_exit", "restart", "arguments"), &OS::set_restart_on_exit, DEFVAL(Vector<String>())); ClassDB::bind_method(D_METHOD("is_restart_on_exit_set"), &OS::is_restart_on_exit_set); ClassDB::bind_method(D_METHOD("get_restart_on_exit_arguments"), &OS::get_restart_on_exit_arguments); @@ -599,8 +605,8 @@ void OS::_bind_methods() { ADD_PROPERTY_DEFAULT("low_processor_usage_mode", false); ADD_PROPERTY_DEFAULT("low_processor_usage_mode_sleep_usec", 6900); - BIND_ENUM_CONSTANT(VIDEO_DRIVER_VULKAN); - BIND_ENUM_CONSTANT(VIDEO_DRIVER_OPENGL_3); + BIND_ENUM_CONSTANT(RENDERING_DRIVER_VULKAN); + BIND_ENUM_CONSTANT(RENDERING_DRIVER_OPENGL3); BIND_ENUM_CONSTANT(DAY_SUNDAY); BIND_ENUM_CONSTANT(DAY_MONDAY); diff --git a/core/core_bind.h b/core/core_bind.h index 9261698076..784f3e63b1 100644 --- a/core/core_bind.h +++ b/core/core_bind.h @@ -124,9 +124,9 @@ protected: static OS *singleton; public: - enum VideoDriver { - VIDEO_DRIVER_VULKAN, - VIDEO_DRIVER_OPENGL_3, + enum RenderingDriver { + RENDERING_DRIVER_VULKAN, + RENDERING_DRIVER_OPENGL3, }; enum Weekday { @@ -196,6 +196,8 @@ public: Vector<String> get_cmdline_args(); Vector<String> get_cmdline_user_args(); + Vector<String> get_video_adapter_driver_info() const; + String get_locale() const; String get_locale_language() const; @@ -576,7 +578,7 @@ VARIANT_ENUM_CAST(core_bind::ResourceLoader::CacheMode); VARIANT_BITFIELD_CAST(core_bind::ResourceSaver::SaverFlags); -VARIANT_ENUM_CAST(core_bind::OS::VideoDriver); +VARIANT_ENUM_CAST(core_bind::OS::RenderingDriver); VARIANT_ENUM_CAST(core_bind::OS::Weekday); VARIANT_ENUM_CAST(core_bind::OS::Month); VARIANT_ENUM_CAST(core_bind::OS::SystemDir); diff --git a/core/doc_data.h b/core/doc_data.h index bb356f027e..d5fbe37c96 100644 --- a/core/doc_data.h +++ b/core/doc_data.h @@ -57,6 +57,27 @@ public: } return name < p_arg.name; } + static ArgumentDoc from_dict(const Dictionary &p_dict) { + ArgumentDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("type")) { + doc.type = p_dict["type"]; + } + + if (p_dict.has("enumeration")) { + doc.enumeration = p_dict["enumeration"]; + } + + if (p_dict.has("default_value")) { + doc.default_value = p_dict["default_value"]; + } + + return doc; + } }; struct MethodDoc { @@ -97,6 +118,55 @@ public: } return name < p_method.name; } + static MethodDoc from_dict(const Dictionary &p_dict) { + MethodDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("return_type")) { + doc.return_type = p_dict["return_type"]; + } + + if (p_dict.has("return_enum")) { + doc.return_enum = p_dict["return_enum"]; + } + + if (p_dict.has("qualifiers")) { + doc.qualifiers = p_dict["qualifiers"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + if (p_dict.has("is_deprecated")) { + doc.is_deprecated = p_dict["is_deprecated"]; + } + + if (p_dict.has("is_experimental")) { + doc.is_experimental = p_dict["is_experimental"]; + } + + Array arguments; + if (p_dict.has("arguments")) { + arguments = p_dict["arguments"]; + } + for (int i = 0; i < arguments.size(); i++) { + doc.arguments.push_back(ArgumentDoc::from_dict(arguments[i])); + } + + Array errors_returned; + if (p_dict.has("errors_returned")) { + errors_returned = p_dict["errors_returned"]; + } + for (int i = 0; i < errors_returned.size(); i++) { + doc.errors_returned.push_back(errors_returned[i]); + } + + return doc; + } }; struct ConstantDoc { @@ -111,6 +181,43 @@ public: bool operator<(const ConstantDoc &p_const) const { return name < p_const.name; } + static ConstantDoc from_dict(const Dictionary &p_dict) { + ConstantDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("value")) { + doc.value = p_dict["value"]; + } + + if (p_dict.has("is_value_valid")) { + doc.is_value_valid = p_dict["is_value_valid"]; + } + + if (p_dict.has("enumeration")) { + doc.enumeration = p_dict["enumeration"]; + } + + if (p_dict.has("is_bitfield")) { + doc.is_bitfield = p_dict["is_bitfield"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + if (p_dict.has("is_deprecated")) { + doc.is_deprecated = p_dict["is_deprecated"]; + } + + if (p_dict.has("is_experimental")) { + doc.is_experimental = p_dict["is_experimental"]; + } + + return doc; + } }; struct EnumDoc { @@ -118,6 +225,31 @@ public: bool is_bitfield = false; String description; Vector<DocData::ConstantDoc> values; + static EnumDoc from_dict(const Dictionary &p_dict) { + EnumDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("is_bitfield")) { + doc.is_bitfield = p_dict["is_bitfield"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + Array values; + if (p_dict.has("values")) { + values = p_dict["values"]; + } + for (int i = 0; i < values.size(); i++) { + doc.values.push_back(ConstantDoc::from_dict(values[i])); + } + + return doc; + } }; struct PropertyDoc { @@ -134,6 +266,55 @@ public: bool operator<(const PropertyDoc &p_prop) const { return name < p_prop.name; } + static PropertyDoc from_dict(const Dictionary &p_dict) { + PropertyDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("type")) { + doc.type = p_dict["type"]; + } + + if (p_dict.has("enumeration")) { + doc.enumeration = p_dict["enumeration"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + if (p_dict.has("setter")) { + doc.setter = p_dict["setter"]; + } + + if (p_dict.has("getter")) { + doc.getter = p_dict["getter"]; + } + + if (p_dict.has("default_value")) { + doc.default_value = p_dict["default_value"]; + } + + if (p_dict.has("overridden")) { + doc.overridden = p_dict["overridden"]; + } + + if (p_dict.has("overrides")) { + doc.overrides = p_dict["overrides"]; + } + + if (p_dict.has("is_deprecated")) { + doc.is_deprecated = p_dict["is_deprecated"]; + } + + if (p_dict.has("is_experimental")) { + doc.is_experimental = p_dict["is_experimental"]; + } + + return doc; + } }; struct ThemeItemDoc { @@ -149,11 +330,49 @@ public: } return data_type < p_theme_item.data_type; } + static ThemeItemDoc from_dict(const Dictionary &p_dict) { + ThemeItemDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("type")) { + doc.type = p_dict["type"]; + } + + if (p_dict.has("data_type")) { + doc.data_type = p_dict["data_type"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + if (p_dict.has("default_value")) { + doc.default_value = p_dict["default_value"]; + } + + return doc; + } }; struct TutorialDoc { String link; String title; + static TutorialDoc from_dict(const Dictionary &p_dict) { + TutorialDoc doc; + + if (p_dict.has("link")) { + doc.link = p_dict["link"]; + } + + if (p_dict.has("title")) { + doc.title = p_dict["title"]; + } + + return doc; + } }; struct ClassDoc { @@ -179,6 +398,127 @@ public: bool operator<(const ClassDoc &p_class) const { return name < p_class.name; } + static ClassDoc from_dict(const Dictionary &p_dict) { + ClassDoc doc; + + if (p_dict.has("name")) { + doc.name = p_dict["name"]; + } + + if (p_dict.has("inherits")) { + doc.inherits = p_dict["inherits"]; + } + + if (p_dict.has("category")) { + doc.category = p_dict["category"]; + } + + if (p_dict.has("brief_description")) { + doc.brief_description = p_dict["brief_description"]; + } + + if (p_dict.has("description")) { + doc.description = p_dict["description"]; + } + + Array tutorials; + if (p_dict.has("tutorials")) { + tutorials = p_dict["tutorials"]; + } + for (int i = 0; i < tutorials.size(); i++) { + doc.tutorials.push_back(TutorialDoc::from_dict(tutorials[i])); + } + + Array constructors; + if (p_dict.has("constructors")) { + constructors = p_dict["constructors"]; + } + for (int i = 0; i < constructors.size(); i++) { + doc.constructors.push_back(MethodDoc::from_dict(constructors[i])); + } + + Array methods; + if (p_dict.has("methods")) { + methods = p_dict["methods"]; + } + for (int i = 0; i < methods.size(); i++) { + doc.methods.push_back(MethodDoc::from_dict(methods[i])); + } + + Array operators; + if (p_dict.has("operators")) { + operators = p_dict["operators"]; + } + for (int i = 0; i < operators.size(); i++) { + doc.operators.push_back(MethodDoc::from_dict(operators[i])); + } + + Array signals; + if (p_dict.has("signals")) { + signals = p_dict["signals"]; + } + for (int i = 0; i < signals.size(); i++) { + doc.signals.push_back(MethodDoc::from_dict(signals[i])); + } + + Array constants; + if (p_dict.has("constants")) { + constants = p_dict["constants"]; + } + for (int i = 0; i < constants.size(); i++) { + doc.constants.push_back(ConstantDoc::from_dict(constants[i])); + } + + Dictionary enums; + if (p_dict.has("enums")) { + enums = p_dict["enums"]; + } + for (int i = 0; i < enums.size(); i++) { + doc.enums[enums.get_key_at_index(i)] = enums.get_value_at_index(i); + } + + Array properties; + if (p_dict.has("properties")) { + properties = p_dict["properties"]; + } + for (int i = 0; i < properties.size(); i++) { + doc.properties.push_back(PropertyDoc::from_dict(properties[i])); + } + + Array annotations; + if (p_dict.has("annotations")) { + annotations = p_dict["annotations"]; + } + for (int i = 0; i < annotations.size(); i++) { + doc.annotations.push_back(MethodDoc::from_dict(annotations[i])); + } + + Array theme_properties; + if (p_dict.has("theme_properties")) { + theme_properties = p_dict["theme_properties"]; + } + for (int i = 0; i < theme_properties.size(); i++) { + doc.theme_properties.push_back(ThemeItemDoc::from_dict(theme_properties[i])); + } + + if (p_dict.has("is_deprecated")) { + doc.is_deprecated = p_dict["is_deprecated"]; + } + + if (p_dict.has("is_experimental")) { + doc.is_experimental = p_dict["is_experimental"]; + } + + if (p_dict.has("is_script_doc")) { + doc.is_script_doc = p_dict["is_script_doc"]; + } + + if (p_dict.has("script_path")) { + doc.script_path = p_dict["script_path"]; + } + + return doc; + } }; static void return_doc_from_retinfo(DocData::MethodDoc &p_method, const PropertyInfo &p_retinfo); diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index a018221b7f..2fb357b520 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -49,6 +49,11 @@ Ref<ResourceFormatLoader> ResourceLoader::loader[ResourceLoader::MAX_LOADERS]; int ResourceLoader::loader_count = 0; bool ResourceFormatLoader::recognize_path(const String &p_path, const String &p_for_type) const { + bool ret = false; + if (GDVIRTUAL_CALL(_recognize_path, p_path, p_for_type, ret)) { + return ret; + } + String extension = p_path.get_extension(); List<String> extensions; @@ -189,6 +194,7 @@ void ResourceFormatLoader::_bind_methods() { BIND_ENUM_CONSTANT(CACHE_MODE_REPLACE); GDVIRTUAL_BIND(_get_recognized_extensions); + GDVIRTUAL_BIND(_recognize_path, "path", "type"); GDVIRTUAL_BIND(_handles_type, "type"); GDVIRTUAL_BIND(_get_resource_type, "path"); GDVIRTUAL_BIND(_get_resource_uid, "path"); diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index 91ba930176..243670b2d0 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -51,6 +51,7 @@ protected: static void _bind_methods(); GDVIRTUAL0RC(Vector<String>, _get_recognized_extensions) + GDVIRTUAL2RC(bool, _recognize_path, String, StringName) GDVIRTUAL1RC(bool, _handles_type, StringName) GDVIRTUAL1RC(String, _get_resource_type, String) GDVIRTUAL1RC(ResourceUID::ID, _get_resource_uid, String) diff --git a/core/math/aabb.cpp b/core/math/aabb.cpp index 026f179445..fcf245d2ad 100644 --- a/core/math/aabb.cpp +++ b/core/math/aabb.cpp @@ -76,6 +76,10 @@ bool AABB::is_equal_approx(const AABB &p_aabb) const { return position.is_equal_approx(p_aabb.position) && size.is_equal_approx(p_aabb.size); } +bool AABB::is_finite() const { + return position.is_finite() && size.is_finite(); +} + AABB AABB::intersection(const AABB &p_aabb) const { #ifdef MATH_CHECKS if (unlikely(size.x < 0 || size.y < 0 || size.z < 0 || p_aabb.size.x < 0 || p_aabb.size.y < 0 || p_aabb.size.z < 0)) { diff --git a/core/math/aabb.h b/core/math/aabb.h index b9f777c6cf..9d5837ad37 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -63,6 +63,7 @@ struct _NO_DISCARD_ AABB { bool operator!=(const AABB &p_rval) const; bool is_equal_approx(const AABB &p_aabb) const; + bool is_finite() const; _FORCE_INLINE_ bool intersects(const AABB &p_aabb) const; /// Both AABBs overlap _FORCE_INLINE_ bool intersects_inclusive(const AABB &p_aabb) const; /// Both AABBs (or their faces) overlap _FORCE_INLINE_ bool encloses(const AABB &p_aabb) const; /// p_aabb is completely inside this diff --git a/core/math/basis.cpp b/core/math/basis.cpp index 845686f339..9b8188eed8 100644 --- a/core/math/basis.cpp +++ b/core/math/basis.cpp @@ -691,6 +691,10 @@ bool Basis::is_equal_approx(const Basis &p_basis) const { return rows[0].is_equal_approx(p_basis.rows[0]) && rows[1].is_equal_approx(p_basis.rows[1]) && rows[2].is_equal_approx(p_basis.rows[2]); } +bool Basis::is_finite() const { + return rows[0].is_finite() && rows[1].is_finite() && rows[2].is_finite(); +} + bool Basis::operator==(const Basis &p_matrix) const { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { diff --git a/core/math/basis.h b/core/math/basis.h index cc2924f5ff..69bef5a7be 100644 --- a/core/math/basis.h +++ b/core/math/basis.h @@ -134,6 +134,7 @@ struct _NO_DISCARD_ Basis { } bool is_equal_approx(const Basis &p_basis) const; + bool is_finite() const; bool operator==(const Basis &p_matrix) const; bool operator!=(const Basis &p_matrix) const; diff --git a/core/math/math_funcs.h b/core/math/math_funcs.h index 7fa674a23d..0af529ad98 100644 --- a/core/math/math_funcs.h +++ b/core/math/math_funcs.h @@ -184,6 +184,9 @@ public: #endif } + static _ALWAYS_INLINE_ bool is_finite(double p_val) { return isfinite(p_val); } + static _ALWAYS_INLINE_ bool is_finite(float p_val) { return isfinite(p_val); } + static _ALWAYS_INLINE_ double abs(double g) { return absd(g); } static _ALWAYS_INLINE_ float abs(float g) { return absf(g); } static _ALWAYS_INLINE_ int abs(int g) { return g > 0 ? g : -g; } diff --git a/core/math/plane.cpp b/core/math/plane.cpp index 3b2eab4ae2..a5d2fe5628 100644 --- a/core/math/plane.cpp +++ b/core/math/plane.cpp @@ -176,6 +176,10 @@ bool Plane::is_equal_approx(const Plane &p_plane) const { return normal.is_equal_approx(p_plane.normal) && Math::is_equal_approx(d, p_plane.d); } +bool Plane::is_finite() const { + return normal.is_finite() && Math::is_finite(d); +} + Plane::operator String() const { return "[N: " + normal.operator String() + ", D: " + String::num_real(d, false) + "]"; } diff --git a/core/math/plane.h b/core/math/plane.h index 73babfa496..77da59fb27 100644 --- a/core/math/plane.h +++ b/core/math/plane.h @@ -74,6 +74,7 @@ struct _NO_DISCARD_ Plane { Plane operator-() const { return Plane(-normal, -d); } bool is_equal_approx(const Plane &p_plane) const; bool is_equal_approx_any_side(const Plane &p_plane) const; + bool is_finite() const; _FORCE_INLINE_ bool operator==(const Plane &p_plane) const; _FORCE_INLINE_ bool operator!=(const Plane &p_plane) const; diff --git a/core/math/quaternion.cpp b/core/math/quaternion.cpp index 4a8d29e402..6a5f29f3d8 100644 --- a/core/math/quaternion.cpp +++ b/core/math/quaternion.cpp @@ -79,6 +79,10 @@ bool Quaternion::is_equal_approx(const Quaternion &p_quaternion) const { return Math::is_equal_approx(x, p_quaternion.x) && Math::is_equal_approx(y, p_quaternion.y) && Math::is_equal_approx(z, p_quaternion.z) && Math::is_equal_approx(w, p_quaternion.w); } +bool Quaternion::is_finite() const { + return Math::is_finite(x) && Math::is_finite(y) && Math::is_finite(z) && Math::is_finite(w); +} + real_t Quaternion::length() const { return Math::sqrt(length_squared()); } diff --git a/core/math/quaternion.h b/core/math/quaternion.h index 178cfaca70..7aa400aa8c 100644 --- a/core/math/quaternion.h +++ b/core/math/quaternion.h @@ -55,6 +55,7 @@ struct _NO_DISCARD_ Quaternion { } _FORCE_INLINE_ real_t length_squared() const; bool is_equal_approx(const Quaternion &p_quaternion) const; + bool is_finite() const; real_t length() const; void normalize(); Quaternion normalized() const; diff --git a/core/math/rect2.cpp b/core/math/rect2.cpp index 9e78ead816..facf4eb3c4 100644 --- a/core/math/rect2.cpp +++ b/core/math/rect2.cpp @@ -38,6 +38,10 @@ bool Rect2::is_equal_approx(const Rect2 &p_rect) const { return position.is_equal_approx(p_rect.position) && size.is_equal_approx(p_rect.size); } +bool Rect2::is_finite() const { + return position.is_finite() && size.is_finite(); +} + bool Rect2::intersects_segment(const Point2 &p_from, const Point2 &p_to, Point2 *r_pos, Point2 *r_normal) const { #ifdef MATH_CHECKS if (unlikely(size.x < 0 || size.y < 0)) { diff --git a/core/math/rect2.h b/core/math/rect2.h index 50dd2dc1df..9863405d8e 100644 --- a/core/math/rect2.h +++ b/core/math/rect2.h @@ -207,6 +207,7 @@ struct _NO_DISCARD_ Rect2 { } bool is_equal_approx(const Rect2 &p_rect) const; + bool is_finite() const; bool operator==(const Rect2 &p_rect) const { return position == p_rect.position && size == p_rect.size; } bool operator!=(const Rect2 &p_rect) const { return position != p_rect.position || size != p_rect.size; } diff --git a/core/math/transform_2d.cpp b/core/math/transform_2d.cpp index 2bfefe979f..548a82d254 100644 --- a/core/math/transform_2d.cpp +++ b/core/math/transform_2d.cpp @@ -168,6 +168,10 @@ bool Transform2D::is_equal_approx(const Transform2D &p_transform) const { return columns[0].is_equal_approx(p_transform.columns[0]) && columns[1].is_equal_approx(p_transform.columns[1]) && columns[2].is_equal_approx(p_transform.columns[2]); } +bool Transform2D::is_finite() const { + return columns[0].is_finite() && columns[1].is_finite() && columns[2].is_finite(); +} + Transform2D Transform2D::looking_at(const Vector2 &p_target) const { Transform2D return_trans = Transform2D(get_rotation(), get_origin()); Vector2 target_position = affine_inverse().xform(p_target); diff --git a/core/math/transform_2d.h b/core/math/transform_2d.h index f23f32867a..2b11f36535 100644 --- a/core/math/transform_2d.h +++ b/core/math/transform_2d.h @@ -98,6 +98,7 @@ struct _NO_DISCARD_ Transform2D { void orthonormalize(); Transform2D orthonormalized() const; bool is_equal_approx(const Transform2D &p_transform) const; + bool is_finite() const; Transform2D looking_at(const Vector2 &p_target) const; diff --git a/core/math/transform_3d.cpp b/core/math/transform_3d.cpp index 6741ef4034..3285cbd664 100644 --- a/core/math/transform_3d.cpp +++ b/core/math/transform_3d.cpp @@ -174,6 +174,10 @@ bool Transform3D::is_equal_approx(const Transform3D &p_transform) const { return basis.is_equal_approx(p_transform.basis) && origin.is_equal_approx(p_transform.origin); } +bool Transform3D::is_finite() const { + return basis.is_finite() && origin.is_finite(); +} + bool Transform3D::operator==(const Transform3D &p_transform) const { return (basis == p_transform.basis && origin == p_transform.origin); } diff --git a/core/math/transform_3d.h b/core/math/transform_3d.h index 44d6d826f3..cb347aa1c1 100644 --- a/core/math/transform_3d.h +++ b/core/math/transform_3d.h @@ -75,6 +75,7 @@ struct _NO_DISCARD_ Transform3D { void orthogonalize(); Transform3D orthogonalized() const; bool is_equal_approx(const Transform3D &p_transform) const; + bool is_finite() const; bool operator==(const Transform3D &p_transform) const; bool operator!=(const Transform3D &p_transform) const; diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp index 56dbba393a..5366587126 100644 --- a/core/math/vector2.cpp +++ b/core/math/vector2.cpp @@ -186,6 +186,10 @@ bool Vector2::is_zero_approx() const { return Math::is_zero_approx(x) && Math::is_zero_approx(y); } +bool Vector2::is_finite() const { + return Math::is_finite(x) && Math::is_finite(y); +} + Vector2::operator String() const { return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ")"; } diff --git a/core/math/vector2.h b/core/math/vector2.h index 75364f72f0..5775d8e735 100644 --- a/core/math/vector2.h +++ b/core/math/vector2.h @@ -121,6 +121,7 @@ struct _NO_DISCARD_ Vector2 { bool is_equal_approx(const Vector2 &p_v) const; bool is_zero_approx() const; + bool is_finite() const; Vector2 operator+(const Vector2 &p_v) const; void operator+=(const Vector2 &p_v); diff --git a/core/math/vector3.cpp b/core/math/vector3.cpp index 55ba509144..b106200c4a 100644 --- a/core/math/vector3.cpp +++ b/core/math/vector3.cpp @@ -139,6 +139,10 @@ bool Vector3::is_zero_approx() const { return Math::is_zero_approx(x) && Math::is_zero_approx(y) && Math::is_zero_approx(z); } +bool Vector3::is_finite() const { + return Math::is_finite(x) && Math::is_finite(y) && Math::is_finite(z); +} + Vector3::operator String() const { return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ", " + String::num_real(z, false) + ")"; } diff --git a/core/math/vector3.h b/core/math/vector3.h index 62e810fb4d..19771eb312 100644 --- a/core/math/vector3.h +++ b/core/math/vector3.h @@ -136,6 +136,7 @@ struct _NO_DISCARD_ Vector3 { bool is_equal_approx(const Vector3 &p_v) const; bool is_zero_approx() const; + bool is_finite() const; /* Operators */ diff --git a/core/math/vector4.cpp b/core/math/vector4.cpp index 9fd980aaff..3b189f7ed4 100644 --- a/core/math/vector4.cpp +++ b/core/math/vector4.cpp @@ -64,6 +64,10 @@ bool Vector4::is_zero_approx() const { return Math::is_zero_approx(x) && Math::is_zero_approx(y) && Math::is_zero_approx(z) && Math::is_zero_approx(w); } +bool Vector4::is_finite() const { + return Math::is_finite(x) && Math::is_finite(y) && Math::is_finite(z) && Math::is_finite(w); +} + real_t Vector4::length() const { return Math::sqrt(length_squared()); } diff --git a/core/math/vector4.h b/core/math/vector4.h index ac7b6c3aee..7c4bdc1788 100644 --- a/core/math/vector4.h +++ b/core/math/vector4.h @@ -71,6 +71,7 @@ struct _NO_DISCARD_ Vector4 { _FORCE_INLINE_ real_t length_squared() const; bool is_equal_approx(const Vector4 &p_vec4) const; bool is_zero_approx() const; + bool is_finite() const; real_t length() const; void normalize(); Vector4 normalized() const; diff --git a/core/object/script_language_extension.h b/core/object/script_language_extension.h index 15e2d57a3a..c32fb9d85b 100644 --- a/core/object/script_language_extension.h +++ b/core/object/script_language_extension.h @@ -82,7 +82,10 @@ public: GDVIRTUAL_REQUIRED_CALL(_get_documentation, doc); Vector<DocData::ClassDoc> class_doc; - // TODO: Missing conversion from documentation to ClassDoc. + for (int i = 0; i < doc.size(); i++) { + class_doc.append(DocData::ClassDoc::from_dict(doc[i])); + } + return class_doc; } #endif // TOOLS_ENABLED diff --git a/core/os/os.h b/core/os/os.h index 1a5e45968d..af7b40f3ec 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -123,6 +123,8 @@ public: int get_display_driver_id() const { return _display_driver_id; } + virtual Vector<String> get_video_adapter_driver_info() const = 0; + void print_error(const char *p_function, const char *p_file, int p_line, const char *p_code, const char *p_rationale, bool p_editor_notify = false, Logger::ErrorType p_type = Logger::ERR_ERROR); void print(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_2_3; void print_rich(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_2_3; diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index dbbcedca84..872c8357ae 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -4654,10 +4654,10 @@ String String::sprintf(const Array &values, bool *error) const { double value = values[value_index]; bool is_negative = (value < 0); String str = String::num(ABS(value), min_decimals); - bool not_numeric = isinf(value) || isnan(value); + const bool is_finite = Math::is_finite(value); // Pad decimals out. - if (!not_numeric) { + if (is_finite) { str = str.pad_decimals(min_decimals); } @@ -4665,7 +4665,7 @@ String String::sprintf(const Array &values, bool *error) const { // Padding. Leave room for sign later if required. int pad_chars_count = (is_negative || show_sign) ? min_chars - 1 : min_chars; - String pad_char = (pad_with_zeros && !not_numeric) ? String("0") : String(" "); // Never pad NaN or inf with zeros + String pad_char = (pad_with_zeros && is_finite) ? String("0") : String(" "); // Never pad NaN or inf with zeros if (left_justified) { str = str.rpad(pad_chars_count, pad_char); } else { @@ -4716,10 +4716,10 @@ String String::sprintf(const Array &values, bool *error) const { for (int i = 0; i < count; i++) { double val = vec[i]; String number_str = String::num(ABS(val), min_decimals); - bool not_numeric = isinf(val) || isnan(val); + const bool is_finite = Math::is_finite(val); // Pad decimals out. - if (!not_numeric) { + if (is_finite) { number_str = number_str.pad_decimals(min_decimals); } @@ -4727,7 +4727,7 @@ String String::sprintf(const Array &values, bool *error) const { // Padding. Leave room for sign later if required. int pad_chars_count = val < 0 ? min_chars - 1 : min_chars; - String pad_char = (pad_with_zeros && !not_numeric) ? String("0") : String(" "); // Never pad NaN or inf with zeros + String pad_char = (pad_with_zeros && is_finite) ? String("0") : String(" "); // Never pad NaN or inf with zeros if (left_justified) { number_str = number_str.rpad(pad_chars_count, pad_char); } else { diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 4eae23b0fb..b4528e67d1 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -47,146 +47,126 @@ String Variant::get_type_name(Variant::Type p_type) { switch (p_type) { case NIL: { return "Nil"; - } break; + } - // atomic types + // Atomic types. case BOOL: { return "bool"; - } break; + } case INT: { return "int"; - - } break; + } case FLOAT: { return "float"; - - } break; + } case STRING: { return "String"; - } break; + } - // math types + // Math types. case VECTOR2: { return "Vector2"; - } break; + } case VECTOR2I: { return "Vector2i"; - } break; + } case RECT2: { return "Rect2"; - } break; + } case RECT2I: { return "Rect2i"; - } break; + } case TRANSFORM2D: { return "Transform2D"; - } break; + } case VECTOR3: { return "Vector3"; - } break; + } case VECTOR3I: { return "Vector3i"; - } break; + } case VECTOR4: { return "Vector4"; - } break; + } case VECTOR4I: { return "Vector4i"; - } break; + } case PLANE: { return "Plane"; - - } break; + } case AABB: { return "AABB"; - } break; + } case QUATERNION: { return "Quaternion"; - - } break; + } case BASIS: { return "Basis"; - - } break; + } case TRANSFORM3D: { return "Transform3D"; - - } break; + } case PROJECTION: { return "Projection"; + } - } break; - - // misc types + // Miscellaneous types. case COLOR: { return "Color"; - - } break; + } case RID: { return "RID"; - } break; + } case OBJECT: { return "Object"; - } break; + } case CALLABLE: { return "Callable"; - } break; + } case SIGNAL: { return "Signal"; - } break; + } case STRING_NAME: { return "StringName"; - - } break; + } case NODE_PATH: { return "NodePath"; - - } break; + } case DICTIONARY: { return "Dictionary"; - - } break; + } case ARRAY: { return "Array"; + } - } break; - - // arrays + // Arrays. case PACKED_BYTE_ARRAY: { return "PackedByteArray"; - - } break; + } case PACKED_INT32_ARRAY: { return "PackedInt32Array"; - - } break; + } case PACKED_INT64_ARRAY: { return "PackedInt64Array"; - - } break; + } case PACKED_FLOAT32_ARRAY: { return "PackedFloat32Array"; - - } break; + } case PACKED_FLOAT64_ARRAY: { return "PackedFloat64Array"; - - } break; + } case PACKED_STRING_ARRAY: { return "PackedStringArray"; - } break; + } case PACKED_VECTOR2_ARRAY: { return "PackedVector2Array"; - - } break; + } case PACKED_VECTOR3_ARRAY: { return "PackedVector3Array"; - - } break; + } case PACKED_COLOR_ARRAY: { return "PackedColorArray"; - - } break; + } default: { } } @@ -880,157 +860,126 @@ bool Variant::is_zero() const { switch (type) { case NIL: { return true; - } break; + } - // atomic types + // Atomic types. case BOOL: { return !(_data._bool); - } break; + } case INT: { return _data._int == 0; - - } break; + } case FLOAT: { return _data._float == 0; - - } break; + } case STRING: { return *reinterpret_cast<const String *>(_data._mem) == String(); + } - } break; - - // math types + // Math types. case VECTOR2: { return *reinterpret_cast<const Vector2 *>(_data._mem) == Vector2(); - - } break; + } case VECTOR2I: { return *reinterpret_cast<const Vector2i *>(_data._mem) == Vector2i(); - - } break; + } case RECT2: { return *reinterpret_cast<const Rect2 *>(_data._mem) == Rect2(); - - } break; + } case RECT2I: { return *reinterpret_cast<const Rect2i *>(_data._mem) == Rect2i(); - - } break; + } case TRANSFORM2D: { return *_data._transform2d == Transform2D(); - - } break; + } case VECTOR3: { return *reinterpret_cast<const Vector3 *>(_data._mem) == Vector3(); - - } break; + } case VECTOR3I: { return *reinterpret_cast<const Vector3i *>(_data._mem) == Vector3i(); - - } break; + } case VECTOR4: { return *reinterpret_cast<const Vector4 *>(_data._mem) == Vector4(); - - } break; + } case VECTOR4I: { return *reinterpret_cast<const Vector4i *>(_data._mem) == Vector4i(); - - } break; + } case PLANE: { return *reinterpret_cast<const Plane *>(_data._mem) == Plane(); - - } break; + } case AABB: { return *_data._aabb == ::AABB(); - } break; + } case QUATERNION: { return *reinterpret_cast<const Quaternion *>(_data._mem) == Quaternion(); - - } break; + } case BASIS: { return *_data._basis == Basis(); - - } break; + } case TRANSFORM3D: { return *_data._transform3d == Transform3D(); - - } break; + } case PROJECTION: { return *_data._projection == Projection(); + } - } break; - - // misc types + // Miscellaneous types. case COLOR: { return *reinterpret_cast<const Color *>(_data._mem) == Color(); - - } break; + } case RID: { return *reinterpret_cast<const ::RID *>(_data._mem) == ::RID(); - } break; + } case OBJECT: { return _get_obj().obj == nullptr; - } break; + } case CALLABLE: { return reinterpret_cast<const Callable *>(_data._mem)->is_null(); - } break; + } case SIGNAL: { return reinterpret_cast<const Signal *>(_data._mem)->is_null(); - } break; + } case STRING_NAME: { return *reinterpret_cast<const StringName *>(_data._mem) != StringName(); - - } break; + } case NODE_PATH: { return reinterpret_cast<const NodePath *>(_data._mem)->is_empty(); - - } break; + } case DICTIONARY: { return reinterpret_cast<const Dictionary *>(_data._mem)->is_empty(); - - } break; + } case ARRAY: { return reinterpret_cast<const Array *>(_data._mem)->is_empty(); + } - } break; - - // arrays + // Arrays. case PACKED_BYTE_ARRAY: { return PackedArrayRef<uint8_t>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_INT32_ARRAY: { return PackedArrayRef<int32_t>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_INT64_ARRAY: { return PackedArrayRef<int64_t>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_FLOAT32_ARRAY: { return PackedArrayRef<float>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_FLOAT64_ARRAY: { return PackedArrayRef<double>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_STRING_ARRAY: { return PackedArrayRef<String>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_VECTOR2_ARRAY: { return PackedArrayRef<Vector2>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_VECTOR3_ARRAY: { return PackedArrayRef<Vector3>::get_array(_data.packed_array).size() == 0; - - } break; + } case PACKED_COLOR_ARRAY: { return PackedArrayRef<Color>::get_array(_data.packed_array).size() == 0; - - } break; + } default: { } } @@ -1042,60 +991,49 @@ bool Variant::is_one() const { switch (type) { case NIL: { return true; - } break; + } - // atomic types case BOOL: { return _data._bool; - } break; + } case INT: { return _data._int == 1; - - } break; + } case FLOAT: { return _data._float == 1; + } - } break; case VECTOR2: { return *reinterpret_cast<const Vector2 *>(_data._mem) == Vector2(1, 1); - - } break; + } case VECTOR2I: { return *reinterpret_cast<const Vector2i *>(_data._mem) == Vector2i(1, 1); - - } break; + } case RECT2: { return *reinterpret_cast<const Rect2 *>(_data._mem) == Rect2(1, 1, 1, 1); - - } break; + } case RECT2I: { return *reinterpret_cast<const Rect2i *>(_data._mem) == Rect2i(1, 1, 1, 1); - - } break; + } case VECTOR3: { return *reinterpret_cast<const Vector3 *>(_data._mem) == Vector3(1, 1, 1); - - } break; + } case VECTOR3I: { return *reinterpret_cast<const Vector3i *>(_data._mem) == Vector3i(1, 1, 1); - - } break; + } case VECTOR4: { return *reinterpret_cast<const Vector4 *>(_data._mem) == Vector4(1, 1, 1, 1); - - } break; + } case VECTOR4I: { return *reinterpret_cast<const Vector4i *>(_data._mem) == Vector4i(1, 1, 1, 1); - - } break; + } case PLANE: { return *reinterpret_cast<const Plane *>(_data._mem) == Plane(1, 1, 1, 1); + } - } break; case COLOR: { return *reinterpret_cast<const Color *>(_data._mem) == Color(1, 1, 1, 1); - - } break; + } default: { return !is_zero(); @@ -1133,10 +1071,10 @@ void Variant::reference(const Variant &p_variant) { switch (p_variant.type) { case NIL: { - // none + // None. } break; - // atomic types + // Atomic types. case BOOL: { _data._bool = p_variant._data._bool; } break; @@ -1150,7 +1088,7 @@ void Variant::reference(const Variant &p_variant) { memnew_placement(_data._mem, String(*reinterpret_cast<const String *>(p_variant._data._mem))); } break; - // math types + // Math types. case VECTOR2: { memnew_placement(_data._mem, Vector2(*reinterpret_cast<const Vector2 *>(p_variant._data._mem))); } break; @@ -1202,10 +1140,9 @@ void Variant::reference(const Variant &p_variant) { memnew_placement(_data._projection, Projection(*p_variant._data._projection)); } break; - // misc types + // Miscellaneous types. case COLOR: { memnew_placement(_data._mem, Color(*reinterpret_cast<const Color *>(p_variant._data._mem))); - } break; case RID: { memnew_placement(_data._mem, ::RID(*reinterpret_cast<const ::RID *>(p_variant._data._mem))); @@ -1224,7 +1161,6 @@ void Variant::reference(const Variant &p_variant) { _get_obj().obj = const_cast<Object *>(p_variant._get_obj().obj); _get_obj().id = p_variant._get_obj().id; - } break; case CALLABLE: { memnew_placement(_data._mem, Callable(*reinterpret_cast<const Callable *>(p_variant._data._mem))); @@ -1234,84 +1170,71 @@ void Variant::reference(const Variant &p_variant) { } break; case STRING_NAME: { memnew_placement(_data._mem, StringName(*reinterpret_cast<const StringName *>(p_variant._data._mem))); - } break; case NODE_PATH: { memnew_placement(_data._mem, NodePath(*reinterpret_cast<const NodePath *>(p_variant._data._mem))); - } break; case DICTIONARY: { memnew_placement(_data._mem, Dictionary(*reinterpret_cast<const Dictionary *>(p_variant._data._mem))); - } break; case ARRAY: { memnew_placement(_data._mem, Array(*reinterpret_cast<const Array *>(p_variant._data._mem))); - } break; - // arrays + // Arrays. case PACKED_BYTE_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<uint8_t> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<uint8_t>::create(); } - } break; case PACKED_INT32_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<int32_t> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<int32_t>::create(); } - } break; case PACKED_INT64_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<int64_t> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<int64_t>::create(); } - } break; case PACKED_FLOAT32_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<float> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<float>::create(); } - } break; case PACKED_FLOAT64_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<double> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<double>::create(); } - } break; case PACKED_STRING_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<String> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<String>::create(); } - } break; case PACKED_VECTOR2_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<Vector2> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<Vector2>::create(); } - } break; case PACKED_VECTOR3_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<Vector3> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<Vector3>::create(); } - } break; case PACKED_COLOR_ARRAY: { _data.packed_array = static_cast<PackedArrayRef<Color> *>(p_variant._data.packed_array)->reference(); if (!_data.packed_array) { _data.packed_array = PackedArrayRef<Color>::create(); } - } break; default: { } @@ -1331,6 +1254,7 @@ void Variant::zero() { case FLOAT: this->_data._float = 0; break; + case VECTOR2: *reinterpret_cast<Vector2 *>(this->_data._mem) = Vector2(); break; @@ -1361,9 +1285,11 @@ void Variant::zero() { case QUATERNION: *reinterpret_cast<Quaternion *>(this->_data._mem) = Quaternion(); break; + case COLOR: *reinterpret_cast<Color *>(this->_data._mem) = Color(); break; + default: this->clear(); break; @@ -1375,15 +1301,8 @@ void Variant::_clear_internal() { case STRING: { reinterpret_cast<String *>(_data._mem)->~String(); } break; - /* - // no point, they don't allocate memory - VECTOR3, - PLANE, - QUATERNION, - COLOR, - VECTOR2, - RECT2 - */ + + // Math types. case TRANSFORM2D: { if (_data._transform2d) { _data._transform2d->~Transform2D(); @@ -1419,7 +1338,8 @@ void Variant::_clear_internal() { _data._projection = nullptr; } } break; - // misc types + + // Miscellaneous types. case STRING_NAME: { reinterpret_cast<StringName *>(_data._mem)->~StringName(); } break; @@ -1428,7 +1348,7 @@ void Variant::_clear_internal() { } break; case OBJECT: { if (_get_obj().id.is_ref_counted()) { - //we are safe that there is a reference here + // We are safe that there is a reference here. RefCounted *ref_counted = static_cast<RefCounted *>(_get_obj().obj); if (ref_counted->unreference()) { memdelete(ref_counted); @@ -1438,8 +1358,8 @@ void Variant::_clear_internal() { _get_obj().id = ObjectID(); } break; case RID: { - // not much need probably - // Can't seem to use destructor + scoping operator, so hack. + // Not much need probably. + // HACK: Can't seem to use destructor + scoping operator, so hack. typedef ::RID RID_Class; reinterpret_cast<RID_Class *>(_data._mem)->~RID_Class(); } break; @@ -1455,7 +1375,8 @@ void Variant::_clear_internal() { case ARRAY: { reinterpret_cast<Array *>(_data._mem)->~Array(); } break; - // arrays + + // Arrays. case PACKED_BYTE_ARRAY: { PackedArrayRefBase::destroy(_data.packed_array); } break; @@ -1484,7 +1405,9 @@ void Variant::_clear_internal() { PackedArrayRefBase::destroy(_data.packed_array); } break; default: { - } /* not needed */ + // Not needed, there is no point. The following do not allocate memory: + // VECTOR2, VECTOR3, RECT2, PLANE, QUATERNION, COLOR. + } } } @@ -1863,34 +1786,34 @@ String Variant::stringify(int recursion_count) const { str += " }"; return str; - } break; + } case PACKED_VECTOR2_ARRAY: { return stringify_vector(operator Vector<Vector2>(), recursion_count); - } break; + } case PACKED_VECTOR3_ARRAY: { return stringify_vector(operator Vector<Vector3>(), recursion_count); - } break; + } case PACKED_COLOR_ARRAY: { return stringify_vector(operator Vector<Color>(), recursion_count); - } break; + } case PACKED_STRING_ARRAY: { return stringify_vector(operator Vector<String>(), recursion_count); - } break; + } case PACKED_BYTE_ARRAY: { return stringify_vector(operator Vector<uint8_t>(), recursion_count); - } break; + } case PACKED_INT32_ARRAY: { return stringify_vector(operator Vector<int32_t>(), recursion_count); - } break; + } case PACKED_INT64_ARRAY: { return stringify_vector(operator Vector<int64_t>(), recursion_count); - } break; + } case PACKED_FLOAT32_ARRAY: { return stringify_vector(operator Vector<float>(), recursion_count); - } break; + } case PACKED_FLOAT64_ARRAY: { return stringify_vector(operator Vector<double>(), recursion_count); - } break; + } case ARRAY: { Array arr = operator Array(); if (recursion_count > MAX_RECURSION) { @@ -1899,8 +1822,7 @@ String Variant::stringify(int recursion_count) const { } return stringify_vector(arr, recursion_count); - - } break; + } case OBJECT: { if (_get_obj().obj) { if (!_get_obj().id.is_ref_counted() && ObjectDB::get_instance(_get_obj().id) == nullptr) { @@ -1911,20 +1833,19 @@ String Variant::stringify(int recursion_count) const { } else { return "<Object#null>"; } - - } break; + } case CALLABLE: { const Callable &c = *reinterpret_cast<const Callable *>(_data._mem); return c; - } break; + } case SIGNAL: { const Signal &s = *reinterpret_cast<const Signal *>(_data._mem); return s; - } break; + } case RID: { const ::RID &s = *reinterpret_cast<const ::RID *>(_data._mem); return "RID(" + itos(s.get_id()) + ")"; - } break; + } default: { return "<" + get_type_name(type) + ">"; } @@ -1932,8 +1853,7 @@ String Variant::stringify(int recursion_count) const { } String Variant::to_json_string() const { - JSON json; - return json.stringify(*this); + return JSON::stringify(*this); } Variant::operator Vector2() const { diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 1831f7b72a..900e3d8e77 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1606,6 +1606,7 @@ static void _register_variant_builtin_methods() { bind_method(Vector2, is_normalized, sarray(), varray()); bind_method(Vector2, is_equal_approx, sarray("to"), varray()); bind_method(Vector2, is_zero_approx, sarray(), varray()); + bind_method(Vector2, is_finite, sarray(), varray()); bind_method(Vector2, posmod, sarray("mod"), varray()); bind_method(Vector2, posmodv, sarray("modv"), varray()); bind_method(Vector2, project, sarray("b"), varray()); @@ -1653,6 +1654,7 @@ static void _register_variant_builtin_methods() { bind_method(Rect2, has_area, sarray(), varray()); bind_method(Rect2, has_point, sarray("point"), varray()); bind_method(Rect2, is_equal_approx, sarray("rect"), varray()); + bind_method(Rect2, is_finite, sarray(), varray()); bind_method(Rect2, intersects, sarray("b", "include_borders"), varray(false)); bind_method(Rect2, encloses, sarray("b"), varray()); bind_method(Rect2, intersection, sarray("b"), varray()); @@ -1695,6 +1697,7 @@ static void _register_variant_builtin_methods() { bind_method(Vector3, is_normalized, sarray(), varray()); bind_method(Vector3, is_equal_approx, sarray("to"), varray()); bind_method(Vector3, is_zero_approx, sarray(), varray()); + bind_method(Vector3, is_finite, sarray(), varray()); bind_method(Vector3, inverse, sarray(), varray()); bind_method(Vector3, clamp, sarray("min", "max"), varray()); bind_method(Vector3, snapped, sarray("step"), varray()); @@ -1759,6 +1762,7 @@ static void _register_variant_builtin_methods() { bind_method(Vector4, inverse, sarray(), varray()); bind_method(Vector4, is_equal_approx, sarray("with"), varray()); bind_method(Vector4, is_zero_approx, sarray(), varray()); + bind_method(Vector4, is_finite, sarray(), varray()); /* Vector4i */ @@ -1775,6 +1779,7 @@ static void _register_variant_builtin_methods() { bind_method(Plane, normalized, sarray(), varray()); bind_method(Plane, center, sarray(), varray()); bind_method(Plane, is_equal_approx, sarray("to_plane"), varray()); + bind_method(Plane, is_finite, sarray(), varray()); bind_method(Plane, is_point_over, sarray("point"), varray()); bind_method(Plane, distance_to, sarray("point"), varray()); bind_method(Plane, has_point, sarray("point", "tolerance"), varray(CMP_EPSILON)); @@ -1790,6 +1795,7 @@ static void _register_variant_builtin_methods() { bind_method(Quaternion, normalized, sarray(), varray()); bind_method(Quaternion, is_normalized, sarray(), varray()); bind_method(Quaternion, is_equal_approx, sarray("to"), varray()); + bind_method(Quaternion, is_finite, sarray(), varray()); bind_method(Quaternion, inverse, sarray(), varray()); bind_method(Quaternion, log, sarray(), varray()); bind_method(Quaternion, exp, sarray(), varray()); @@ -1909,6 +1915,7 @@ static void _register_variant_builtin_methods() { bind_method(Transform2D, basis_xform_inv, sarray("v"), varray()); bind_method(Transform2D, interpolate_with, sarray("xform", "weight"), varray()); bind_method(Transform2D, is_equal_approx, sarray("xform"), varray()); + bind_method(Transform2D, is_finite, sarray(), varray()); bind_method(Transform2D, set_rotation, sarray("rotation"), varray()); bind_method(Transform2D, set_scale, sarray("scale"), varray()); bind_method(Transform2D, set_skew, sarray("skew"), varray()); @@ -1929,6 +1936,7 @@ static void _register_variant_builtin_methods() { bind_method(Basis, tdotz, sarray("with"), varray()); bind_method(Basis, slerp, sarray("to", "weight"), varray()); bind_method(Basis, is_equal_approx, sarray("b"), varray()); + bind_method(Basis, is_finite, sarray(), varray()); bind_method(Basis, get_rotation_quaternion, sarray(), varray()); bind_static_method(Basis, looking_at, sarray("target", "up"), varray(Vector3(0, 1, 0))); bind_static_method(Basis, from_scale, sarray("scale"), varray()); @@ -1943,6 +1951,7 @@ static void _register_variant_builtin_methods() { bind_method(AABB, has_surface, sarray(), varray()); bind_method(AABB, has_point, sarray("point"), varray()); bind_method(AABB, is_equal_approx, sarray("aabb"), varray()); + bind_method(AABB, is_finite, sarray(), varray()); bind_method(AABB, intersects, sarray("with"), varray()); bind_method(AABB, encloses, sarray("with"), varray()); bind_method(AABB, intersects_plane, sarray("plane"), varray()); @@ -1975,6 +1984,7 @@ static void _register_variant_builtin_methods() { bind_method(Transform3D, looking_at, sarray("target", "up"), varray(Vector3(0, 1, 0))); bind_method(Transform3D, interpolate_with, sarray("xform", "weight"), varray()); bind_method(Transform3D, is_equal_approx, sarray("xform"), varray()); + bind_method(Transform3D, is_finite, sarray(), varray()); /* Projection */ diff --git a/core/variant/variant_utility.cpp b/core/variant/variant_utility.cpp index 670b66d53e..3843c32bcc 100644 --- a/core/variant/variant_utility.cpp +++ b/core/variant/variant_utility.cpp @@ -310,6 +310,10 @@ struct VariantUtilityFunctions { return Math::is_zero_approx(x); } + static inline bool is_finite(double x) { + return Math::is_finite(x); + } + static inline double ease(float x, float curve) { return Math::ease(x, curve); } @@ -1420,6 +1424,7 @@ void Variant::_register_variant_utility_functions() { FUNCBINDR(is_equal_approx, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_MATH); FUNCBINDR(is_zero_approx, sarray("x"), Variant::UTILITY_FUNC_TYPE_MATH); + FUNCBINDR(is_finite, sarray("x"), Variant::UTILITY_FUNC_TYPE_MATH); FUNCBINDR(ease, sarray("x", "curve"), Variant::UTILITY_FUNC_TYPE_MATH); FUNCBINDR(step_decimals, sarray("x"), Variant::UTILITY_FUNC_TYPE_MATH); diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 6e9e82bbf0..43f85fcdbc 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -476,6 +476,13 @@ Infinity values of the same sign are considered equal. </description> </method> + <method name="is_finite"> + <return type="bool" /> + <param index="0" name="x" type="float" /> + <description> + Returns whether [code]x[/code] is a finite value, i.e. it is not [constant @GDScript.NAN], positive infinity, or negative infinity. + </description> + </method> <method name="is_inf"> <return type="bool" /> <param index="0" name="x" type="float" /> diff --git a/doc/classes/AABB.xml b/doc/classes/AABB.xml index 23dd41f275..1ac3e6b64c 100644 --- a/doc/classes/AABB.xml +++ b/doc/classes/AABB.xml @@ -205,6 +205,12 @@ Returns [code]true[/code] if this [AABB] and [param aabb] are approximately equal, by calling [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this [AABB] is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="merge" qualifiers="const"> <return type="AABB" /> <param index="0" name="with" type="AABB" /> diff --git a/doc/classes/Area3D.xml b/doc/classes/Area3D.xml index ce49be9bc1..ea8cab324d 100644 --- a/doc/classes/Area3D.xml +++ b/doc/classes/Area3D.xml @@ -110,11 +110,11 @@ <member name="reverb_bus_amount" type="float" setter="set_reverb_amount" getter="get_reverb_amount" default="0.0"> The degree to which this area applies reverb to its associated audio. Ranges from [code]0[/code] to [code]1[/code] with [code]0.1[/code] precision. </member> - <member name="reverb_bus_enable" type="bool" setter="set_use_reverb_bus" getter="is_using_reverb_bus" default="false"> + <member name="reverb_bus_enabled" type="bool" setter="set_use_reverb_bus" getter="is_using_reverb_bus" default="false"> If [code]true[/code], the area applies reverb to its associated audio. </member> - <member name="reverb_bus_name" type="StringName" setter="set_reverb_bus" getter="get_reverb_bus" default="&"Master""> - The reverb bus name to use for this area's associated audio. + <member name="reverb_bus_name" type="StringName" setter="set_reverb_bus_name" getter="get_reverb_bus_name" default="&"Master""> + The name of the reverb bus to use for this area's associated audio. </member> <member name="reverb_bus_uniformity" type="float" setter="set_reverb_uniformity" getter="get_reverb_uniformity" default="0.0"> The degree to which this area's reverb is a uniform effect. Ranges from [code]0[/code] to [code]1[/code] with [code]0.1[/code] precision. diff --git a/doc/classes/BaseMaterial3D.xml b/doc/classes/BaseMaterial3D.xml index cbb58a3e1e..dbc7d0fb29 100644 --- a/doc/classes/BaseMaterial3D.xml +++ b/doc/classes/BaseMaterial3D.xml @@ -305,7 +305,7 @@ <member name="proximity_fade_distance" type="float" setter="set_proximity_fade_distance" getter="get_proximity_fade_distance" default="1.0"> Distance over which the fade effect takes place. The larger the distance the longer it takes for an object to fade. </member> - <member name="proximity_fade_enable" type="bool" setter="set_proximity_fade" getter="is_proximity_fade_enabled" default="false"> + <member name="proximity_fade_enabled" type="bool" setter="set_proximity_fade_enabled" getter="is_proximity_fade_enabled" default="false"> If [code]true[/code], the proximity fade effect is enabled. The proximity fade effect fades out each pixel based on its distance to another object. </member> <member name="refraction_enabled" type="bool" setter="set_feature" getter="get_feature" default="false"> diff --git a/doc/classes/Basis.xml b/doc/classes/Basis.xml index 6d9b679fbc..f499be34a0 100644 --- a/doc/classes/Basis.xml +++ b/doc/classes/Basis.xml @@ -112,6 +112,12 @@ Returns [code]true[/code] if this basis and [param b] are approximately equal, by calling [code]is_equal_approx[/code] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this basis is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="looking_at" qualifiers="static"> <return type="Basis" /> <param index="0" name="target" type="Vector3" /> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 7f3ffce9b7..68de08f094 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -457,6 +457,12 @@ If [code]true[/code], when saving a file, the editor will rename the old file to a different name, save a new file, then only remove the old file once the new file has been saved. This makes loss of data less likely to happen if the editor or operating system exits unexpectedly while saving (e.g. due to a crash or power outage). [b]Note:[/b] On Windows, this feature can interact negatively with certain antivirus programs. In this case, you may have to set this to [code]false[/code] to prevent file locking issues. </member> + <member name="interface/editor/accept_dialog_cancel_ok_buttons" type="int" setter="" getter=""> + How to position the Cancel and OK buttons in the editor's [AcceptDialog]s. Different platforms have different standard behaviors for this, which can be overridden using this setting. This is useful if you use Godot both on Windows and macOS/Linux and your Godot muscle memory is stronger than your OS specific one. + - [b]Auto[/b] follows the platform convention: Cancel first on macOS and Linux, OK first on Windows. + - [b]Cancel First[/b] forces the ordering Cancel/OK. + - [b]OK First[/b] forces the ordering OK/Cancel. + </member> <member name="interface/editor/automatically_open_screenshots" type="bool" setter="" getter=""> If [code]true[/code], automatically opens screenshots with the default program associated to [code].png[/code] files after a screenshot is taken using the [b]Editor > Take Screenshot[/b] action. </member> diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml index 15b3d4958c..3aa26cbb21 100644 --- a/doc/classes/OS.xml +++ b/doc/classes/OS.xml @@ -445,6 +445,15 @@ [b]Note:[/b] This method is not supported on the web platform. It returns an empty string. </description> </method> + <method name="get_video_adapter_driver_info" qualifiers="const"> + <return type="PackedStringArray" /> + <description> + Returns the video adapter driver name and version for the user's currently active graphics card. + The first element holds the driver name, such as [code]nvidia[/code], [code]amdgpu[/code], etc. + The second element holds the driver version. For e.g. the [code]nvidia[/code] driver on a Linux/BSD platform, the version is in the format [code]510.85.02[/code]. For Windows, the driver's format is [code]31.0.15.1659[/code]. + [b]Note:[/b] This method is only supported on the platforms Linux/BSD and Windows. It returns an empty array on other platforms. + </description> + </method> <method name="has_environment" qualifiers="const"> <return type="bool" /> <param index="0" name="variable" type="String" /> @@ -616,11 +625,11 @@ </member> </members> <constants> - <constant name="VIDEO_DRIVER_VULKAN" value="0" enum="VideoDriver"> - The Vulkan rendering backend. It requires Vulkan 1.0 support and automatically uses features from Vulkan 1.1 and 1.2 if available. + <constant name="RENDERING_DRIVER_VULKAN" value="0" enum="RenderingDriver"> + The Vulkan rendering driver. It requires Vulkan 1.0 support and automatically uses features from Vulkan 1.1 and 1.2 if available. </constant> - <constant name="VIDEO_DRIVER_OPENGL_3" value="1" enum="VideoDriver"> - The OpenGL 3 rendering backend. It uses OpenGL 3.3 Core Profile on desktop platforms, OpenGL ES 3.0 on mobile devices, and WebGL 2.0 on Web. + <constant name="RENDERING_DRIVER_OPENGL3" value="1" enum="RenderingDriver"> + The OpenGL 3 rendering driver. It uses OpenGL 3.3 Core Profile on desktop platforms, OpenGL ES 3.0 on mobile devices, and WebGL 2.0 on Web. </constant> <constant name="DAY_SUNDAY" value="0" enum="Weekday"> Sunday. diff --git a/doc/classes/Plane.xml b/doc/classes/Plane.xml index e51e3753fc..fbe8afa8d1 100644 --- a/doc/classes/Plane.xml +++ b/doc/classes/Plane.xml @@ -119,6 +119,12 @@ Returns [code]true[/code] if this plane and [param to_plane] are approximately equal, by running [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this plane is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="is_point_over" qualifiers="const"> <return type="bool" /> <param index="0" name="point" type="Vector3" /> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 214f087d78..4b2bb3b6e9 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -566,7 +566,7 @@ If [code]true[/code], the home indicator is hidden automatically. This only affects iOS devices without a physical home button. </member> <member name="display/window/per_pixel_transparency/allowed" type="bool" setter="" getter="" default="false"> - If [code]true[/code], allows per-pixel transparency for the window background. This affects performance, so leave it on [code]false[/code] unless you need it. + If [code]true[/code], allows per-pixel transparency for the window background. This affects performance, so leave it on [code]false[/code] unless you need it. See also [member display/window/size/transparent] and [member rendering/transparent_background]. </member> <member name="display/window/size/always_on_top" type="bool" setter="" getter="" default="false"> Forces the main window to be always on top. @@ -591,8 +591,8 @@ [b]Note:[/b] This setting is ignored on iOS. </member> <member name="display/window/size/transparent" type="bool" setter="" getter="" default="false"> - Main window background can be transparent. - [b]Note:[/b] To use transparent splash screen, set [member application/boot_splash/bg_color] to [code]Color(0, 0, 0, 0)[/code]. + If [code]true[/code], enables a window manager hint that the main window background [i]can[/i] be transparent. This does not make the background actually transparent. For the background to be transparent, the root viewport must also be made transparent by enabling [member rendering/transparent_background]. + [b]Note:[/b] To use a transparent splash screen, set [member application/boot_splash/bg_color] to [code]Color(0, 0, 0, 0)[/code]. [b]Note:[/b] This setting has no effect if [member display/window/per_pixel_transparency/allowed] is set to [code]false[/code]. </member> <member name="display/window/size/viewport_height" type="int" setter="" getter="" default="648"> @@ -2100,8 +2100,6 @@ <member name="rendering/renderer/rendering_method.web" type="String" setter="" getter="" default=""gl_compatibility""> Override for [member rendering/renderer/rendering_method] on web. </member> - <member name="rendering/rendering_device/descriptor_pools/max_descriptors_per_pool" type="int" setter="" getter="" default="64"> - </member> <member name="rendering/rendering_device/driver" type="String" setter="" getter="" default=""vulkan""> Sets the driver to be used by the renderer when using a RenderingDevice-based renderer like the clustered renderer or the mobile renderer. This property can not be edited directly, instead, set the driver using the platform-specific overrides. </member> @@ -2126,6 +2124,8 @@ </member> <member name="rendering/rendering_device/staging_buffer/texture_upload_region_size_px" type="int" setter="" getter="" default="64"> </member> + <member name="rendering/rendering_device/vulkan/max_descriptors_per_pool" type="int" setter="" getter="" default="64"> + </member> <member name="rendering/scaling_3d/fsr_sharpness" type="float" setter="" getter="" default="0.2"> Determines how sharp the upscaled image will be when using the FSR upscaling mode. Sharpness halves with every whole number. Values go from 0.0 (sharpest) to 2.0. Values above 2.0 won't make a visible difference. </member> @@ -2202,6 +2202,9 @@ If [code]true[/code], the texture importer will import VRAM-compressed textures using the S3 Texture Compression algorithm. This algorithm is only supported on desktop platforms and consoles. [b]Note:[/b] Changing this setting does [i]not[/i] impact textures that were already imported before. To make this setting apply to textures that were already imported, exit the editor, remove the [code].godot/imported/[/code] folder located inside the project folder then restart the editor (see [member application/config/use_hidden_project_data_directory]). </member> + <member name="rendering/transparent_background" type="bool" setter="" getter="" default="false"> + If [code]true[/code], enables [member Viewport.transparent_bg] on the root viewport. This allows per-pixel transparency to be effective after also enabling [member display/window/size/transparent] and [member display/window/per_pixel_transparency/allowed]. + </member> <member name="rendering/vrs/mode" type="int" setter="" getter="" default="0"> Set the default Variable Rate Shading (VRS) mode for the main viewport. See [member Viewport.vrs_mode] to change this at runtime, and [enum Viewport.VRSMode] for possible values. </member> diff --git a/doc/classes/Quaternion.xml b/doc/classes/Quaternion.xml index f21ebf57e2..99dffeff9d 100644 --- a/doc/classes/Quaternion.xml +++ b/doc/classes/Quaternion.xml @@ -115,6 +115,12 @@ Returns [code]true[/code] if this quaternion and [param to] are approximately equal, by running [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this quaternion is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="is_normalized" qualifiers="const"> <return type="bool" /> <description> diff --git a/doc/classes/RDPipelineRasterizationState.xml b/doc/classes/RDPipelineRasterizationState.xml index 39a64c730a..48599b6262 100644 --- a/doc/classes/RDPipelineRasterizationState.xml +++ b/doc/classes/RDPipelineRasterizationState.xml @@ -13,7 +13,7 @@ </member> <member name="depth_bias_constant_factor" type="float" setter="set_depth_bias_constant_factor" getter="get_depth_bias_constant_factor" default="0.0"> </member> - <member name="depth_bias_enable" type="bool" setter="set_depth_bias_enable" getter="get_depth_bias_enable" default="false"> + <member name="depth_bias_enabled" type="bool" setter="set_depth_bias_enabled" getter="get_depth_bias_enabled" default="false"> </member> <member name="depth_bias_slope_factor" type="float" setter="set_depth_bias_slope_factor" getter="get_depth_bias_slope_factor" default="0.0"> </member> diff --git a/doc/classes/Rect2.xml b/doc/classes/Rect2.xml index ac012e9604..28fe42afae 100644 --- a/doc/classes/Rect2.xml +++ b/doc/classes/Rect2.xml @@ -165,6 +165,12 @@ Returns [code]true[/code] if this [Rect2] and [param rect] are approximately equal, by calling [code]is_equal_approx[/code] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this [Rect2] is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="merge" qualifiers="const"> <return type="Rect2" /> <param index="0" name="b" type="Rect2" /> diff --git a/doc/classes/ResourceFormatLoader.xml b/doc/classes/ResourceFormatLoader.xml index 9b8c8d4d9d..2b6376f2cd 100644 --- a/doc/classes/ResourceFormatLoader.xml +++ b/doc/classes/ResourceFormatLoader.xml @@ -71,6 +71,15 @@ The [param cache_mode] property defines whether and how the cache should be used or updated when loading the resource. See [enum CacheMode] for details. </description> </method> + <method name="_recognize_path" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="path" type="String" /> + <param index="1" name="type" type="StringName" /> + <description> + Tells whether or not this loader should load a resource from its resource path for a given type. + If it is not implemented, the default behavior returns whether the path's extension is within the ones provided by [method _get_recognized_extensions], and if the type is within the ones provided by [method _get_resource_type]. + </description> + </method> <method name="_rename_dependencies" qualifiers="virtual const"> <return type="int" /> <param index="0" name="path" type="String" /> diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index f4cbf4c442..b5a917b2bb 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -23,9 +23,11 @@ <param index="2" name="height" type="int" default="0" /> <param index="3" name="color" type="Color" default="Color(1, 1, 1, 1)" /> <param index="4" name="inline_align" type="int" enum="InlineAlignment" default="5" /> + <param index="5" name="region" type="Rect2" default="Rect2(0, 0, 0, 0)" /> <description> - Adds an image's opening and closing tags to the tag stack, optionally providing a [param width] and [param height] to resize the image and a [param color] to tint the image. + Adds an image's opening and closing tags to the tag stack, optionally providing a [param width] and [param height] to resize the image, a [param color] to tint the image and a [param region] to only use parts of the image. If [param width] or [param height] is set to 0, the image size will be adjusted in order to keep the original aspect ratio. + If [param width] and [param height] are not set, but [param region] is, the region's rect will be used. </description> </method> <method name="add_text"> diff --git a/doc/classes/Transform2D.xml b/doc/classes/Transform2D.xml index 905b3d77af..23d20a5a75 100644 --- a/doc/classes/Transform2D.xml +++ b/doc/classes/Transform2D.xml @@ -123,6 +123,12 @@ Returns [code]true[/code] if this transform and [code]transform[/code] are approximately equal, by calling [code]is_equal_approx[/code] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this transform is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="looking_at" qualifiers="const"> <return type="Transform2D" /> <param index="0" name="target" type="Vector2" default="Vector2(0, 0)" /> diff --git a/doc/classes/Transform3D.xml b/doc/classes/Transform3D.xml index 18b4f9e6f9..b3145ea022 100644 --- a/doc/classes/Transform3D.xml +++ b/doc/classes/Transform3D.xml @@ -82,6 +82,12 @@ Returns [code]true[/code] if this transform and [code]transform[/code] are approximately equal, by calling [code]is_equal_approx[/code] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this transform is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="looking_at" qualifiers="const"> <return type="Transform3D" /> <param index="0" name="target" type="Vector3" /> diff --git a/doc/classes/Vector2.xml b/doc/classes/Vector2.xml index e1852340c0..5590f82336 100644 --- a/doc/classes/Vector2.xml +++ b/doc/classes/Vector2.xml @@ -206,6 +206,12 @@ Returns [code]true[/code] if this vector and [code]v[/code] are approximately equal, by running [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this vector is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="is_normalized" qualifiers="const"> <return type="bool" /> <description> diff --git a/doc/classes/Vector3.xml b/doc/classes/Vector3.xml index 1ef84050cd..81e8dd2260 100644 --- a/doc/classes/Vector3.xml +++ b/doc/classes/Vector3.xml @@ -174,6 +174,12 @@ Returns [code]true[/code] if this vector and [param to] are approximately equal, by running [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this vector is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="is_normalized" qualifiers="const"> <return type="bool" /> <description> diff --git a/doc/classes/Vector4.xml b/doc/classes/Vector4.xml index fdc93f82ec..662d0bce3a 100644 --- a/doc/classes/Vector4.xml +++ b/doc/classes/Vector4.xml @@ -135,6 +135,12 @@ Returns [code]true[/code] if this vector and [param with] are approximately equal, by running [method @GlobalScope.is_equal_approx] on each component. </description> </method> + <method name="is_finite" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this vector is finite, by calling [method @GlobalScope.is_finite] on each component. + </description> + </method> <method name="is_normalized" qualifiers="const"> <return type="bool" /> <description> diff --git a/drivers/SCsub b/drivers/SCsub index dd81fc645c..6cfcb1d18c 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -24,6 +24,7 @@ SConscript("winmidi/SCsub") # Graphics drivers if env["vulkan"]: + SConscript("spirv-reflect/SCsub") SConscript("vulkan/SCsub") if env["opengl3"]: SConscript("gl_context/SCsub") @@ -31,7 +32,6 @@ if env["opengl3"]: # Core dependencies SConscript("png/SCsub") -SConscript("spirv-reflect/SCsub") env.add_source_files(env.drivers_sources, "*.cpp") diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index 8e6009c943..f5241e33ab 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -897,7 +897,9 @@ _FORCE_INLINE_ static void _fill_std140_ubo_empty(ShaderLanguage::DataType type, case ShaderLanguage::TYPE_BVEC3: case ShaderLanguage::TYPE_IVEC3: case ShaderLanguage::TYPE_UVEC3: - case ShaderLanguage::TYPE_VEC3: + case ShaderLanguage::TYPE_VEC3: { + memset(data, 0, 12 * p_array_size); + } break; case ShaderLanguage::TYPE_BVEC4: case ShaderLanguage::TYPE_IVEC4: case ShaderLanguage::TYPE_UVEC4: diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp index 10d65b83db..fc06291a3a 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -145,6 +145,10 @@ void OS_Unix::finalize_core() { NetSocketPosix::cleanup(); } +Vector<String> OS_Unix::get_video_adapter_driver_info() const { + return Vector<String>(); +} + String OS_Unix::get_stdin_string(bool p_block) { if (p_block) { char buff[1024]; diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h index fce962e32c..ce06a52a95 100644 --- a/drivers/unix/os_unix.h +++ b/drivers/unix/os_unix.h @@ -51,6 +51,8 @@ protected: public: OS_Unix(); + virtual Vector<String> get_video_adapter_driver_info() const override; + virtual String get_stdin_string(bool p_block) override; virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) override; diff --git a/drivers/unix/thread_posix.cpp b/drivers/unix/thread_posix.cpp index f6adbee108..5154feb478 100644 --- a/drivers/unix/thread_posix.cpp +++ b/drivers/unix/thread_posix.cpp @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#if defined(UNIX_ENABLED) || defined(PTHREAD_ENABLED) +#if defined(UNIX_ENABLED) #include "thread_posix.h" @@ -73,4 +73,4 @@ void init_thread_posix() { Thread::_set_platform_functions({ .set_name = set_name }); } -#endif // UNIX_ENABLED || PTHREAD_ENABLED +#endif // UNIX_ENABLED diff --git a/drivers/vulkan/rendering_device_vulkan.cpp b/drivers/vulkan/rendering_device_vulkan.cpp index 74bcc7cbff..72c9a80a94 100644 --- a/drivers/vulkan/rendering_device_vulkan.cpp +++ b/drivers/vulkan/rendering_device_vulkan.cpp @@ -5170,9 +5170,9 @@ Vector<uint8_t> RenderingDeviceVulkan::shader_compile_binary_from_spirv(const Ve uint32_t offset = 0; uint8_t *binptr = ret.ptrw(); binptr[0] = 'G'; - binptr[1] = 'V'; + binptr[1] = 'S'; binptr[2] = 'B'; - binptr[3] = 'D'; // Godot vulkan binary data. + binptr[3] = 'D'; // Godot Shader Binary Data. offset += 4; encode_uint32(SHADER_BINARY_VERSION, binptr + offset); offset += sizeof(uint32_t); @@ -5233,7 +5233,7 @@ RID RenderingDeviceVulkan::shader_create_from_bytecode(const Vector<uint8_t> &p_ uint32_t read_offset = 0; // Consistency check. ERR_FAIL_COND_V(binsize < sizeof(uint32_t) * 3 + sizeof(RenderingDeviceVulkanShaderBinaryData), RID()); - ERR_FAIL_COND_V(binptr[0] != 'G' || binptr[1] != 'V' || binptr[2] != 'B' || binptr[3] != 'D', RID()); + ERR_FAIL_COND_V(binptr[0] != 'G' || binptr[1] != 'S' || binptr[2] != 'B' || binptr[3] != 'D', RID()); uint32_t bin_version = decode_uint32(binptr + 4); ERR_FAIL_COND_V(bin_version != SHADER_BINARY_VERSION, RID()); @@ -6559,7 +6559,7 @@ RID RenderingDeviceVulkan::render_pipeline_create(RID p_shader, FramebufferForma ERR_FAIL_INDEX_V(p_rasterization_state.cull_mode, 3, RID()); rasterization_state_create_info.cullMode = cull_mode[p_rasterization_state.cull_mode]; rasterization_state_create_info.frontFace = (p_rasterization_state.front_face == POLYGON_FRONT_FACE_CLOCKWISE ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE); - rasterization_state_create_info.depthBiasEnable = p_rasterization_state.depth_bias_enable; + rasterization_state_create_info.depthBiasEnable = p_rasterization_state.depth_bias_enabled; rasterization_state_create_info.depthBiasConstantFactor = p_rasterization_state.depth_bias_constant_factor; rasterization_state_create_info.depthBiasClamp = p_rasterization_state.depth_bias_clamp; rasterization_state_create_info.depthBiasSlopeFactor = p_rasterization_state.depth_bias_slope_factor; @@ -9391,7 +9391,7 @@ void RenderingDeviceVulkan::initialize(VulkanContext *p_context, bool p_local_de ERR_CONTINUE(err != OK); } - max_descriptors_per_pool = GLOBAL_DEF("rendering/rendering_device/descriptor_pools/max_descriptors_per_pool", 64); + max_descriptors_per_pool = GLOBAL_DEF("rendering/rendering_device/vulkan/max_descriptors_per_pool", 64); // Check to make sure DescriptorPoolKey is good. static_assert(sizeof(uint64_t) * 3 >= UNIFORM_TYPE_MAX * sizeof(uint16_t)); diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index b73b49a434..e328f76545 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -230,6 +230,14 @@ void FindReplaceBar::_replace_all() { Point2i prev_match = Point2(-1, -1); bool selection_enabled = text_editor->has_selection(0); + if (!is_selection_only()) { + text_editor->deselect(); + selection_enabled = false; + } else { + result_line = -1; + result_col = -1; + } + Point2i selection_begin, selection_end; if (selection_enabled) { selection_begin = Point2i(text_editor->get_selection_from_line(0), text_editor->get_selection_from_column(0)); @@ -238,9 +246,6 @@ void FindReplaceBar::_replace_all() { int vsval = text_editor->get_v_scroll(); - text_editor->set_caret_line(0, false, true, 0, 0); - text_editor->set_caret_column(0, true, 0); - String repl_text = get_replace_text(); int search_text_len = get_search_text().length(); @@ -253,7 +258,11 @@ void FindReplaceBar::_replace_all() { if (selection_enabled && is_selection_only()) { text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); text_editor->set_caret_column(selection_begin.height, true, 0); + } else { + text_editor->set_caret_line(0, false, true, 0, 0); + text_editor->set_caret_column(0, true, 0); } + if (search_current()) { do { // replace area @@ -269,7 +278,7 @@ void FindReplaceBar::_replace_all() { text_editor->unfold_line(result_line); text_editor->select(result_line, result_col, result_line, match_to.y, 0); - if (selection_enabled && is_selection_only()) { + if (selection_enabled) { if (match_from < selection_begin || match_to > selection_end) { break; // Done. } @@ -297,11 +306,9 @@ void FindReplaceBar::_replace_all() { text_editor->set_caret_line(orig_cursor.x, false, true, 0, 0); text_editor->set_caret_column(orig_cursor.y, true, 0); - if (selection_enabled && is_selection_only()) { + if (selection_enabled) { // Reselect. text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y, 0); - } else { - text_editor->deselect(0); } text_editor->set_v_scroll(vsval); @@ -314,21 +321,28 @@ void FindReplaceBar::_replace_all() { needs_to_count_results = true; } -void FindReplaceBar::_get_search_from(int &r_line, int &r_col) { - r_line = text_editor->get_caret_line(0); - r_col = text_editor->get_caret_column(0); +void FindReplaceBar::_get_search_from(int &r_line, int &r_col, bool p_is_searching_next) { + if (!text_editor->has_selection(0) || is_selection_only()) { + r_line = text_editor->get_caret_line(0); + r_col = text_editor->get_caret_column(0); - if (text_editor->has_selection(0) && is_selection_only()) { + if (!p_is_searching_next && r_line == result_line && r_col >= result_col && r_col <= result_col + get_search_text().length()) { + r_col = result_col; + } return; } - if (r_line == result_line && r_col >= result_col && r_col <= result_col + get_search_text().length()) { - r_col = result_col; + if (p_is_searching_next) { + r_line = text_editor->get_selection_to_line(); + r_col = text_editor->get_selection_to_column(); + } else { + r_line = text_editor->get_selection_from_line(); + r_col = text_editor->get_selection_from_column(); } } void FindReplaceBar::_update_results_count() { - if (!needs_to_count_results && (result_line != -1)) { + if (!needs_to_count_results && (result_line != -1) && results_count_to_current > 0) { results_count_to_current += (flags & TextEdit::SEARCH_BACKWARDS) ? -1 : 1; if (results_count_to_current > results_count) { @@ -340,9 +354,6 @@ void FindReplaceBar::_update_results_count() { return; } - results_count = 0; - results_count_to_current = 0; - String searched = get_search_text(); if (searched.is_empty()) { return; @@ -350,6 +361,8 @@ void FindReplaceBar::_update_results_count() { needs_to_count_results = false; + results_count = 0; + for (int i = 0; i < text_editor->get_line_count(); i++) { String line_text = text_editor->get_line(i); @@ -373,8 +386,13 @@ void FindReplaceBar::_update_results_count() { results_count++; - if (i == result_line && col_pos == result_col) { - results_count_to_current = results_count; + if (i == result_line) { + if (col_pos == result_col) { + results_count_to_current = results_count; + } else if (col_pos < result_col && col_pos + searched.length() > result_col) { + col_pos = result_col; + results_count_to_current = results_count; + } } col_pos += searched.length(); @@ -392,10 +410,10 @@ void FindReplaceBar::_update_matches_label() { if (results_count == 0) { matches_label->set_text("No match"); - } else if (results_count == 1) { - matches_label->set_text(vformat(TTR("%d match"), results_count)); + } else if (results_count_to_current == -1) { + matches_label->set_text(vformat(TTRN("%d match", "%d matches", results_count), results_count)); } else { - matches_label->set_text(vformat(TTR("%d of %d matches"), results_count_to_current, results_count)); + matches_label->set_text(vformat(TTRN("%d of %d match", "%d of %d matches", results_count), results_count_to_current, results_count)); } } } @@ -417,6 +435,10 @@ bool FindReplaceBar::search_current() { } bool FindReplaceBar::search_prev() { + if (is_selection_only() && !replace_all_mode) { + return false; + } + if (!is_visible()) { popup_search(true); } @@ -435,9 +457,6 @@ bool FindReplaceBar::search_prev() { int line, col; _get_search_from(line, col); - if (text_editor->has_selection(0)) { - col--; // Skip currently selected word. - } col -= text.length(); if (col < 0) { @@ -452,17 +471,15 @@ bool FindReplaceBar::search_prev() { } bool FindReplaceBar::search_next() { + if (is_selection_only() && !replace_all_mode) { + return false; + } + if (!is_visible()) { popup_search(true); } flags = 0; - String text; - if (replace_all_mode) { - text = get_replace_text(); - } else { - text = get_search_text(); - } if (is_whole_words()) { flags |= TextEdit::SEARCH_WHOLE_WORDS; @@ -472,18 +489,7 @@ bool FindReplaceBar::search_next() { } int line, col; - _get_search_from(line, col); - - if (line == result_line && col == result_col) { - col += text.length(); - if (col > text_editor->get_line(line).length()) { - line += 1; - if (line >= text_editor->get_line_count()) { - line = 0; - } - col = 0; - } - } + _get_search_from(line, col, true); return _search(flags, line, col); } @@ -513,8 +519,10 @@ void FindReplaceBar::_show_search(bool p_focus_replace, bool p_show_only) { search_text->call_deferred(SNAME("grab_focus")); } - if (text_editor->has_selection(0) && !selection_only->is_pressed()) { + if (text_editor->has_selection(0) && !is_selection_only()) { search_text->set_text(text_editor->get_selected_text(0)); + result_line = text_editor->get_selection_from_line(); + result_col = text_editor->get_selection_from_column(); } if (!get_search_text().is_empty()) { @@ -538,6 +546,7 @@ void FindReplaceBar::popup_search(bool p_show_only) { replace_text->hide(); hbc_button_replace->hide(); hbc_option_replace->hide(); + selection_only->set_pressed(false); _show_search(false, p_show_only); } diff --git a/editor/code_editor.h b/editor/code_editor.h index c3279e8764..ded7518287 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -92,7 +92,7 @@ class FindReplaceBar : public HBoxContainer { bool replace_all_mode = false; bool preserve_cursor = false; - void _get_search_from(int &r_line, int &r_col); + void _get_search_from(int &r_line, int &r_col, bool p_is_searching_next = false); void _update_results_count(); void _update_matches_label(); diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 258ce434f6..2105a101d8 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -160,6 +160,9 @@ void ConnectDialog::_tree_node_selected() { } dst_path = source->get_path_to(current); + if (!edit_mode) { + set_dst_method(generate_method_callback_name(source, signal, current)); + } _update_ok_enabled(); } @@ -205,6 +208,45 @@ void ConnectDialog::_remove_bind() { cdbinds->params.remove_at(idx); cdbinds->notify_changed(); } +/* + * Automatically generates a name for the callback method. + */ +StringName ConnectDialog::generate_method_callback_name(Node *p_source, String p_signal_name, Node *p_target) { + String node_name = p_source->get_name(); + for (int i = 0; i < node_name.length(); i++) { // TODO: Regex filter may be cleaner. + char32_t c = node_name[i]; + if (!is_ascii_identifier_char(c)) { + if (c == ' ') { + // Replace spaces with underlines. + c = '_'; + } else { + // Remove any other characters. + node_name.remove_at(i); + i--; + continue; + } + } + node_name[i] = c; + } + + Dictionary subst; + subst["NodeName"] = node_name.to_pascal_case(); + subst["nodeName"] = node_name.to_camel_case(); + subst["node_name"] = node_name.to_snake_case(); + + subst["SignalName"] = p_signal_name.to_pascal_case(); + subst["signalName"] = p_signal_name.to_camel_case(); + subst["signal_name"] = p_signal_name.to_snake_case(); + + String dst_method; + if (p_source == p_target) { + dst_method = String(EDITOR_GET("interface/editors/default_signal_callback_to_self_name")).format(subst); + } else { + dst_method = String(EDITOR_GET("interface/editors/default_signal_callback_name")).format(subst); + } + + return dst_method; +} /* * Enables or disables the connect button. The connect button is enabled if a @@ -371,6 +413,7 @@ void ConnectDialog::popup_dialog(const String &p_for_signal) { first_popup = false; _advanced_pressed(); } + popup_centered(); } @@ -743,43 +786,17 @@ bool ConnectionsDock::_is_item_signal(TreeItem &p_item) { void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) { String signal_name = p_item.get_metadata(0).operator Dictionary()["name"]; const String &signal_name_ref = signal_name; - String node_name = selected_node->get_name(); - for (int i = 0; i < node_name.length(); i++) { // TODO: Regex filter may be cleaner. - char32_t c = node_name[i]; - if (!is_ascii_identifier_char(c)) { - if (c == ' ') { - // Replace spaces with underlines. - c = '_'; - } else { - // Remove any other characters. - node_name.remove_at(i); - i--; - continue; - } - } - node_name[i] = c; - } Node *dst_node = selected_node->get_owner() ? selected_node->get_owner() : selected_node; if (!dst_node || dst_node->get_script().is_null()) { dst_node = _find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root()); } - Dictionary subst; - subst["NodeName"] = node_name.to_pascal_case(); - subst["nodeName"] = node_name.to_camel_case(); - subst["node_name"] = node_name.to_snake_case(); - subst["SignalName"] = signal_name.to_pascal_case(); - subst["signalName"] = signal_name.to_camel_case(); - subst["signal_name"] = signal_name.to_snake_case(); - - String dst_method = String(EDITOR_GET("interface/editors/default_signal_callback_name")).format(subst); - ConnectDialog::ConnectionData cd; cd.source = selected_node; cd.signal = StringName(signal_name_ref); cd.target = dst_node; - cd.method = StringName(dst_method); + cd.method = ConnectDialog::generate_method_callback_name(cd.source, signal_name, cd.target); connect_dialog->popup_dialog(signal_name_ref); connect_dialog->init(cd); connect_dialog->set_title(TTR("Connect a Signal to a Method")); @@ -1187,6 +1204,7 @@ ConnectionsDock::ConnectionsDock() { add_theme_constant_override("separation", 3 * EDSCALE); EDITOR_DEF("interface/editors/default_signal_callback_name", "_on_{node_name}_{signal_name}"); + EDITOR_DEF("interface/editors/default_signal_callback_to_self_name", "_on_{signal_name}"); } ConnectionsDock::~ConnectionsDock() { diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h index db2f855617..16a60306aa 100644 --- a/editor/connections_dialog.h +++ b/editor/connections_dialog.h @@ -144,6 +144,7 @@ protected: static void _bind_methods(); public: + static StringName generate_method_callback_name(Node *p_source, String p_signal_name, Node *p_target); Node *get_source() const; StringName get_signal_name() const; NodePath get_dst_path() const; diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index 809c0c2cff..ae8752eb6d 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -656,7 +656,7 @@ int DebugAdapterProtocol::parse_variant(const Variant &p_var) { bool DebugAdapterProtocol::process_message(const String &p_text) { JSON json; - ERR_FAIL_COND_V_MSG(json.parse(p_text) != OK, true, "Mal-formed message!"); + ERR_FAIL_COND_V_MSG(json.parse(p_text) != OK, true, "Malformed message!"); Dictionary params = json.get_data(); bool completed = true; diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp index 0f0ab4a339..7fd27692b0 100644 --- a/editor/editor_build_profile.cpp +++ b/editor/editor_build_profile.cpp @@ -255,8 +255,7 @@ Error EditorBuildProfile::save_to_file(const String &p_path) { Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); - JSON json; - String text = json.stringify(data, "\t"); + String text = JSON::stringify(data, "\t"); f->store_string(text); return OK; } diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 7aca8fde6f..bf50efc4f9 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -5909,7 +5909,7 @@ void EditorNode::_update_renderer_color() { if (renderer->get_text() == "gl_compatibility") { renderer->add_theme_color_override("font_color", Color::hex(0x5586a4ff)); } else if (renderer->get_text() == "forward_plus" || renderer->get_text() == "mobile") { - renderer->add_theme_color_override("font_color", theme_base->get_theme_color(SNAME("vulkan_color"), SNAME("Editor"))); + renderer->add_theme_color_override("font_color", theme_base->get_theme_color(SNAME("highend_color"), SNAME("Editor"))); } } @@ -6181,10 +6181,17 @@ EditorNode::EditorNode() { // Define a minimum window size to prevent UI elements from overlapping or being cut off. DisplayServer::get_singleton()->window_set_min_size(Size2(1024, 600) * EDSCALE); - ResourceLoader::set_abort_on_missing_resources(false); FileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files")); EditorFileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files")); EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EditorSettings::get_singleton()->get("filesystem/file_dialog/display_mode").operator int()); + + int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons"); + if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer. + // Swap on means OK first. + AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2); + } + + ResourceLoader::set_abort_on_missing_resources(false); ResourceLoader::set_error_notify_func(this, _load_error_notify); ResourceLoader::set_dependency_error_notify_func(this, _dependency_error_report); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 0c01fcb869..5bdfd8d377 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -438,6 +438,9 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/single_window_mode", false, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) _initial_set("interface/editor/mouse_extra_buttons_navigate_history", true); _initial_set("interface/editor/save_each_scene_on_quit", true); // Regression + EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/accept_dialog_cancel_ok_buttons", 0, + vformat("Auto (%s),Cancel First,OK First", DisplayServer::get_singleton()->get_swap_cancel_ok() ? "OK First" : "Cancel First"), + PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED); #ifdef DEV_ENABLED EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/show_internal_errors_in_toast_notifications", 0, "Auto (Enabled),Enabled,Disabled") #else diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index 27ac57216a..1d9e320be1 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -564,9 +564,9 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_color("readonly_color", "Editor", readonly_color); if (!dark_theme) { - theme->set_color("vulkan_color", "Editor", Color::hex(0xad1128ff)); + theme->set_color("highend_color", "Editor", Color::hex(0xad1128ff)); } else { - theme->set_color("vulkan_color", "Editor", Color(1.0, 0.0, 0.0)); + theme->set_color("highend_color", "Editor", Color(1.0, 0.0, 0.0)); } const int thumb_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size"); theme->set_constant("scale", "Editor", EDSCALE); diff --git a/editor/groups_editor.cpp b/editor/groups_editor.cpp index dac86acae4..f11e328087 100644 --- a/editor/groups_editor.cpp +++ b/editor/groups_editor.cpp @@ -39,6 +39,24 @@ #include "scene/gui/label.h" #include "scene/resources/packed_scene.h" +static bool can_edit(Node *p_node, String p_group) { + Node *n = p_node; + bool can_edit = true; + while (n) { + Ref<SceneState> ss = (n == EditorNode::get_singleton()->get_edited_scene()) ? n->get_scene_inherited_state() : n->get_scene_instance_state(); + if (ss.is_valid()) { + int path = ss->find_node_by_path(n->get_path_to(p_node)); + if (path != -1) { + if (ss->is_node_in_group(path, p_group)) { + can_edit = false; + } + } + } + n = n->get_owner(); + } + return can_edit; +} + void GroupDialog::_group_selected() { nodes_to_add->clear(); add_node_root = nodes_to_add->create_item(); @@ -94,7 +112,7 @@ void GroupDialog::_load_nodes(Node *p_current) { Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(p_current, "Node"); node->set_icon(0, icon); - if (!_can_edit(p_current, selected_group)) { + if (!can_edit(p_current, selected_group)) { node->set_selectable(0, false); node->set_custom_color(0, groups->get_theme_color(SNAME("disabled_font_color"), SNAME("Editor"))); } @@ -105,24 +123,6 @@ void GroupDialog::_load_nodes(Node *p_current) { } } -bool GroupDialog::_can_edit(Node *p_node, String p_group) { - Node *n = p_node; - bool can_edit = true; - while (n) { - Ref<SceneState> ss = (n == EditorNode::get_singleton()->get_edited_scene()) ? n->get_scene_inherited_state() : n->get_scene_instance_state(); - if (ss.is_valid()) { - int path = ss->find_node_by_path(n->get_path_to(p_node)); - if (path != -1) { - if (ss->is_node_in_group(path, p_group)) { - can_edit = false; - } - } - } - n = n->get_owner(); - } - return can_edit; -} - void GroupDialog::_add_pressed() { TreeItem *selected = nodes_to_add->get_next_selected(nullptr); @@ -218,19 +218,14 @@ void GroupDialog::_add_group_text_changed(const String &p_new_text) { } void GroupDialog::_group_renamed() { - TreeItem *renamed_group = groups->get_edited(); + TreeItem *renamed_group = groups->get_selected(); if (!renamed_group) { return; } const String name = renamed_group->get_text(0).strip_edges(); - for (TreeItem *E = groups_root->get_first_child(); E; E = E->get_next()) { - if (E != renamed_group && E->get_text(0) == name) { - renamed_group->set_text(0, selected_group); - error->set_text(TTR("Group name already exists.")); - error->popup_centered(); - return; - } + if (name == selected_group) { + return; } if (name.is_empty()) { @@ -240,6 +235,15 @@ void GroupDialog::_group_renamed() { return; } + for (TreeItem *E = groups_root->get_first_child(); E; E = E->get_next()) { + if (E != renamed_group && E->get_text(0) == name) { + renamed_group->set_text(0, selected_group); + error->set_text(TTR("Group name already exists.")); + error->popup_centered(); + return; + } + } + renamed_group->set_text(0, name); // Spaces trimmed. undo_redo->create_action(TTR("Rename Group")); @@ -248,7 +252,7 @@ void GroupDialog::_group_renamed() { scene_tree->get_nodes_in_group(selected_group, &nodes); bool removed_all = true; for (Node *node : nodes) { - if (_can_edit(node, selected_group)) { + if (can_edit(node, selected_group)) { undo_redo->add_do_method(node, "remove_from_group", selected_group); undo_redo->add_undo_method(node, "remove_from_group", name); undo_redo->add_do_method(node, "add_to_group", name, true); @@ -324,7 +328,7 @@ void GroupDialog::_modify_group_pressed(Object *p_item, int p_column, int p_id, scene_tree->get_nodes_in_group(name, &nodes); bool removed_all = true; for (Node *E : nodes) { - if (_can_edit(E, name)) { + if (can_edit(E, name)) { undo_redo->add_do_method(E, "remove_from_group", name); undo_redo->add_undo_method(E, "add_to_group", name, true); } else { @@ -571,7 +575,7 @@ GroupDialog::GroupDialog() { set_title(TTR("Group Editor")); - error = memnew(ConfirmationDialog); + error = memnew(AcceptDialog); add_child(error); error->set_ok_button_text(TTR("Close")); @@ -584,14 +588,12 @@ void GroupsEditor::_add_group(const String &p_group) { if (!node) { return; } - const String name = group_name->get_text().strip_edges(); - if (name.is_empty()) { - return; - } group_name->clear(); if (node->is_in_group(name)) { + error->set_text(TTR("Group name already exists.")); + error->popup_centered(); return; } @@ -609,6 +611,65 @@ void GroupsEditor::_add_group(const String &p_group) { undo_redo->commit_action(); } +void GroupsEditor::_group_selected() { + if (!tree->is_anything_selected()) { + return; + } + selected_group = tree->get_selected()->get_text(0); +} + +void GroupsEditor::_group_renamed() { + if (!node || !can_edit(node, selected_group)) { + return; + } + + TreeItem *ti = tree->get_selected(); + if (!ti) { + return; + } + + const String name = ti->get_text(0).strip_edges(); + if (name == selected_group) { + return; + } + + if (name.is_empty()) { + ti->set_text(0, selected_group); + error->set_text(TTR("Invalid group name.")); + error->popup_centered(); + return; + } + + for (TreeItem *E = groups_root->get_first_child(); E; E = E->get_next()) { + if (E != ti && E->get_text(0) == name) { + ti->set_text(0, selected_group); + error->set_text(TTR("Group name already exists.")); + error->popup_centered(); + return; + } + } + + ti->set_text(0, name); // Spaces trimmed. + + undo_redo->create_action(TTR("Rename Group")); + + undo_redo->add_do_method(node, "remove_from_group", selected_group); + undo_redo->add_undo_method(node, "remove_from_group", name); + undo_redo->add_do_method(node, "add_to_group", name, true); + undo_redo->add_undo_method(node, "add_to_group", selected_group, true); + + undo_redo->add_do_method(this, "_group_selected"); + undo_redo->add_undo_method(this, "_group_selected"); + undo_redo->add_do_method(this, "update_tree"); + undo_redo->add_undo_method(this, "update_tree"); + + // To force redraw of scene tree. + undo_redo->add_do_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree"); + undo_redo->add_undo_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree"); + + undo_redo->commit_action(); +} + void GroupsEditor::_modify_group(Object *p_item, int p_column, int p_id, MouseButton p_button) { if (p_button != MouseButton::LEFT) { return; @@ -624,7 +685,7 @@ void GroupsEditor::_modify_group(Object *p_item, int p_column, int p_id, MouseBu } switch (p_id) { case DELETE_GROUP: { - String name = ti->get_text(0); + const String name = ti->get_text(0); undo_redo->create_action(TTR("Remove from Group")); undo_redo->add_do_method(node, "remove_from_group", name); @@ -666,6 +727,7 @@ void GroupsEditor::update_tree() { groups.sort_custom<_GroupInfoComparator>(); TreeItem *root = tree->create_item(); + groups_root = root; for (const GroupInfo &gi : groups) { if (!gi.persistent) { @@ -692,6 +754,7 @@ void GroupsEditor::update_tree() { TreeItem *item = tree->create_item(root); item->set_text(0, gi.name); + item->set_editable(0, true); if (can_be_deleted) { item->add_button(0, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")), DELETE_GROUP); item->add_button(0, get_theme_icon(SNAME("ActionCopy"), SNAME("EditorIcons")), COPY_GROUP); @@ -717,6 +780,7 @@ void GroupsEditor::_show_group_dialog() { void GroupsEditor::_bind_methods() { ClassDB::bind_method("update_tree", &GroupsEditor::update_tree); + ClassDB::bind_method("_group_selected", &GroupsEditor::_group_selected); } GroupsEditor::GroupsEditor() { @@ -749,13 +813,21 @@ GroupsEditor::GroupsEditor() { add->connect("pressed", callable_mp(this, &GroupsEditor::_add_group).bind(String())); tree = memnew(Tree); + vbc->add_child(tree); tree->set_hide_root(true); + tree->set_allow_reselect(true); + tree->set_allow_rmb_select(true); tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); - vbc->add_child(tree); + tree->connect("item_selected", callable_mp(this, &GroupsEditor::_group_selected)); tree->connect("button_clicked", callable_mp(this, &GroupsEditor::_modify_group)); + tree->connect("item_edited", callable_mp(this, &GroupsEditor::_group_renamed)); tree->add_theme_constant_override("draw_guides", 1); add_theme_constant_override("separation", 3 * EDSCALE); + error = memnew(AcceptDialog); + add_child(error); + error->get_ok_button()->set_text(TTR("Close")); + _group_name_changed(""); } diff --git a/editor/groups_editor.h b/editor/groups_editor.h index 8bbea4e652..5d012a3501 100644 --- a/editor/groups_editor.h +++ b/editor/groups_editor.h @@ -44,7 +44,7 @@ class EditorUndoRedoManager; class GroupDialog : public AcceptDialog { GDCLASS(GroupDialog, AcceptDialog); - ConfirmationDialog *error = nullptr; + AcceptDialog *error = nullptr; SceneTree *scene_tree = nullptr; TreeItem *groups_root = nullptr; @@ -88,8 +88,6 @@ class GroupDialog : public AcceptDialog { void _modify_group_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button); void _delete_group_item(const String &p_name); - bool _can_edit(Node *p_node, String p_group); - void _load_groups(Node *p_current); void _load_nodes(Node *p_current); @@ -113,8 +111,10 @@ class GroupsEditor : public VBoxContainer { GDCLASS(GroupsEditor, VBoxContainer); Node *node = nullptr; + TreeItem *groups_root = nullptr; GroupDialog *group_dialog = nullptr; + AcceptDialog *error = nullptr; LineEdit *group_name = nullptr; Button *add = nullptr; @@ -122,11 +122,16 @@ class GroupsEditor : public VBoxContainer { Ref<EditorUndoRedoManager> undo_redo; + String selected_group; + void update_tree(); void _add_group(const String &p_group = ""); void _modify_group(Object *p_item, int p_column, int p_id, MouseButton p_button); void _group_name_changed(const String &p_new_text); + void _group_selected(); + void _group_renamed(); + void _show_group_dialog(); protected: diff --git a/editor/icons/BoneMapHumanBody.svg b/editor/icons/BoneMapHumanBody.svg index 2c2c5db1f6..8674157aaa 100644 --- a/editor/icons/BoneMapHumanBody.svg +++ b/editor/icons/BoneMapHumanBody.svg @@ -1 +1,26 @@ -<svg enable-background="new 0 0 1024 1024" height="1024" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h1024v1024h-1024z" fill="#3f3f3f"/><path d="m926.5 217.162c-11.5-2-26.03 4.547-37.5 6.5-15.723 2.678-25.238 3.24-33.333 5.167-1.227.292-3.103.763-5.792.958 0 0-.019.16-.052.437-36.819.994-106.823-6.062-138.156-2.062-23.816 3.041-86.334-5.667-105.667-6-13.911-.239-59.292-4.583-71.75-2.5-.667-4.083-1.5-10.75.95-17.468 14.881-7.246 27.229-21.569 35.341-38.467.922 4.424 6.252 4.929 12.459-14.231 5.662-17.478 2.324-22.254-2.313-22.525.172-2.056.279-4.105.313-6.142.788-48.041-15-78.667-69-78.667s-69.787 30.626-69 78.667c.033 2.036.141 4.086.313 6.142-4.637.271-7.975 5.048-2.313 22.525 6.207 19.16 11.537 18.655 12.459 14.231 8.113 16.897 20.461 31.221 35.342 38.467 2.449 6.718 1.617 13.385.949 17.468-12.457-2.083-57.838 2.261-71.75 2.5-19.332.333-81.85 9.041-105.666 6-31.333-4-101.337 3.056-138.156 2.062-.033-.276-.053-.437-.053-.437-2.689-.195-4.564-.666-5.791-.958-8.096-1.927-17.611-2.489-33.334-5.167-11.469-1.953-26-8.5-37.5-6.5-3.367.586 6 9.834 15.5 12.334 13.635 3.588 25.25 10.666 36 13.166-2.25 3.75-15.59 7.063-23 12-5.336 3.557 6.5 6.5 12 5 20.842-5.684 22.973.389 37.514-9.019 30.078 4.078 102.537 20.514 122.154 14.186 12.457-4.018 100.332 7.083 142.332 5.833 6.039-.18 1.656 65.563 2 73.5 3 69-16.842 133.135-18.666 169.667-1.92 38.42-3.42 57.919 7.666 131.333 6.967 46.126-2.521 82.079-2 94 6 137 29 172 4 221-14 27.44 67.449 26.958 65 9-3.012-22.092-12.666-22.333-10.666-46.333 1.896-22.768 16.049-151.298 8.666-206.667-2-15 0-26 2-66 2.355-47.101 7-88 14-123 7 35 11.645 75.899 14 123 2 40 4 51 2 66-7.383 55.369 6.77 183.899 8.667 206.667 2 24-7.654 24.241-10.667 46.333-2.449 17.958 79 18.44 65-9-25-49-2-84 4-221 .522-11.921-8.966-47.874-2-94 11.086-73.414 9.586-92.913 7.667-131.333-1.824-36.532-21.667-100.667-18.667-169.667.345-7.938-4.039-73.68 2-73.5 42 1.25 129.876-9.852 142.333-5.833 19.616 6.328 92.076-10.107 122.153-14.186 14.541 9.407 16.673 3.335 37.514 9.019 5.5 1.5 17.336-1.443 12-5-7.409-4.937-20.75-8.25-23-12 10.75-2.5 22.366-9.578 36.001-13.166 9.5-2.5 18.866-11.748 15.499-12.334z" fill="#b2b2b2"/></svg> +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" + y="0px" width="1024px" height="1024px" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve"> +<path fill="#3F3F3F" d="M0,0h1024v1024H0V0z"/> +<path fill="#B2B2B2" d="M512,536.162c7,35,11.645,66.898,14,114c2,40,4,51,2,66c-7.384,55.369,6.77,183.898,8.666,206.667 + c2,24-7.653,24.241-10.666,46.333c-2.449,17.958,79,18.439,65-9c-25-49-2-84,4-221c0.521-11.921-8.967-47.874-2-94 + c11.086-73.414,8.42-107.242,6.5-145.662c-1.245-31.973-1-56.963-9-138.963c-0.976-10.002,5.915-79.268,11.954-79.088 + c42,1.25,97.313-5.009,118.145-14.68c28.901,3.73,97.81-12.047,127.887-16.126c14.541,9.407,16.673,3.335,37.515,9.019 + c5.5,1.5,17.336-1.443,12-5c-7.409-4.937-20.75-8.25-23-12c10.75-2.5,22.365-9.578,36-13.166c9.5-2.5,18.866-11.748,15.5-12.334l0,0 + c-11.5-2-26.03,4.547-37.5,6.5c-15.724,2.678-25.238,3.24-33.334,5.167c-1.227,0.292-3.103,0.763-5.791,0.958 + c0,0-0.02,0.16-0.053,0.437c-36.818,0.994-80.322-9.724-130.31-5.569c-34.026-3.925-94.181-5.16-113.513-5.493 + c-13.911-0.239-59.293-2.583-71.75-0.5c-0.668-4.083-1.5-9.75,0.949-16.468c14.881-7.246,19.188-17.796,27.301-34.694 + c0.922,4.424,6.252,4.929,12.459-14.231c5.661-17.478,2.323-22.254-2.313-22.525c0.172-2.056,0.279-4.105,0.313-6.142 + C573.746,76.562,566,42.163,512,42.163s-61.746,34.399-60.959,82.44c0.034,2.037,0.142,4.086,0.313,6.142 + c-4.637,0.271-7.975,5.047-2.313,22.525c6.207,19.16,11.537,18.655,12.459,14.231c8.112,16.898,12.42,27.448,27.301,34.694 + c2.449,6.718,1.617,12.385,0.949,16.468c-12.457-2.083-57.839,0.261-71.75,0.5c-19.332,0.333-79.486,1.568-113.513,5.493 + c-49.987-4.155-93.491,6.563-130.31,5.569c-0.033-0.277-0.053-0.437-0.053-0.437c-2.688-0.195-4.564-0.666-5.791-0.958 + c-8.096-1.927-17.61-2.489-33.334-5.167c-11.47-1.953-26-8.5-37.5-6.5l0,0c-3.366,0.586,6,9.834,15.5,12.334 + c13.635,3.588,25.25,10.666,36,13.166c-2.25,3.75-15.591,7.063-23,12c-5.336,3.557,6.5,6.5,12,5 + c20.842-5.684,22.974,0.388,37.515-9.019c30.077,4.079,98.985,19.857,127.887,16.126c20.832,9.671,76.145,15.93,118.145,14.68 + c6.039-0.18,12.93,69.085,11.954,79.088c-8,82-7.755,106.99-9,138.963c-1.92,38.419-4.586,72.248,6.5,145.662 + c6.967,46.126-2.521,82.079-2,94c6,137,29,172,4,221c-14,27.439,67.449,26.958,65,9c-3.013-22.092-12.666-22.333-10.666-46.333 + c1.896-22.769,16.05-151.298,8.666-206.667c-2-15,0-26,2-66C500.356,603.061,505,571.162,512,536.162z"/> +</svg> diff --git a/editor/import/resource_importer_scene.cpp b/editor/import/resource_importer_scene.cpp index b5798a5784..756d61f712 100644 --- a/editor/import/resource_importer_scene.cpp +++ b/editor/import/resource_importer_scene.cpp @@ -953,43 +953,49 @@ Node *ResourceImporterScene::_pre_fix_animations(Node *p_node, Node *p_root, con if (Object::cast_to<AnimationPlayer>(p_node)) { AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(p_node); + List<StringName> anims; + ap->get_animation_list(&anims); - Array animation_clips; - { - int clip_count = node_settings["clips/amount"]; + for (const StringName &name : anims) { + Ref<Animation> anim = ap->get_animation(name); + Array animation_slices; - for (int i = 0; i < clip_count; i++) { - String name = node_settings["clip_" + itos(i + 1) + "/name"]; - int from_frame = node_settings["clip_" + itos(i + 1) + "/start_frame"]; - int end_frame = node_settings["clip_" + itos(i + 1) + "/end_frame"]; - Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)node_settings["clip_" + itos(i + 1) + "/loop_mode"]); - bool save_to_file = node_settings["clip_" + itos(i + 1) + "/save_to_file/enabled"]; - bool save_to_path = node_settings["clip_" + itos(i + 1) + "/save_to_file/path"]; - bool save_to_file_keep_custom = node_settings["clip_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"]; + if (p_animation_data.has(name)) { + Dictionary anim_settings = p_animation_data[name]; + int slices_count = anim_settings["slices/amount"]; + + for (int i = 0; i < slices_count; i++) { + String slice_name = anim_settings["slice_" + itos(i + 1) + "/name"]; + int from_frame = anim_settings["slice_" + itos(i + 1) + "/start_frame"]; + int end_frame = anim_settings["slice_" + itos(i + 1) + "/end_frame"]; + Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)anim_settings["slice_" + itos(i + 1) + "/loop_mode"]); + bool save_to_file = anim_settings["slice_" + itos(i + 1) + "/save_to_file/enabled"]; + bool save_to_path = anim_settings["slice_" + itos(i + 1) + "/save_to_file/path"]; + bool save_to_file_keep_custom = anim_settings["slice_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"]; + + animation_slices.push_back(slice_name); + animation_slices.push_back(from_frame / p_animation_fps); + animation_slices.push_back(end_frame / p_animation_fps); + animation_slices.push_back(loop_mode); + animation_slices.push_back(save_to_file); + animation_slices.push_back(save_to_path); + animation_slices.push_back(save_to_file_keep_custom); + } + } - animation_clips.push_back(name); - animation_clips.push_back(from_frame / p_animation_fps); - animation_clips.push_back(end_frame / p_animation_fps); - animation_clips.push_back(loop_mode); - animation_clips.push_back(save_to_file); - animation_clips.push_back(save_to_path); - animation_clips.push_back(save_to_file_keep_custom); + if (animation_slices.size() > 0) { + _create_slices(ap, anim, animation_slices, true); } } - if (animation_clips.size()) { - _create_clips(ap, animation_clips, true); - } else { - List<StringName> anims; - ap->get_animation_list(&anims); - AnimationImportTracks import_tracks_mode[TRACK_CHANNEL_MAX] = { - AnimationImportTracks(int(node_settings["import_tracks/position"])), - AnimationImportTracks(int(node_settings["import_tracks/rotation"])), - AnimationImportTracks(int(node_settings["import_tracks/scale"])) - }; - if (anims.size() > 1 && (import_tracks_mode[0] != ANIMATION_IMPORT_TRACKS_IF_PRESENT || import_tracks_mode[1] != ANIMATION_IMPORT_TRACKS_IF_PRESENT || import_tracks_mode[2] != ANIMATION_IMPORT_TRACKS_IF_PRESENT)) { - _optimize_track_usage(ap, import_tracks_mode); - } + AnimationImportTracks import_tracks_mode[TRACK_CHANNEL_MAX] = { + AnimationImportTracks(int(node_settings["import_tracks/position"])), + AnimationImportTracks(int(node_settings["import_tracks/rotation"])), + AnimationImportTracks(int(node_settings["import_tracks/scale"])) + }; + + if (anims.size() > 1 && (import_tracks_mode[0] != ANIMATION_IMPORT_TRACKS_IF_PRESENT || import_tracks_mode[1] != ANIMATION_IMPORT_TRACKS_IF_PRESENT || import_tracks_mode[2] != ANIMATION_IMPORT_TRACKS_IF_PRESENT)) { + _optimize_track_usage(ap, import_tracks_mode); } } @@ -1408,144 +1414,138 @@ Ref<Animation> ResourceImporterScene::_save_animation_to_file(Ref<Animation> ani return anim; } -void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_clips, bool p_bake_all) { - if (!anim->has_animation("default")) { - ERR_FAIL_COND_MSG(p_clips.size() > 0, "To create clips, animations must be named \"default\"."); - return; - } +void ResourceImporterScene::_create_slices(AnimationPlayer *ap, Ref<Animation> anim, const Array &p_slices, bool p_bake_all) { + Ref<AnimationLibrary> al = ap->get_animation_library(ap->find_animation_library(anim)); - Ref<Animation> default_anim = anim->get_animation("default"); - Ref<AnimationLibrary> al = anim->get_animation_library(anim->find_animation(default_anim)); - - for (int i = 0; i < p_clips.size(); i += 7) { - String name = p_clips[i]; - float from = p_clips[i + 1]; - float to = p_clips[i + 2]; - Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)p_clips[i + 3]); - bool save_to_file = p_clips[i + 4]; - String save_to_path = p_clips[i + 5]; - bool keep_current = p_clips[i + 6]; + for (int i = 0; i < p_slices.size(); i += 7) { + String name = p_slices[i]; + float from = p_slices[i + 1]; + float to = p_slices[i + 2]; + Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)p_slices[i + 3]); + bool save_to_file = p_slices[i + 4]; + String save_to_path = p_slices[i + 5]; + bool keep_current = p_slices[i + 6]; if (from >= to) { continue; } Ref<Animation> new_anim = memnew(Animation); - for (int j = 0; j < default_anim->get_track_count(); j++) { + for (int j = 0; j < anim->get_track_count(); j++) { List<float> keys; - int kc = default_anim->track_get_key_count(j); + int kc = anim->track_get_key_count(j); int dtrack = -1; for (int k = 0; k < kc; k++) { - float kt = default_anim->track_get_key_time(j, k); + float kt = anim->track_get_key_time(j, k); if (kt >= from && kt < to) { //found a key within range, so create track if (dtrack == -1) { - new_anim->add_track(default_anim->track_get_type(j)); + new_anim->add_track(anim->track_get_type(j)); dtrack = new_anim->get_track_count() - 1; - new_anim->track_set_path(dtrack, default_anim->track_get_path(j)); + new_anim->track_set_path(dtrack, anim->track_get_path(j)); if (kt > (from + 0.01) && k > 0) { - if (default_anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { + if (anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { Vector3 p; - default_anim->position_track_interpolate(j, from, &p); + anim->position_track_interpolate(j, from, &p); new_anim->position_track_insert_key(dtrack, 0, p); - } else if (default_anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { Quaternion r; - default_anim->rotation_track_interpolate(j, from, &r); + anim->rotation_track_interpolate(j, from, &r); new_anim->rotation_track_insert_key(dtrack, 0, r); - } else if (default_anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { Vector3 s; - default_anim->scale_track_interpolate(j, from, &s); + anim->scale_track_interpolate(j, from, &s); new_anim->scale_track_insert_key(dtrack, 0, s); - } else if (default_anim->track_get_type(j) == Animation::TYPE_VALUE) { - Variant var = default_anim->value_track_interpolate(j, from); + } else if (anim->track_get_type(j) == Animation::TYPE_VALUE) { + Variant var = anim->value_track_interpolate(j, from); new_anim->track_insert_key(dtrack, 0, var); - } else if (default_anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { + } else if (anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { float interp; - default_anim->blend_shape_track_interpolate(j, from, &interp); + anim->blend_shape_track_interpolate(j, from, &interp); new_anim->blend_shape_track_insert_key(dtrack, 0, interp); } } } - if (default_anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { + if (anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { Vector3 p; - default_anim->position_track_get_key(j, k, &p); + anim->position_track_get_key(j, k, &p); new_anim->position_track_insert_key(dtrack, kt - from, p); - } else if (default_anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { Quaternion r; - default_anim->rotation_track_get_key(j, k, &r); + anim->rotation_track_get_key(j, k, &r); new_anim->rotation_track_insert_key(dtrack, kt - from, r); - } else if (default_anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { Vector3 s; - default_anim->scale_track_get_key(j, k, &s); + anim->scale_track_get_key(j, k, &s); new_anim->scale_track_insert_key(dtrack, kt - from, s); - } else if (default_anim->track_get_type(j) == Animation::TYPE_VALUE) { - Variant var = default_anim->track_get_key_value(j, k); + } else if (anim->track_get_type(j) == Animation::TYPE_VALUE) { + Variant var = anim->track_get_key_value(j, k); new_anim->track_insert_key(dtrack, kt - from, var); - } else if (default_anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { + } else if (anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { float interp; - default_anim->blend_shape_track_get_key(j, k, &interp); + anim->blend_shape_track_get_key(j, k, &interp); new_anim->blend_shape_track_insert_key(dtrack, kt - from, interp); } } if (dtrack != -1 && kt >= to) { - if (default_anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { + if (anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { Vector3 p; - default_anim->position_track_interpolate(j, to, &p); + anim->position_track_interpolate(j, to, &p); new_anim->position_track_insert_key(dtrack, to - from, p); - } else if (default_anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { Quaternion r; - default_anim->rotation_track_interpolate(j, to, &r); + anim->rotation_track_interpolate(j, to, &r); new_anim->rotation_track_insert_key(dtrack, to - from, r); - } else if (default_anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { Vector3 s; - default_anim->scale_track_interpolate(j, to, &s); + anim->scale_track_interpolate(j, to, &s); new_anim->scale_track_insert_key(dtrack, to - from, s); - } else if (default_anim->track_get_type(j) == Animation::TYPE_VALUE) { - Variant var = default_anim->value_track_interpolate(j, to); + } else if (anim->track_get_type(j) == Animation::TYPE_VALUE) { + Variant var = anim->value_track_interpolate(j, to); new_anim->track_insert_key(dtrack, to - from, var); - } else if (default_anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { + } else if (anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { float interp; - default_anim->blend_shape_track_interpolate(j, to, &interp); + anim->blend_shape_track_interpolate(j, to, &interp); new_anim->blend_shape_track_insert_key(dtrack, to - from, interp); } } } if (dtrack == -1 && p_bake_all) { - new_anim->add_track(default_anim->track_get_type(j)); + new_anim->add_track(anim->track_get_type(j)); dtrack = new_anim->get_track_count() - 1; - new_anim->track_set_path(dtrack, default_anim->track_get_path(j)); - if (default_anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { + new_anim->track_set_path(dtrack, anim->track_get_path(j)); + if (anim->track_get_type(j) == Animation::TYPE_POSITION_3D) { Vector3 p; - default_anim->position_track_interpolate(j, from, &p); + anim->position_track_interpolate(j, from, &p); new_anim->position_track_insert_key(dtrack, 0, p); - default_anim->position_track_interpolate(j, to, &p); + anim->position_track_interpolate(j, to, &p); new_anim->position_track_insert_key(dtrack, to - from, p); - } else if (default_anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_ROTATION_3D) { Quaternion r; - default_anim->rotation_track_interpolate(j, from, &r); + anim->rotation_track_interpolate(j, from, &r); new_anim->rotation_track_insert_key(dtrack, 0, r); - default_anim->rotation_track_interpolate(j, to, &r); + anim->rotation_track_interpolate(j, to, &r); new_anim->rotation_track_insert_key(dtrack, to - from, r); - } else if (default_anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { + } else if (anim->track_get_type(j) == Animation::TYPE_SCALE_3D) { Vector3 s; - default_anim->scale_track_interpolate(j, from, &s); + anim->scale_track_interpolate(j, from, &s); new_anim->scale_track_insert_key(dtrack, 0, s); - default_anim->scale_track_interpolate(j, to, &s); + anim->scale_track_interpolate(j, to, &s); new_anim->scale_track_insert_key(dtrack, to - from, s); - } else if (default_anim->track_get_type(j) == Animation::TYPE_VALUE) { - Variant var = default_anim->value_track_interpolate(j, from); + } else if (anim->track_get_type(j) == Animation::TYPE_VALUE) { + Variant var = anim->value_track_interpolate(j, from); new_anim->track_insert_key(dtrack, 0, var); - Variant to_var = default_anim->value_track_interpolate(j, to); + Variant to_var = anim->value_track_interpolate(j, to); new_anim->track_insert_key(dtrack, to - from, to_var); - } else if (default_anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { + } else if (anim->track_get_type(j) == Animation::TYPE_BLEND_SHAPE) { float interp; - default_anim->blend_shape_track_interpolate(j, from, &interp); + anim->blend_shape_track_interpolate(j, from, &interp); new_anim->blend_shape_track_insert_key(dtrack, 0, interp); - default_anim->blend_shape_track_interpolate(j, to, &interp); + anim->blend_shape_track_interpolate(j, to, &interp); new_anim->blend_shape_track_insert_key(dtrack, to - from, interp); } } @@ -1562,7 +1562,7 @@ void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_ } } - al->remove_animation("default"); // Remove default (no longer needed). + al->remove_animation(ap->find_animation(anim)); // Remove original animation (no longer needed). } void ResourceImporterScene::_optimize_animations(AnimationPlayer *anim, float p_max_vel_error, float p_max_ang_error, int p_prc_error) { @@ -1642,6 +1642,17 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "save_to_file/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "save_to_file/path", PROPERTY_HINT_SAVE_FILE, "*.res,*.tres"), "")); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "save_to_file/keep_custom_tracks"), "")); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slices/amount", PROPERTY_HINT_RANGE, "0,256,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); + + for (int i = 0; i < 256; i++) { + r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/name"), "")); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/start_frame"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/end_frame"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/loop_mode", PROPERTY_HINT_ENUM, "None,Linear,Pingpong"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/save_to_file/path", PROPERTY_HINT_SAVE_FILE, ".res,*.tres"), "")); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"), false)); + } } break; case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); @@ -1654,17 +1665,6 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/position", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/rotation", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/scale", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slices/amount", PROPERTY_HINT_RANGE, "0,256,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); - - for (int i = 0; i < 256; i++) { - r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/name"), "")); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/start_frame"), 0)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/end_frame"), 0)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/loop_mode", PROPERTY_HINT_ENUM, "None,Linear,Pingpong"), 0)); - r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); - r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/save_to_file/path", PROPERTY_HINT_SAVE_FILE, ".res,*.tres"), "")); - r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"), false)); - } } break; case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); @@ -1767,6 +1767,13 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor if (p_option == "save_to_file/path" || p_option == "save_to_file/keep_custom_tracks") { return p_options["save_to_file/enabled"]; } + if (p_option.begins_with("slice_")) { + int max_slice = p_options["slices/amount"]; + int slice = p_option.get_slice("_", 1).to_int() - 1; + if (slice >= max_slice) { + return false; + } + } } break; case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: { if (p_option.begins_with("optimizer/") && p_option != "optimizer/enabled" && !bool(p_options["optimizer/enabled"])) { @@ -1775,14 +1782,6 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor if (p_option.begins_with("compression/") && p_option != "compression/enabled" && !bool(p_options["compression/enabled"])) { return false; } - - if (p_option.begins_with("slice_")) { - int max_slice = p_options["slices/amount"]; - int slice = p_option.get_slice("_", 1).to_int() - 1; - if (slice >= max_slice) { - return false; - } - } } break; case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: { const bool use_retarget = p_options["retarget/bone_map"].get_validated_object() != nullptr; diff --git a/editor/import/resource_importer_scene.h b/editor/import/resource_importer_scene.h index 386519bc59..498e9f2964 100644 --- a/editor/import/resource_importer_scene.h +++ b/editor/import/resource_importer_scene.h @@ -283,7 +283,7 @@ public: Node *_post_fix_animations(Node *p_node, Node *p_root, const Dictionary &p_node_data, const Dictionary &p_animation_data, float p_animation_fps); Ref<Animation> _save_animation_to_file(Ref<Animation> anim, bool p_save_to_file, String p_save_to_path, bool p_keep_custom_tracks); - void _create_clips(AnimationPlayer *anim, const Array &p_clips, bool p_bake_all); + void _create_slices(AnimationPlayer *ap, Ref<Animation> anim, const Array &p_clips, bool p_bake_all); void _optimize_animations(AnimationPlayer *anim, float p_max_vel_error, float p_max_ang_error, int p_prc_error); void _compress_animations(AnimationPlayer *anim, int p_page_size_kb); diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index 7a21397910..4d3ffdc12b 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -2400,6 +2400,9 @@ void Node3DEditorViewport::_project_settings_changed() { const bool use_taa = GLOBAL_GET("rendering/anti_aliasing/quality/use_taa"); viewport->set_use_taa(use_taa); + const bool transparent_background = GLOBAL_GET("rendering/transparent_background"); + viewport->set_transparent_background(transparent_background); + const bool use_debanding = GLOBAL_GET("rendering/anti_aliasing/quality/use_debanding"); viewport->set_use_debanding(use_debanding); @@ -4775,7 +4778,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p view_menu->set_disable_shortcuts(true); // TODO: Re-evaluate with new OpenGL3 renderer, and implement. - //if (OS::get_singleton()->get_current_video_driver() == OS::VIDEO_DRIVER_GLES2) { + //if (OS::get_singleton()->get_current_video_driver() == OS::RENDERING_DRIVER_OPENGL3) { if (false) { // Alternate display modes only work when using the Vulkan renderer; make this explicit. const int normal_idx = view_menu->get_popup()->get_item_index(VIEW_DISPLAY_NORMAL); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index d87513bfe1..8355f64fe5 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -3707,7 +3707,7 @@ void VisualShaderEditor::_notification(int p_what) { [[fallthrough]]; } case NOTIFICATION_THEME_CHANGED: { - highend_label->set_modulate(get_theme_color(SNAME("vulkan_color"), SNAME("Editor"))); + highend_label->set_modulate(get_theme_color(SNAME("highend_color"), SNAME("Editor"))); node_filter->set_right_icon(Control::get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); diff --git a/editor/project_converter_3_to_4.cpp b/editor/project_converter_3_to_4.cpp index e702ba7020..1a2c670aef 100644 --- a/editor/project_converter_3_to_4.cpp +++ b/editor/project_converter_3_to_4.cpp @@ -302,6 +302,7 @@ static const char *gdscript_function_renames[][2] = { { "get_cull_mask_bit", "get_cull_mask_value" }, // Camera3D { "get_cursor_position", "get_caret_column" }, // LineEdit { "get_d", "get_distance" }, // LineShape2D + { "get_depth_bias_enable", "get_depth_bias_enabled" }, // RDPipelineRasterizationState { "get_drag_data", "_get_drag_data" }, // Control { "get_drag_data_fw", "_get_drag_data_fw" }, // ScriptEditor { "get_editor_viewport", "get_editor_main_screen" }, // EditorPlugin @@ -358,6 +359,7 @@ static const char *gdscript_function_renames[][2] = { { "get_render_targetsize", "get_render_target_size" }, // XRInterface { "get_resource_type", "_get_resource_type" }, // ResourceFormatLoader { "get_result", "get_data" }, //JSON + { "get_reverb_bus", "set_reverb_bus_name" }, // Area3D { "get_rpc_sender_id", "get_remote_sender_id" }, // Multiplayer API { "get_save_extension", "_get_save_extension" }, // EditorImportPlugin { "get_scancode", "get_keycode" }, // InputEventKey @@ -495,6 +497,7 @@ static const char *gdscript_function_renames[][2] = { { "set_cull_mask_bit", "set_cull_mask_value" }, // Camera3D { "set_cursor_position", "set_caret_column" }, // LineEdit { "set_d", "set_distance" }, // WorldMarginShape2D + { "set_depth_bias_enable", "set_depth_bias_enabled" }, // RDPipelineRasterizationState { "set_doubleclick", "set_double_click" }, // InputEventMouseButton { "set_draw_red", "set_draw_warning" }, // EditorProperty { "set_enabled_focus_mode", "set_focus_mode" }, // BaseButton @@ -525,9 +528,11 @@ static const char *gdscript_function_renames[][2] = { { "set_oneshot", "set_one_shot" }, // AnimatedTexture { "set_pause_mode", "set_process_mode" }, // Node { "set_physical_scancode", "set_physical_keycode" }, // InputEventKey + { "set_proximity_fade", "set_proximity_fade_enabled" }, // Material { "set_refuse_new_network_connections", "set_refuse_new_connections" }, // Multiplayer API { "set_region", "set_region_enabled" }, // Sprite2D, Sprite broke AtlasTexture { "set_region_filter_clip", "set_region_filter_clip_enabled" }, // Sprite2D + { "set_reverb_bus", "set_reverb_bus_name" }, // Area3D { "set_rotate", "set_rotates" }, // PathFollow2D { "set_scancode", "set_keycode" }, // InputEventKey { "set_shift", "set_shift_pressed" }, // InputEventWithModifiers @@ -741,6 +746,7 @@ static const char *csharp_function_renames[][2] = { { "GetCullMaskBit", "GetCullMaskValue" }, // Camera3D { "GetCursorPosition", "GetCaretColumn" }, // LineEdit { "GetD", "GetDistance" }, // LineShape2D + { "GetDepthBiasEnable", "GetDepthBiasEnabled" }, // RDPipelineRasterizationState { "GetDragDataFw", "_GetDragDataFw" }, // ScriptEditor { "GetEditorViewport", "GetViewport" }, // EditorPlugin { "GetEnabledFocusMode", "GetFocusMode" }, // BaseButton @@ -794,6 +800,7 @@ static const char *csharp_function_renames[][2] = { { "GetRenderTargetsize", "GetRenderTargetSize" }, // XRInterface { "GetResourceType", "_GetResourceType" }, // ResourceFormatLoader { "GetResult", "GetData" }, //JSON + { "GetReverbBus", "GetReverbBusName" }, // Area3D { "GetRpcSenderId", "GetRemoteSenderId" }, // Multiplayer API { "GetSaveExtension", "_GetSaveExtension" }, // EditorImportPlugin { "GetScancode", "GetKeycode" }, // InputEventKey @@ -925,6 +932,7 @@ static const char *csharp_function_renames[][2] = { { "SetCullMaskBit", "SetCullMaskValue" }, // Camera3D { "SetCursorPosition", "SetCaretColumn" }, // LineEdit { "SetD", "SetDistance" }, // WorldMarginShape2D + { "SetDepthBiasEnable", "SetDepthBiasEnabled" }, // RDPipelineRasterizationState { "SetDoubleclick", "SetDoubleClick" }, // InputEventMouseButton { "SetEnabledFocusMode", "SetFocusMode" }, // BaseButton { "SetEndianSwap", "SetBigEndian" }, // File @@ -951,9 +959,11 @@ static const char *csharp_function_renames[][2] = { { "SetNetworkPeer", "SetMultiplayerPeer" }, // Multiplayer API { "SetOneshot", "SetOneShot" }, // AnimatedTexture { "SetPhysicalScancode", "SetPhysicalKeycode" }, // InputEventKey + { "SetProximityFade", "SetProximityFadeEnabled" }, // Material { "SetRefuseNewNetworkConnections", "SetRefuseNewConnections" }, // Multiplayer API { "SetRegion", "SetRegionEnabled" }, // Sprite2D, Sprite broke AtlasTexture { "SetRegionFilterClip", "SetRegionFilterClipEnabled" }, // Sprite2D + { "SetReverbBus", "SetReverbBusName" }, // Area3D { "SetRotate", "SetRotates" }, // PathFollow2D { "SetScancode", "SetKeycode" }, // InputEventKey { "SetShift", "SetShiftPressed" }, // InputEventWithModifiers @@ -1071,6 +1081,7 @@ static const char *gdscript_properties_renames[][2] = { { "close_v_ofs", "close_v_offset" }, // Theme { "commentfocus", "comment_focus" }, // Theme { "contacts_reported", "max_contacts_reported" }, // RigidBody + { "depth_bias_enable", "depth_bias_enabled" }, // RDPipelineRasterizationState { "drag_margin_bottom", "drag_bottom_margin" }, // Camera2D { "drag_margin_h_enabled", "drag_horizontal_enabled" }, // Camera2D { "drag_margin_left", "drag_left_margin" }, // Camera2D @@ -1113,6 +1124,7 @@ static const char *gdscript_properties_renames[][2] = { { "pause_mode", "process_mode" }, // Node { "physical_scancode", "physical_keycode" }, // InputEventKey { "popup_exclusive", "exclusive" }, // Window + { "proximity_fade_enable", "proximity_fade_enabled" }, // Material { "rect_position", "position" }, // Control { "rect_global_position", "global_position" }, // Control { "rect_size", "size" }, // Control @@ -1123,6 +1135,7 @@ static const char *gdscript_properties_renames[][2] = { { "rect_clip_content", "clip_contents" }, // Control { "refuse_new_network_connections", "refuse_new_connections" }, // MultiplayerAPI { "region_filter_clip", "region_filter_clip_enabled" }, // Sprite2D + { "reverb_bus_enable", "reverb_bus_enabled" }, // Area3D { "selectedframe", "selected_frame" }, // Theme { "size_override_stretch", "size_2d_override_stretch" }, // SubViewport { "slips_on_slope", "slide_on_slope" }, // SeparationRayShape2D @@ -1177,6 +1190,7 @@ static const char *csharp_properties_renames[][2] = { { "CloseHOfs", "CloseHOffset" }, // Theme { "CloseVOfs", "CloseVOffset" }, // Theme { "Commentfocus", "CommentFocus" }, // Theme + { "DepthBiasEnable", "DepthBiasEnabled" }, // RDPipelineRasterizationState { "DragMarginBottom", "DragBottomMargin" }, // Camera2D { "DragMarginHEnabled", "DragHorizontalEnabled" }, // Camera2D { "DragMarginLeft", "DragLeftMargin" }, // Camera2D @@ -1212,8 +1226,10 @@ static const char *csharp_properties_renames[][2] = { { "PauseMode", "ProcessMode" }, // Node { "PhysicalScancode", "PhysicalKeycode" }, // InputEventKey { "PopupExclusive", "Exclusive" }, // Window + { "ProximityFadeEnable", "ProximityFadeEnabled" }, // Material { "RefuseNewNetworkConnections", "RefuseNewConnections" }, // MultiplayerAPI { "RegionFilterClip", "RegionFilterClipEnabled" }, // Sprite2D + { "ReverbBusEnable", "ReverbBusEnabled" }, // Area3D { "Selectedframe", "SelectedFrame" }, // Theme { "SizeOverrideStretch", "Size2dOverrideStretch" }, // SubViewport { "SlipsOnSlope", "SlideOnSlope" }, // SeparationRayShape2D diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 05015528c6..5a1eedd8a2 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -143,7 +143,11 @@ private: install_status_rect->set_texture(new_icon); } - set_size(Size2(500, 0) * EDSCALE); + Size2i window_size = get_size(); + Size2 contents_min_size = get_contents_minimum_size(); + if (window_size.x < contents_min_size.x || window_size.y < contents_min_size.y) { + set_size(window_size.max(contents_min_size)); + } } String _test_path() { @@ -2594,6 +2598,12 @@ ProjectManager::ProjectManager() { EditorFileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files")); + int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons"); + if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer. + // Swap on means OK first. + AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2); + } + set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); set_theme(create_custom_theme()); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index acaa4ec3c9..ee358a8064 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -2176,11 +2176,15 @@ void SceneTreeDock::_selection_changed() { void SceneTreeDock::_do_create(Node *p_parent) { Variant c = create_dialog->instance_selected(); - - ERR_FAIL_COND(!c); Node *child = Object::cast_to<Node>(c); ERR_FAIL_COND(!child); + String new_name = p_parent->validate_child_name(child); + if (GLOBAL_GET("editor/node_naming/name_casing").operator int() != NAME_CASING_PASCAL_CASE) { + new_name = adjust_name_casing(new_name); + } + child->set_name(new_name); + editor_data->get_undo_redo()->create_action_for_history(TTR("Create Node"), editor_data->get_current_edited_scene_history_id()); if (edited_scene) { @@ -2191,7 +2195,6 @@ void SceneTreeDock::_do_create(Node *p_parent) { editor_data->get_undo_redo()->add_do_reference(child); editor_data->get_undo_redo()->add_undo_method(p_parent, "remove_child", child); - String new_name = p_parent->validate_child_name(child); EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); editor_data->get_undo_redo()->add_do_method(ed, "live_debug_create_node", edited_scene->get_path_to(p_parent), child->get_class(), new_name); editor_data->get_undo_redo()->add_undo_method(ed, "live_debug_remove_node", NodePath(String(edited_scene->get_path_to(p_parent)).path_join(new_name))); diff --git a/modules/mono/class_db_api_json.cpp b/modules/mono/class_db_api_json.cpp index c4547b4323..1f4b085bfb 100644 --- a/modules/mono/class_db_api_json.cpp +++ b/modules/mono/class_db_api_json.cpp @@ -227,8 +227,7 @@ void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) { Ref<FileAccess> f = FileAccess::open(p_output_file, FileAccess::WRITE); ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file '" + p_output_file + "'."); - JSON json; - f->store_string(json.stringify(classes_dict, "\t")); + f->store_string(JSON::stringify(classes_dict, "\t")); print_line(String() + "ClassDB API JSON written to: " + ProjectSettings::get_singleton()->globalize_path(p_output_file)); } diff --git a/modules/websocket/doc_classes/WebSocketClient.xml b/modules/websocket/doc_classes/WebSocketClient.xml deleted file mode 100644 index 1978d2e7c6..0000000000 --- a/modules/websocket/doc_classes/WebSocketClient.xml +++ /dev/null @@ -1,94 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="WebSocketClient" inherits="WebSocketMultiplayerPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> - <brief_description> - A WebSocket client implementation. - </brief_description> - <description> - This class implements a WebSocket client compatible with any RFC 6455-compliant WebSocket server. - This client can be optionally used as a multiplayer peer for the [MultiplayerAPI]. - After starting the client ([method connect_to_url]), you will need to [method MultiplayerPeer.poll] it at regular intervals (e.g. inside [method Node._process]). - You will receive appropriate signals when connecting, disconnecting, or when new data is available. - [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android. - </description> - <tutorials> - </tutorials> - <methods> - <method name="connect_to_url"> - <return type="int" enum="Error" /> - <param index="0" name="url" type="String" /> - <param index="1" name="protocols" type="PackedStringArray" default="PackedStringArray()" /> - <param index="2" name="gd_mp_api" type="bool" default="false" /> - <param index="3" name="custom_headers" type="PackedStringArray" default="PackedStringArray()" /> - <description> - Connects to the given URL requesting one of the given [code]protocols[/code] as sub-protocol. If the list empty (default), no sub-protocol will be requested. - If [code]true[/code] is passed as [code]gd_mp_api[/code], the client will behave like a multiplayer peer for the [MultiplayerAPI], connections to non-Godot servers will not work, and [signal data_received] will not be emitted. - If [code]false[/code] is passed instead (default), you must call [PacketPeer] functions ([code]put_packet[/code], [code]get_packet[/code], etc.) on the [WebSocketPeer] returned via [code]get_peer(1)[/code] and not on this object directly (e.g. [code]get_peer(1).put_packet(data)[/code]). - You can optionally pass a list of [code]custom_headers[/code] to be added to the handshake HTTP request. - [b]Note:[/b] To avoid mixed content warnings or errors in Web, you may have to use a [code]url[/code] that starts with [code]wss://[/code] (secure) instead of [code]ws://[/code]. When doing so, make sure to use the fully qualified domain name that matches the one defined in the server's TLS certificate. Do not connect directly via the IP address for [code]wss://[/code] connections, as it won't match with the TLS certificate. - [b]Note:[/b] Specifying [code]custom_headers[/code] is not supported in Web exports due to browsers' restrictions. - </description> - </method> - <method name="disconnect_from_host"> - <return type="void" /> - <param index="0" name="code" type="int" default="1000" /> - <param index="1" name="reason" type="String" default="""" /> - <description> - Disconnects this client from the connected host. See [method WebSocketPeer.close] for more information. - </description> - </method> - <method name="get_connected_host" qualifiers="const"> - <return type="String" /> - <description> - Returns the IP address of the currently connected host. - </description> - </method> - <method name="get_connected_port" qualifiers="const"> - <return type="int" /> - <description> - Returns the IP port of the currently connected host. - </description> - </method> - </methods> - <members> - <member name="trusted_tls_certificate" type="X509Certificate" setter="set_trusted_tls_certificate" getter="get_trusted_tls_certificate"> - If specified, this [X509Certificate] will be the only one accepted when connecting to an TLS host. Any other certificate provided by the server will be regarded as invalid. - [b]Note:[/b] Specifying a custom [code]trusted_tls_certificate[/code] is not supported in Web exports due to browsers' restrictions. - </member> - <member name="verify_tls" type="bool" setter="set_verify_tls_enabled" getter="is_verify_tls_enabled"> - If [code]true[/code], TLS certificate verification is enabled. - [b]Note:[/b] You must specify the certificates to be used in the Project Settings for it to work when exported. - </member> - </members> - <signals> - <signal name="connection_closed"> - <param index="0" name="was_clean_close" type="bool" /> - <description> - Emitted when the connection to the server is closed. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. - </description> - </signal> - <signal name="connection_error"> - <description> - Emitted when the connection to the server fails. - </description> - </signal> - <signal name="connection_established"> - <param index="0" name="protocol" type="String" /> - <description> - Emitted when a connection with the server is established, [code]protocol[/code] will contain the sub-protocol agreed with the server. - </description> - </signal> - <signal name="data_received"> - <description> - Emitted when a WebSocket message is received. - [b]Note:[/b] This signal is [i]not[/i] emitted when used as high-level multiplayer peer. - </description> - </signal> - <signal name="server_close_request"> - <param index="0" name="code" type="int" /> - <param index="1" name="reason" type="String" /> - <description> - Emitted when the server requests a clean close. You should keep polling until you get a [signal connection_closed] signal to achieve the clean close. See [method WebSocketPeer.close] for more details. - </description> - </signal> - </signals> -</class> diff --git a/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml b/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml index 4cc4d515e7..c4481b046b 100644 --- a/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml +++ b/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml @@ -10,6 +10,42 @@ <tutorials> </tutorials> <methods> + <method name="close"> + <return type="void" /> + <description> + Closes this [MultiplayerPeer], resetting the state to [constant MultiplayerPeer.CONNECTION_CONNECTED]. + [b]Note:[/b] To make sure remote peers receive a clean close prefer disconnecting clients via [method disconnect_peer]. + </description> + </method> + <method name="create_client"> + <return type="int" enum="Error" /> + <param index="0" name="url" type="String" /> + <param index="1" name="verify_tls" type="bool" default="true" /> + <param index="2" name="tls_certificate" type="X509Certificate" default="null" /> + <description> + Starts a new multiplayer client connecting to the given [param url]. If [param verify_tls] is [code]false[/code] certificate validation will be disabled. If specified, the [param tls_certificate] will be used to verify the TLS host. + [b]Note[/b]: It is recommended to specify the scheme part of the URL, i.e. the [param url] should start with either [code]ws://[/code] or [code]wss://[/code]. + </description> + </method> + <method name="create_server"> + <return type="int" enum="Error" /> + <param index="0" name="port" type="int" /> + <param index="1" name="bind_address" type="String" default=""*"" /> + <param index="2" name="tls_key" type="CryptoKey" default="null" /> + <param index="3" name="tls_certificate" type="X509Certificate" default="null" /> + <description> + Starts a new multiplayer server listening on the given [param port]. You can optionally specify a [param bind_address], and provide a [param tls_key] and [param tls_certificate] to use TLS. + </description> + </method> + <method name="disconnect_peer"> + <return type="void" /> + <param index="0" name="id" type="int" /> + <param index="1" name="code" type="int" default="1000" /> + <param index="2" name="reason" type="String" default="""" /> + <description> + Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more information. + </description> + </method> <method name="get_peer" qualifiers="const"> <return type="WebSocketPeer" /> <param index="0" name="peer_id" type="int" /> @@ -17,27 +53,39 @@ Returns the [WebSocketPeer] associated to the given [code]peer_id[/code]. </description> </method> - <method name="set_buffers"> - <return type="int" enum="Error" /> - <param index="0" name="input_buffer_size_kb" type="int" /> - <param index="1" name="input_max_packets" type="int" /> - <param index="2" name="output_buffer_size_kb" type="int" /> - <param index="3" name="output_max_packets" type="int" /> + <method name="get_peer_address" qualifiers="const"> + <return type="String" /> + <param index="0" name="id" type="int" /> <description> - Configures the buffer sizes for this WebSocket peer. Default values can be specified in the Project Settings under [code]network/limits[/code]. For server, values are meant per connected peer. - The first two parameters define the size and queued packets limits of the input buffer, the last two of the output buffer. - Buffer sizes are expressed in KiB, so [code]4 = 2^12 = 4096 bytes[/code]. All parameters will be rounded up to the nearest power of two. - [b]Note:[/b] Web exports only use the input buffer since the output one is managed by browsers. + Returns the IP address of the given peer. </description> </method> - </methods> - <signals> - <signal name="peer_packet"> - <param index="0" name="peer_source" type="int" /> + <method name="get_peer_port" qualifiers="const"> + <return type="int" /> + <param index="0" name="id" type="int" /> <description> - Emitted when a packet is received from a peer. - [b]Note:[/b] This signal is only emitted when the client or server is configured to use Godot multiplayer API. + Returns the remote port of the given peer. </description> - </signal> - </signals> + </method> + </methods> + <members> + <member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()"> + The extra headers to use during handshake. See [member WebSocketPeer.handshake_headers] for more details. + </member> + <member name="handshake_timeout" type="float" setter="set_handshake_timeout" getter="get_handshake_timeout" default="3.0"> + The maximum time each peer can stay in a connecting state before being dropped. + </member> + <member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535"> + The inbound buffer size for connected peers. See [member WebSocketPeer.inbound_buffer_size] for more details. + </member> + <member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="2048"> + The maximum number of queued packets for connected peers. See [member WebSocketPeer.max_queued_packets] for more details. + </member> + <member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535"> + The outbound buffer size for connected peers. See [member WebSocketPeer.outbound_buffer_size] for more details. + </member> + <member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()"> + The supported WebSocket sub-protocols. See [member WebSocketPeer.supported_protocols] for more details. + </member> + </members> </class> diff --git a/modules/websocket/doc_classes/WebSocketPeer.xml b/modules/websocket/doc_classes/WebSocketPeer.xml index 627b9c607c..fe0aae412e 100644 --- a/modules/websocket/doc_classes/WebSocketPeer.xml +++ b/modules/websocket/doc_classes/WebSocketPeer.xml @@ -1,25 +1,82 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="WebSocketPeer" inherits="PacketPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> - A class representing a specific WebSocket connection. + A WebSocket connection. </brief_description> <description> - This class represents a specific WebSocket connection, allowing you to do lower level operations with it. - You can choose to write to the socket in binary or text mode, and you can recognize the mode used for writing by the other peer. + This class represents WebSocket connection, and can be used as a WebSocket client (RFC 6455-compliant) or as a remote peer of a WebSocket server. + You can send WebSocket binary frames using [method PacketPeer.put_packet], and WebSocket text frames using [method send] (prefer text frames when interacting with text-based API). You can check the frame type of the last packet via [method was_string_packet]. + To start a WebSocket client, first call [method connect_to_url], then regularly call [method poll] (e.g. during [Node] process). You can query the socket state via [method get_ready_state], get the number of pending packets using [method PacketPeer.get_available_packet_count], and retrieve them via [method PacketPeer.get_packet]. + [codeblocks] + [gdscript] + extends Node + + var socket = WebSocketPeer.new() + + func _ready(): + socket.connect_to_url("wss://example.com") + + func _process(delta): + socket.poll() + var state = socket.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + while socket.get_available_packet_count(): + print("Packet: ", socket.get_packet()) + elif state == WebSocketPeer.STATE_CLOSING: + # Keep polling to achieve proper close. + pass + elif state == WebSocketPeer.STATE_CLOSED: + var code = socket.get_close_code() + var reason = socket.get_close_reason() + print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1]) + set_process(false) # Stop processing. + [/gdscript] + [/codeblocks] + To use the peer as part of a WebSocket server refer to [method accept_stream] and the online tutorial. </description> <tutorials> </tutorials> <methods> + <method name="accept_stream"> + <return type="int" enum="Error" /> + <param index="0" name="stream" type="StreamPeer" /> + <description> + Accepts a peer connection performing the HTTP handshake as a WebSocket server. The [param stream] must be a valid TCP stream retrieved via [method TCPServer.take_connection], or a TLS stream accepted via [method StreamPeerTLS.accept_stream]. + [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. + </description> + </method> <method name="close"> <return type="void" /> <param index="0" name="code" type="int" default="1000" /> <param index="1" name="reason" type="String" default="""" /> <description> - Closes this WebSocket connection. [code]code[/code] is the status code for the closure (see RFC 6455 section 7.4 for a list of valid status codes). [code]reason[/code] is the human readable reason for closing the connection (can be any UTF-8 string that's smaller than 123 bytes). - [b]Note:[/b] To achieve a clean close, you will need to keep polling until either [signal WebSocketClient.connection_closed] or [signal WebSocketServer.client_disconnected] is received. + Closes this WebSocket connection. [param code] is the status code for the closure (see RFC 6455 section 7.4 for a list of valid status codes). [param reason] is the human readable reason for closing the connection (can be any UTF-8 string that's smaller than 123 bytes). If [param code] is negative, the connection will be closed immediately without notifying the remote peer. + [b]Note:[/b] To achieve a clean close, you will need to keep polling until [constant STATE_CLOSED] is reached. [b]Note:[/b] The Web export might not support all status codes. Please refer to browser-specific documentation for more details. </description> </method> + <method name="connect_to_url"> + <return type="int" enum="Error" /> + <param index="0" name="url" type="String" /> + <param index="1" name="verify_tls" type="bool" default="true" /> + <param index="2" name="trusted_tls_certificate" type="X509Certificate" default="null" /> + <description> + Connects to the given URL. If [param verify_tls] is [code]false[/code] certificate validation will be disabled. If specified, the [param trusted_tls_certificate] will be the only one accepted when connecting to a TLS host. + [b]Note:[/b] To avoid mixed content warnings or errors in Web, you may have to use a [code]url[/code] that starts with [code]wss://[/code] (secure) instead of [code]ws://[/code]. When doing so, make sure to use the fully qualified domain name that matches the one defined in the server's TLS certificate. Do not connect directly via the IP address for [code]wss://[/code] connections, as it won't match with the TLS certificate. + </description> + </method> + <method name="get_close_code" qualifiers="const"> + <return type="int" /> + <description> + Returns the received WebSocket close frame status code, or [code]-1[/code] when the connection was not cleanly closed. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED]. + </description> + </method> + <method name="get_close_reason" qualifiers="const"> + <return type="String" /> + <description> + Returns the received WebSocket close frame status reason string. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED]. + </description> + </method> <method name="get_connected_host" qualifiers="const"> <return type="String" /> <description> @@ -40,31 +97,51 @@ Returns the current amount of data in the outbound websocket buffer. [b]Note:[/b] Web exports use WebSocket.bufferedAmount, while other platforms use an internal buffer. </description> </method> - <method name="get_write_mode" qualifiers="const"> - <return type="int" enum="WebSocketPeer.WriteMode" /> + <method name="get_ready_state" qualifiers="const"> + <return type="int" enum="WebSocketPeer.State" /> <description> - Gets the current selected write mode. See [enum WriteMode]. + Returns the ready state of the connection. See [enum State]. </description> </method> - <method name="is_connected_to_host" qualifiers="const"> - <return type="bool" /> + <method name="get_requested_url" qualifiers="const"> + <return type="String" /> <description> - Returns [code]true[/code] if this peer is currently connected. + Returns the URL requested by this peer. The URL is derived from the [code]url[/code] passed to [method connect_to_url] or from the HTTP headers when acting as server (i.e. when using [method accept_stream]). </description> </method> - <method name="set_no_delay"> + <method name="get_selected_protocol" qualifiers="const"> + <return type="String" /> + <description> + Returns the selected WebSocket sub-protocol for this connection or an empty string if the sub-protocol has not been selected yet. + </description> + </method> + <method name="poll"> <return type="void" /> - <param index="0" name="enabled" type="bool" /> <description> - Disable Nagle's algorithm on the underling TCP socket (default). See [method StreamPeerTCP.set_no_delay] for more information. - [b]Note:[/b] Not available in the Web export. + Updates the connection state and receive incoming packets. Call this function regularly to keep it in a clean state. </description> </method> - <method name="set_write_mode"> + <method name="send"> + <return type="int" enum="Error" /> + <param index="0" name="message" type="PackedByteArray" /> + <param index="1" name="write_mode" type="int" enum="WebSocketPeer.WriteMode" default="1" /> + <description> + Sends the given [param message] using the desired [param write_mode]. When sending a [String], prefer using [method send_text]. + </description> + </method> + <method name="send_text"> + <return type="int" enum="Error" /> + <param index="0" name="message" type="String" /> + <description> + Sends the given [param message] using WebSocket text mode. Perfer this method over [method PacketPeer.put_packet] when interacting with third-party text-based API (e.g. when using [JSON] formatted messages). + </description> + </method> + <method name="set_no_delay"> <return type="void" /> - <param index="0" name="mode" type="int" enum="WebSocketPeer.WriteMode" /> + <param index="0" name="enabled" type="bool" /> <description> - Sets the socket to use the given [enum WriteMode]. + Disable Nagle's algorithm on the underling TCP socket (default). See [method StreamPeerTCP.set_no_delay] for more information. + [b]Note:[/b] Not available in the Web export. </description> </method> <method name="was_string_packet" qualifiers="const"> @@ -74,6 +151,24 @@ </description> </method> </methods> + <members> + <member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()"> + The extra HTTP headers to be sent during the WebSocket handshake. + [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. + </member> + <member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535"> + The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the inbound packets). + </member> + <member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="2048"> + The maximum amount of packets that will be allowed in the queues (both inbound and outbound). + </member> + <member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535"> + The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the outbound packets). + </member> + <member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()"> + The WebSocket sub-protocols allowed during the WebSocket handshake. + </member> + </members> <constants> <constant name="WRITE_MODE_TEXT" value="0" enum="WriteMode"> Specifies that WebSockets messages should be transferred as text payload (only valid UTF-8 is allowed). @@ -81,5 +176,17 @@ <constant name="WRITE_MODE_BINARY" value="1" enum="WriteMode"> Specifies that WebSockets messages should be transferred as binary payload (any byte combination is allowed). </constant> + <constant name="STATE_CONNECTING" value="0" enum="State"> + Socket has been created. The connection is not yet open. + </constant> + <constant name="STATE_OPEN" value="1" enum="State"> + The connection is open and ready to communicate. + </constant> + <constant name="STATE_CLOSING" value="2" enum="State"> + The connection is in the process of closing. This means a close request has been sent to the remote peer but confirmation has not been received. + </constant> + <constant name="STATE_CLOSED" value="3" enum="State"> + The connection is closed or couldn't be opened. + </constant> </constants> </class> diff --git a/modules/websocket/doc_classes/WebSocketServer.xml b/modules/websocket/doc_classes/WebSocketServer.xml deleted file mode 100644 index 07a55b73f1..0000000000 --- a/modules/websocket/doc_classes/WebSocketServer.xml +++ /dev/null @@ -1,127 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="WebSocketServer" inherits="WebSocketMultiplayerPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> - <brief_description> - A WebSocket server implementation. - </brief_description> - <description> - This class implements a WebSocket server that can also support the high-level multiplayer API. - After starting the server ([method listen]), you will need to [method MultiplayerPeer.poll] it at regular intervals (e.g. inside [method Node._process]). When clients connect, disconnect, or send data, you will receive the appropriate signal. - [b]Note:[/b] Not available in Web exports. - [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android. - </description> - <tutorials> - </tutorials> - <methods> - <method name="disconnect_peer"> - <return type="void" /> - <param index="0" name="id" type="int" /> - <param index="1" name="code" type="int" default="1000" /> - <param index="2" name="reason" type="String" default="""" /> - <description> - Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more information. - </description> - </method> - <method name="get_peer_address" qualifiers="const"> - <return type="String" /> - <param index="0" name="id" type="int" /> - <description> - Returns the IP address of the given peer. - </description> - </method> - <method name="get_peer_port" qualifiers="const"> - <return type="int" /> - <param index="0" name="id" type="int" /> - <description> - Returns the remote port of the given peer. - </description> - </method> - <method name="has_peer" qualifiers="const"> - <return type="bool" /> - <param index="0" name="id" type="int" /> - <description> - Returns [code]true[/code] if a peer with the given ID is connected. - </description> - </method> - <method name="is_listening" qualifiers="const"> - <return type="bool" /> - <description> - Returns [code]true[/code] if the server is actively listening on a port. - </description> - </method> - <method name="listen"> - <return type="int" enum="Error" /> - <param index="0" name="port" type="int" /> - <param index="1" name="protocols" type="PackedStringArray" default="PackedStringArray()" /> - <param index="2" name="gd_mp_api" type="bool" default="false" /> - <description> - Starts listening on the given port. - You can specify the desired subprotocols via the "protocols" array. If the list empty (default), no sub-protocol will be requested. - If [code]true[/code] is passed as [code]gd_mp_api[/code], the server will behave like a multiplayer peer for the [MultiplayerAPI], connections from non-Godot clients will not work, and [signal data_received] will not be emitted. - If [code]false[/code] is passed instead (default), you must call [PacketPeer] functions ([code]put_packet[/code], [code]get_packet[/code], etc.), on the [WebSocketPeer] returned via [code]get_peer(id)[/code] to communicate with the peer with given [code]id[/code] (e.g. [code]get_peer(id).get_available_packet_count[/code]). - </description> - </method> - <method name="set_extra_headers"> - <return type="void" /> - <param index="0" name="headers" type="PackedStringArray" default="PackedStringArray()" /> - <description> - Sets additional headers to be sent to clients during the HTTP handshake. - </description> - </method> - <method name="stop"> - <return type="void" /> - <description> - Stops the server and clear its state. - </description> - </method> - </methods> - <members> - <member name="bind_ip" type="String" setter="set_bind_ip" getter="get_bind_ip" default=""*""> - When not set to [code]*[/code] will restrict incoming connections to the specified IP address. Setting [code]bind_ip[/code] to [code]127.0.0.1[/code] will cause the server to listen only to the local host. - </member> - <member name="ca_chain" type="X509Certificate" setter="set_ca_chain" getter="get_ca_chain"> - When using TLS (see [member private_key] and [member tls_certificate]), you can set this to a valid [X509Certificate] to be provided as additional CA chain information during the TLS handshake. - </member> - <member name="handshake_timeout" type="float" setter="set_handshake_timeout" getter="get_handshake_timeout" default="3.0"> - The time in seconds before a pending client (i.e. a client that has not yet finished the HTTP handshake) is considered stale and forcefully disconnected. - </member> - <member name="private_key" type="CryptoKey" setter="set_private_key" getter="get_private_key"> - When set to a valid [CryptoKey] (along with [member tls_certificate]) will cause the server to require TLS instead of regular TCP (i.e. the [code]wss://[/code] protocol). - </member> - <member name="tls_certificate" type="X509Certificate" setter="set_tls_certificate" getter="get_tls_certificate"> - When set to a valid [X509Certificate] (along with [member private_key]) will cause the server to require TLS instead of regular TCP (i.e. the [code]wss://[/code] protocol). - </member> - </members> - <signals> - <signal name="client_close_request"> - <param index="0" name="id" type="int" /> - <param index="1" name="code" type="int" /> - <param index="2" name="reason" type="String" /> - <description> - Emitted when a client requests a clean close. You should keep polling until you get a [signal client_disconnected] signal with the same [code]id[/code] to achieve the clean close. See [method WebSocketPeer.close] for more details. - </description> - </signal> - <signal name="client_connected"> - <param index="0" name="id" type="int" /> - <param index="1" name="protocol" type="String" /> - <param index="2" name="resource_name" type="String" /> - <description> - Emitted when a new client connects. "protocol" will be the sub-protocol agreed with the client, and "resource_name" will be the resource name of the URI the peer used. - "resource_name" is a path (at the very least a single forward slash) and potentially a query string. - </description> - </signal> - <signal name="client_disconnected"> - <param index="0" name="id" type="int" /> - <param index="1" name="was_clean_close" type="bool" /> - <description> - Emitted when a client disconnects. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. - </description> - </signal> - <signal name="data_received"> - <param index="0" name="id" type="int" /> - <description> - Emitted when a new message is received. - [b]Note:[/b] This signal is [i]not[/i] emitted when used as high-level multiplayer peer. - </description> - </signal> - </signals> -</class> diff --git a/modules/websocket/editor/editor_debugger_server_websocket.cpp b/modules/websocket/editor/editor_debugger_server_websocket.cpp index 0443147d98..1c4ebd0f55 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.cpp +++ b/modules/websocket/editor/editor_debugger_server_websocket.cpp @@ -38,18 +38,25 @@ #include "editor/editor_node.h" #include "editor/editor_settings.h" -void EditorDebuggerServerWebSocket::_peer_connected(int p_id, String _protocol) { - pending_peers.push_back(p_id); -} - -void EditorDebuggerServerWebSocket::_peer_disconnected(int p_id, bool p_was_clean) { - if (pending_peers.find(p_id)) { - pending_peers.erase(p_id); - } -} - void EditorDebuggerServerWebSocket::poll() { - server->poll(); + if (pending_peer.is_null() && tcp_server->is_connection_available()) { + Ref<WebSocketPeer> peer; + Error err = peer->accept_stream(tcp_server->take_connection()); + if (err == OK) { + pending_timer = OS::get_singleton()->get_ticks_msec(); + pending_peer = peer; + } + } + if (pending_peer.is_valid() && pending_peer->get_ready_state() != WebSocketPeer::STATE_OPEN) { + pending_peer->poll(); + WebSocketPeer::State ready_state = pending_peer->get_ready_state(); + if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) { + pending_peer.unref(); // Failed. + } + if (ready_state == WebSocketPeer::STATE_CONNECTING && OS::get_singleton()->get_ticks_msec() - pending_timer > 3000) { + pending_peer.unref(); // Timeout. + } + } } String EditorDebuggerServerWebSocket::get_uri() const { @@ -69,15 +76,10 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } - // Set up the server - server->set_bind_ip(bind_host); - Vector<String> compatible_protocols; - compatible_protocols.push_back("binary"); // compatibility with EMSCRIPTEN TCP-to-WebSocket layer. - // Try listening on ports const int max_attempts = 5; for (int attempt = 1;; ++attempt) { - const Error err = server->listen(bind_port, compatible_protocols); + const Error err = tcp_server->listen(bind_port, bind_host); if (err == OK) { break; } @@ -96,31 +98,27 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { } void EditorDebuggerServerWebSocket::stop() { - server->stop(); - pending_peers.clear(); + pending_peer.unref(); + tcp_server->stop(); } bool EditorDebuggerServerWebSocket::is_active() const { - return server->is_listening(); + return tcp_server->is_listening(); } bool EditorDebuggerServerWebSocket::is_connection_available() const { - return pending_peers.size() > 0; + return pending_peer.is_valid() && pending_peer->get_ready_state() == WebSocketPeer::STATE_OPEN; } Ref<RemoteDebuggerPeer> EditorDebuggerServerWebSocket::take_connection() { ERR_FAIL_COND_V(!is_connection_available(), Ref<RemoteDebuggerPeer>()); - RemoteDebuggerPeer *peer = memnew(RemoteDebuggerPeerWebSocket(server->get_peer(pending_peers[0]))); - pending_peers.pop_front(); + RemoteDebuggerPeer *peer = memnew(RemoteDebuggerPeerWebSocket(pending_peer)); + pending_peer.unref(); return peer; } EditorDebuggerServerWebSocket::EditorDebuggerServerWebSocket() { - server = Ref<WebSocketServer>(WebSocketServer::create()); - int max_pkts = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages"); - server->set_buffers(8192, max_pkts, 8192, max_pkts); - server->connect("client_connected", callable_mp(this, &EditorDebuggerServerWebSocket::_peer_connected)); - server->connect("client_disconnected", callable_mp(this, &EditorDebuggerServerWebSocket::_peer_disconnected)); + tcp_server.instantiate(); } EditorDebuggerServerWebSocket::~EditorDebuggerServerWebSocket() { diff --git a/modules/websocket/editor/editor_debugger_server_websocket.h b/modules/websocket/editor/editor_debugger_server_websocket.h index 7c0705302d..31e54cb5df 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.h +++ b/modules/websocket/editor/editor_debugger_server_websocket.h @@ -33,15 +33,18 @@ #ifdef TOOLS_ENABLED -#include "../websocket_server.h" +#include "../websocket_peer.h" + +#include "core/io/tcp_server.h" #include "editor/debugger/editor_debugger_server.h" class EditorDebuggerServerWebSocket : public EditorDebuggerServer { GDCLASS(EditorDebuggerServerWebSocket, EditorDebuggerServer); private: - Ref<WebSocketServer> server; - List<int> pending_peers; + Ref<TCPServer> tcp_server; + Ref<WebSocketPeer> pending_peer; + uint64_t pending_timer = 0; String endpoint; public: diff --git a/modules/websocket/emws_client.cpp b/modules/websocket/emws_client.cpp deleted file mode 100644 index 933a1f43e9..0000000000 --- a/modules/websocket/emws_client.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/*************************************************************************/ -/* emws_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef WEB_ENABLED - -#include "emws_client.h" - -#include "core/config/project_settings.h" -#include "core/io/ip.h" -#include "emscripten.h" - -void EMWSClient::_esws_on_connect(void *obj, char *proto) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_is_connecting = false; - client->_on_connect(String(proto)); -} - -void EMWSClient::_esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - - Error err = static_cast<EMWSPeer *>(*client->get_peer(1))->read_msg(p_data, p_data_size, p_is_string == 1); - if (err == OK) { - client->_on_peer_packet(); - } -} - -void EMWSClient::_esws_on_error(void *obj) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_is_connecting = false; - client->_on_error(); -} - -void EMWSClient::_esws_on_close(void *obj, int code, const char *reason, int was_clean) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_on_close_request(code, String(reason)); - client->_is_connecting = false; - client->disconnect_from_host(); - client->_on_disconnect(was_clean != 0); -} - -Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocols, const Vector<String> p_custom_headers) { - if (_js_id) { - godot_js_websocket_destroy(_js_id); - _js_id = 0; - } - - String proto_string; - for (int i = 0; i < p_protocols.size(); i++) { - if (i != 0) { - proto_string += ","; - } - proto_string += p_protocols[i]; - } - - String str = "ws://"; - - if (p_custom_headers.size()) { - WARN_PRINT_ONCE("Custom headers are not supported in Web platform."); - } - if (p_tls) { - str = "wss://"; - if (tls_cert.is_valid()) { - WARN_PRINT_ONCE("Custom SSL certificate is not supported in Web platform."); - } - } - str += p_host + ":" + itos(p_port) + p_path; - _is_connecting = true; - - _js_id = godot_js_websocket_create(this, str.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close); - if (!_js_id) { - return FAILED; - } - - static_cast<Ref<EMWSPeer>>(_peer)->set_sock(_js_id, _in_buf_size, _in_pkt_size, _out_buf_size); - - return OK; -} - -void EMWSClient::poll() { -} - -Ref<WebSocketPeer> EMWSClient::get_peer(int p_peer_id) const { - return _peer; -} - -MultiplayerPeer::ConnectionStatus EMWSClient::get_connection_status() const { - if (_peer->is_connected_to_host()) { - if (_is_connecting) { - return CONNECTION_CONNECTING; - } - return CONNECTION_CONNECTED; - } - - return CONNECTION_DISCONNECTED; -} - -void EMWSClient::disconnect_from_host(int p_code, String p_reason) { - _peer->close(p_code, p_reason); -} - -IPAddress EMWSClient::get_connected_host() const { - ERR_FAIL_V_MSG(IPAddress(), "Not supported in Web export."); -} - -uint16_t EMWSClient::get_connected_port() const { - ERR_FAIL_V_MSG(0, "Not supported in Web export."); -} - -int EMWSClient::get_max_packet_size() const { - return (1 << _in_buf_size) - PROTO_SIZE; -} - -Error EMWSClient::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - return OK; -} - -EMWSClient::EMWSClient() { - _peer = Ref<EMWSPeer>(memnew(EMWSPeer)); -} - -EMWSClient::~EMWSClient() { - disconnect_from_host(); - _peer = Ref<EMWSPeer>(); - if (_js_id) { - godot_js_websocket_destroy(_js_id); - } -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/emws_client.h b/modules/websocket/emws_client.h deleted file mode 100644 index cdcec31e19..0000000000 --- a/modules/websocket/emws_client.h +++ /dev/null @@ -1,71 +0,0 @@ -/*************************************************************************/ -/* emws_client.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef EMWS_CLIENT_H -#define EMWS_CLIENT_H - -#ifdef WEB_ENABLED - -#include "core/error/error_list.h" -#include "emws_peer.h" -#include "websocket_client.h" - -class EMWSClient : public WebSocketClient { - GDCIIMPL(EMWSClient, WebSocketClient); - -private: - int _js_id = 0; - bool _is_connecting = false; - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; - - static void _esws_on_connect(void *obj, char *proto); - static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); - static void _esws_on_error(void *obj); - static void _esws_on_close(void *obj, int code, const char *reason, int was_clean); - -public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) override; - Ref<WebSocketPeer> get_peer(int p_peer_id) const override; - void disconnect_from_host(int p_code = 1000, String p_reason = "") override; - IPAddress get_connected_host() const override; - uint16_t get_connected_port() const override; - virtual ConnectionStatus get_connection_status() const override; - int get_max_packet_size() const override; - virtual void poll() override; - EMWSClient(); - ~EMWSClient(); -}; - -#endif // WEB_ENABLED - -#endif // EMWS_CLIENT_H diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index 859c92b457..5f3cb76852 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -34,55 +34,116 @@ #include "core/io/ip.h" -void EMWSPeer::set_sock(int p_sock, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size) { - peer_sock = p_sock; - _in_buffer.resize(p_in_pkt_size, p_in_buf_size); - _packet_buffer.resize((1 << p_in_buf_size)); - _out_buf_size = p_out_buf_size; +void EMWSPeer::_esws_on_connect(void *p_obj, char *p_proto) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->ready_state = STATE_OPEN; + peer->selected_protocol.parse_utf8(p_proto); } -void EMWSPeer::set_write_mode(WriteMode p_mode) { - write_mode = p_mode; +void EMWSPeer::_esws_on_message(void *p_obj, const uint8_t *p_data, int p_data_size, int p_is_string) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + uint8_t is_string = p_is_string ? 1 : 0; + peer->in_buffer.write_packet(p_data, p_data_size, &is_string); } -EMWSPeer::WriteMode EMWSPeer::get_write_mode() const { - return write_mode; +void EMWSPeer::_esws_on_error(void *p_obj) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->ready_state = STATE_CLOSED; } -Error EMWSPeer::read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string) { - uint8_t is_string = p_is_string ? 1 : 0; - return _in_buffer.write_packet(p_data, p_size, &is_string); +void EMWSPeer::_esws_on_close(void *p_obj, int p_code, const char *p_reason, int p_was_clean) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->close_code = p_code; + peer->close_reason.parse_utf8(p_reason); + peer->ready_state = STATE_CLOSED; } -Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V(_out_buf_size && ((uint64_t)godot_js_websocket_buffered_amount(peer_sock) + p_buffer_size >= (1ULL << _out_buf_size)), ERR_OUT_OF_MEMORY); +Error EMWSPeer::connect_to_url(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(ready_state != STATE_CLOSED, ERR_ALREADY_IN_USE); + _clear(); + + String host; + String path; + String scheme; + int port = 0; + Error err = p_url.parse_url(scheme, host, port, path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); + + if (scheme.is_empty()) { + scheme = "ws://"; + } + ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme)); + + String proto_string; + for (int i = 0; i < supported_protocols.size(); i++) { + if (i != 0) { + proto_string += ","; + } + proto_string += supported_protocols[i]; + } + + if (handshake_headers.size()) { + WARN_PRINT_ONCE("Custom headers are not supported in Web platform."); + } + if (p_tls_certificate.is_valid()) { + WARN_PRINT_ONCE("Custom SSL certificates are not supported in Web platform."); + } - int is_bin = write_mode == WebSocketPeer::WRITE_MODE_BINARY ? 1 : 0; + requested_url = scheme + host; - if (godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, is_bin) != 0) { + if (port && ((scheme == "ws://" && port != 80) || (scheme == "wss://" && port != 443))) { + requested_url += ":" + String::num(port); + } + + peer_sock = godot_js_websocket_create(this, requested_url.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close); + if (peer_sock == -1) { return FAILED; } + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_CONNECTING; + return OK; +} + +Error EMWSPeer::accept_stream(Ref<StreamPeer> p_stream) { + WARN_PRINT_ONCE("Acting as WebSocket server is not supported in Web platforms."); + return ERR_UNAVAILABLE; +} + +Error EMWSPeer::_send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary) { + ERR_FAIL_COND_V(outbound_buffer_size > 0 && (get_current_outbound_buffered_amount() + p_buffer_size >= outbound_buffer_size), ERR_OUT_OF_MEMORY); + if (godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, p_binary ? 1 : 0) != 0) { + return FAILED; + } return OK; } +Error EMWSPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) { + return _send(p_buffer, p_buffer_size, p_mode == WRITE_MODE_TEXT); +} + +Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { + return _send(p_buffer, p_buffer_size, true); +} + Error EMWSPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { - if (_in_buffer.packets_left() == 0) { + if (in_buffer.packets_left() == 0) { return ERR_UNAVAILABLE; } int read = 0; - Error err = _in_buffer.read_packet(_packet_buffer.ptrw(), _packet_buffer.size(), &_is_string, read); + Error err = in_buffer.read_packet(packet_buffer.ptrw(), packet_buffer.size(), &was_string, read); ERR_FAIL_COND_V(err != OK, err); - *r_buffer = _packet_buffer.ptr(); + *r_buffer = packet_buffer.ptr(); r_buffer_size = read; return OK; } int EMWSPeer::get_available_packet_count() const { - return _in_buffer.packets_left(); + return in_buffer.packets_left(); } int EMWSPeer::get_current_outbound_buffered_amount() const { @@ -93,20 +154,66 @@ int EMWSPeer::get_current_outbound_buffered_amount() const { } bool EMWSPeer::was_string_packet() const { - return _is_string; + return was_string; } -bool EMWSPeer::is_connected_to_host() const { - return peer_sock != -1; +void EMWSPeer::_clear() { + if (peer_sock != -1) { + godot_js_websocket_destroy(peer_sock); + peer_sock = -1; + } + ready_state = STATE_CLOSED; + was_string = 0; + close_code = -1; + close_reason.clear(); + selected_protocol.clear(); + requested_url.clear(); + in_buffer.clear(); + packet_buffer.clear(); } void EMWSPeer::close(int p_code, String p_reason) { - if (peer_sock != -1) { - godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data()); + if (p_code < 0) { + if (peer_sock != -1) { + godot_js_websocket_destroy(peer_sock); + peer_sock = -1; + } + ready_state = STATE_CLOSED; + } + if (ready_state == STATE_CONNECTING || ready_state == STATE_OPEN) { + ready_state = STATE_CLOSING; + if (peer_sock != -1) { + godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data()); + } else { + ready_state = STATE_CLOSED; + } } - _is_string = 0; - _in_buffer.clear(); - peer_sock = -1; + in_buffer.clear(); + packet_buffer.clear(); +} + +void EMWSPeer::poll() { + // Automatically polled by the navigator. +} + +WebSocketPeer::State EMWSPeer::get_ready_state() const { + return ready_state; +} + +int EMWSPeer::get_close_code() const { + return close_code; +} + +String EMWSPeer::get_close_reason() const { + return close_reason; +} + +String EMWSPeer::get_selected_protocol() const { + return selected_protocol; +} + +String EMWSPeer::get_requested_url() const { + return requested_url; } IPAddress EMWSPeer::get_connected_host() const { @@ -122,11 +229,10 @@ void EMWSPeer::set_no_delay(bool p_enabled) { } EMWSPeer::EMWSPeer() { - close(); } EMWSPeer::~EMWSPeer() { - close(); + _clear(); } #endif // WEB_ENABLED diff --git a/modules/websocket/emws_peer.h b/modules/websocket/emws_peer.h index cdbc9212a5..322cc3b700 100644 --- a/modules/websocket/emws_peer.h +++ b/modules/websocket/emws_peer.h @@ -54,33 +54,53 @@ extern void godot_js_websocket_destroy(int p_id); } class EMWSPeer : public WebSocketPeer { - GDCIIMPL(EMWSPeer, WebSocketPeer); - private: int peer_sock = -1; - WriteMode write_mode = WRITE_MODE_BINARY; - Vector<uint8_t> _packet_buffer; - PacketBuffer<uint8_t> _in_buffer; - uint8_t _is_string = 0; - int _out_buf_size = 0; + State ready_state = STATE_CLOSED; + Vector<uint8_t> packet_buffer; + PacketBuffer<uint8_t> in_buffer; + uint8_t was_string = 0; + int close_code = -1; + String close_reason; + String selected_protocol; + String requested_url; + + static WebSocketPeer *_create() { return memnew(EMWSPeer); } + static void _esws_on_connect(void *obj, char *proto); + static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); + static void _esws_on_error(void *obj); + static void _esws_on_close(void *obj, int code, const char *reason, int was_clean); + + void _clear(); + Error _send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary); public: - Error read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string); - void set_sock(int p_sock, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size); + static void initialize() { WebSocketPeer::_create = EMWSPeer::_create; } + + // PacketPeer virtual int get_available_packet_count() const override; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override; virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; - virtual int get_max_packet_size() const override { return _packet_buffer.size(); }; - virtual int get_current_outbound_buffered_amount() const override; + virtual int get_max_packet_size() const override { return packet_buffer.size(); }; + // WebSocketPeer + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override; + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) override; + virtual Error accept_stream(Ref<StreamPeer> p_stream) override; virtual void close(int p_code = 1000, String p_reason = "") override; - virtual bool is_connected_to_host() const override; + virtual void poll() override; + + virtual State get_ready_state() const override; + virtual int get_close_code() const override; + virtual String get_close_reason() const override; + virtual int get_current_outbound_buffered_amount() const override; + virtual IPAddress get_connected_host() const override; virtual uint16_t get_connected_port() const override; + virtual String get_selected_protocol() const override; + virtual String get_requested_url() const override; - virtual WriteMode get_write_mode() const override; - virtual void set_write_mode(WriteMode p_mode) override; virtual bool was_string_packet() const override; virtual void set_no_delay(bool p_enabled) override; diff --git a/modules/websocket/packet_buffer.h b/modules/websocket/packet_buffer.h index 7b4a164576..cd81dc43cd 100644 --- a/modules/websocket/packet_buffer.h +++ b/modules/websocket/packet_buffer.h @@ -41,32 +41,29 @@ private: T info; } _Packet; - RingBuffer<_Packet> _packets; + Vector<_Packet> _packets; + int _queued = 0; + int _write_pos = 0; + int _read_pos = 0; RingBuffer<uint8_t> _payload; public: Error write_packet(const uint8_t *p_payload, uint32_t p_size, const T *p_info) { -#ifdef TOOLS_ENABLED - // Verbose buffer warnings - if (p_payload && _payload.space_left() < (int32_t)p_size) { - ERR_PRINT("Buffer payload full! Dropping data."); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); - } - if (p_info && _packets.space_left() < 1) { - ERR_PRINT("Too many packets in queue! Dropping data."); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); - } -#else - ERR_FAIL_COND_V(p_payload && (uint32_t)_payload.space_left() < p_size, ERR_OUT_OF_MEMORY); - ERR_FAIL_COND_V(p_info && _packets.space_left() < 1, ERR_OUT_OF_MEMORY); -#endif + ERR_FAIL_COND_V_MSG(p_payload && (uint32_t)_payload.space_left() < p_size, ERR_OUT_OF_MEMORY, "Buffer payload full! Dropping data."); + ERR_FAIL_COND_V_MSG(p_info && _queued >= _packets.size(), ERR_OUT_OF_MEMORY, "Too many packets in queue! Dropping data."); // If p_info is nullptr, only the payload is written if (p_info) { + ERR_FAIL_COND_V(_write_pos > _packets.size(), ERR_OUT_OF_MEMORY); _Packet p; p.size = p_size; - memcpy(&p.info, p_info, sizeof(T)); - _packets.write(p); + p.info = *p_info; + _packets.write[_write_pos] = p; + _queued += 1; + _write_pos++; + if (_write_pos >= _packets.size()) { + _write_pos = 0; + } } // If p_payload is nullptr, only the packet information is written. @@ -78,9 +75,14 @@ public: } Error read_packet(uint8_t *r_payload, int p_bytes, T *r_info, int &r_read) { - ERR_FAIL_COND_V(_packets.data_left() < 1, ERR_UNAVAILABLE); - _Packet p; - _packets.read(&p, 1); + ERR_FAIL_COND_V(_queued < 1, ERR_UNAVAILABLE); + _Packet p = _packets[_read_pos]; + _read_pos += 1; + if (_read_pos >= _packets.size()) { + _read_pos = 0; + } + _queued -= 1; + ERR_FAIL_COND_V(_payload.data_left() < (int)p.size, ERR_BUG); ERR_FAIL_COND_V(p_bytes < (int)p.size, ERR_OUT_OF_MEMORY); @@ -90,22 +92,24 @@ public: return OK; } - void discard_payload(int p_size) { - _packets.decrease_write(p_size); - } - - void resize(int p_pkt_shift, int p_buf_shift) { - _packets.resize(p_pkt_shift); + void resize(int p_buf_shift, int p_max_packets) { _payload.resize(p_buf_shift); + _packets.resize(p_max_packets); + _read_pos = 0; + _write_pos = 0; + _queued = 0; } int packets_left() const { - return _packets.data_left(); + return _queued; } void clear() { _payload.resize(0); _packets.resize(0); + _read_pos = 0; + _write_pos = 0; + _queued = 0; } PacketBuffer() { diff --git a/modules/websocket/register_types.cpp b/modules/websocket/register_types.cpp index faa7021b2f..691b031bbd 100644 --- a/modules/websocket/register_types.cpp +++ b/modules/websocket/register_types.cpp @@ -33,16 +33,13 @@ #include "core/config/project_settings.h" #include "core/error/error_macros.h" -#include "websocket_client.h" -#include "websocket_server.h" +#include "websocket_multiplayer_peer.h" +#include "websocket_peer.h" #ifdef WEB_ENABLED -#include "emscripten.h" -#include "emws_client.h" #include "emws_peer.h" #else -#include "wsl_client.h" -#include "wsl_server.h" +#include "wsl_peer.h" #endif #ifdef TOOLS_ENABLED @@ -60,17 +57,12 @@ static void _editor_init_callback() { void initialize_websocket_module(ModuleInitializationLevel p_level) { if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { #ifdef WEB_ENABLED - EMWSPeer::make_default(); - EMWSClient::make_default(); + EMWSPeer::initialize(); #else - WSLPeer::make_default(); - WSLClient::make_default(); - WSLServer::make_default(); + WSLPeer::initialize(); #endif - GDREGISTER_ABSTRACT_CLASS(WebSocketMultiplayerPeer); - ClassDB::register_custom_instance_class<WebSocketServer>(); - ClassDB::register_custom_instance_class<WebSocketClient>(); + GDREGISTER_CLASS(WebSocketMultiplayerPeer); ClassDB::register_custom_instance_class<WebSocketPeer>(); } @@ -85,4 +77,7 @@ void uninitialize_websocket_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } +#ifndef WEB_ENABLED + WSLPeer::deinitialize(); +#endif } diff --git a/modules/websocket/remote_debugger_peer_websocket.cpp b/modules/websocket/remote_debugger_peer_websocket.cpp index f703873cbf..fc4f51b59b 100644 --- a/modules/websocket/remote_debugger_peer_websocket.cpp +++ b/modules/websocket/remote_debugger_peer_websocket.cpp @@ -36,27 +36,35 @@ Error RemoteDebuggerPeerWebSocket::connect_to_host(const String &p_uri) { Vector<String> protocols; protocols.push_back("binary"); // Compatibility for emscripten TCP-to-WebSocket. - ws_client->connect_to_url(p_uri, protocols); - ws_client->poll(); + ws_peer = Ref<WebSocketPeer>(WebSocketPeer::create()); + ws_peer->set_supported_protocols(protocols); - if (ws_client->get_connection_status() == WebSocketClient::CONNECTION_DISCONNECTED) { - ERR_PRINT("Remote Debugger: Unable to connect. Status: " + String::num(ws_client->get_connection_status()) + "."); + ws_peer->set_max_queued_packets(max_queued_messages); + ws_peer->set_inbound_buffer_size((1 << 23) - 1); + ws_peer->set_outbound_buffer_size((1 << 23) - 1); + + Error err = ws_peer->connect_to_url(p_uri); + ERR_FAIL_COND_V(err != OK, err); + + ws_peer->poll(); + WebSocketPeer::State ready_state = ws_peer->get_ready_state(); + if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) { + ERR_PRINT(vformat("Remote Debugger: Unable to connect. State: %s.", ws_peer->get_ready_state())); return FAILED; } - ws_peer = ws_client->get_peer(1); - return OK; } bool RemoteDebuggerPeerWebSocket::is_peer_connected() { - return ws_peer.is_valid() && ws_peer->is_connected_to_host(); + return ws_peer.is_valid() && (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN || ws_peer->get_ready_state() == WebSocketPeer::STATE_CONNECTING); } void RemoteDebuggerPeerWebSocket::poll() { - ws_client->poll(); + ERR_FAIL_COND(ws_peer.is_null()); + ws_peer->poll(); - while (ws_peer->is_connected_to_host() && ws_peer->get_available_packet_count() > 0 && in_queue.size() < max_queued_messages) { + while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && ws_peer->get_available_packet_count() > 0 && in_queue.size() < max_queued_messages) { Variant var; Error err = ws_peer->get_var(var); ERR_CONTINUE(err != OK); @@ -64,7 +72,7 @@ void RemoteDebuggerPeerWebSocket::poll() { in_queue.push_back(var); } - while (ws_peer->is_connected_to_host() && out_queue.size() > 0) { + while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && out_queue.size() > 0) { Array var = out_queue[0]; Error err = ws_peer->put_var(var); ERR_BREAK(err != OK); // Peer buffer full? @@ -73,7 +81,7 @@ void RemoteDebuggerPeerWebSocket::poll() { } int RemoteDebuggerPeerWebSocket::get_max_message_size() const { - return 8 << 20; // 8 Mib + return ws_peer->get_max_packet_size(); } bool RemoteDebuggerPeerWebSocket::has_message() { @@ -99,7 +107,6 @@ void RemoteDebuggerPeerWebSocket::close() { if (ws_peer.is_valid()) { ws_peer.unref(); } - ws_client->disconnect_from_host(); } bool RemoteDebuggerPeerWebSocket::can_block() const { @@ -111,14 +118,13 @@ bool RemoteDebuggerPeerWebSocket::can_block() const { } RemoteDebuggerPeerWebSocket::RemoteDebuggerPeerWebSocket(Ref<WebSocketPeer> p_peer) { -#ifdef WEB_ENABLED - ws_client = Ref<WebSocketClient>(memnew(EMWSClient)); -#else - ws_client = Ref<WebSocketClient>(memnew(WSLClient)); -#endif max_queued_messages = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages"); - ws_client->set_buffers(8192, max_queued_messages, 8192, max_queued_messages); ws_peer = p_peer; + if (ws_peer.is_valid()) { + ws_peer->set_max_queued_packets(max_queued_messages); + ws_peer->set_inbound_buffer_size((1 << 23) - 1); + ws_peer->set_outbound_buffer_size((1 << 23) - 1); + } } RemoteDebuggerPeer *RemoteDebuggerPeerWebSocket::create(const String &p_uri) { diff --git a/modules/websocket/remote_debugger_peer_websocket.h b/modules/websocket/remote_debugger_peer_websocket.h index 0292de68ad..4d496ae891 100644 --- a/modules/websocket/remote_debugger_peer_websocket.h +++ b/modules/websocket/remote_debugger_peer_websocket.h @@ -33,14 +33,9 @@ #include "core/debugger/remote_debugger_peer.h" -#ifdef WEB_ENABLED -#include "emws_client.h" -#else -#include "wsl_client.h" -#endif +#include "websocket_peer.h" class RemoteDebuggerPeerWebSocket : public RemoteDebuggerPeer { - Ref<WebSocketClient> ws_client; Ref<WebSocketPeer> ws_peer; List<Array> in_queue; List<Array> out_queue; diff --git a/modules/websocket/websocket_client.cpp b/modules/websocket/websocket_client.cpp deleted file mode 100644 index 0b2d5d1918..0000000000 --- a/modules/websocket/websocket_client.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/*************************************************************************/ -/* websocket_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#include "websocket_client.h" - -GDCINULL(WebSocketClient); - -WebSocketClient::WebSocketClient() { -} - -WebSocketClient::~WebSocketClient() { -} - -Error WebSocketClient::connect_to_url(String p_url, const Vector<String> p_protocols, bool gd_mp_api, const Vector<String> p_custom_headers) { - _is_multiplayer = gd_mp_api; - - String host = p_url; - String path; - String scheme; - int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); - ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); - - bool tls = false; - if (scheme == "wss://") { - tls = true; - } - if (port == 0) { - port = tls ? 443 : 80; - } - if (path.is_empty()) { - path = "/"; - } - return connect_to_host(host, path, port, tls, p_protocols, p_custom_headers); -} - -void WebSocketClient::set_verify_tls_enabled(bool p_verify_tls) { - verify_tls = p_verify_tls; -} - -bool WebSocketClient::is_verify_tls_enabled() const { - return verify_tls; -} - -Ref<X509Certificate> WebSocketClient::get_trusted_tls_certificate() const { - return tls_cert; -} - -void WebSocketClient::set_trusted_tls_certificate(Ref<X509Certificate> p_cert) { - ERR_FAIL_COND(get_connection_status() != CONNECTION_DISCONNECTED); - tls_cert = p_cert; -} - -bool WebSocketClient::is_server() const { - return false; -} - -void WebSocketClient::_on_peer_packet() { - if (_is_multiplayer) { - _process_multiplayer(get_peer(1), 1); - } else { - emit_signal(SNAME("data_received")); - } -} - -void WebSocketClient::_on_connect(String p_protocol) { - if (_is_multiplayer) { - // need to wait for ID confirmation... - } else { - emit_signal(SNAME("connection_established"), p_protocol); - } -} - -void WebSocketClient::_on_close_request(int p_code, String p_reason) { - emit_signal(SNAME("server_close_request"), p_code, p_reason); -} - -void WebSocketClient::_on_disconnect(bool p_was_clean) { - if (_is_multiplayer) { - emit_signal(SNAME("connection_failed")); - } else { - emit_signal(SNAME("connection_closed"), p_was_clean); - } -} - -void WebSocketClient::_on_error() { - if (_is_multiplayer) { - emit_signal(SNAME("connection_failed")); - } else { - emit_signal(SNAME("connection_error")); - } -} - -void WebSocketClient::_bind_methods() { - ClassDB::bind_method(D_METHOD("connect_to_url", "url", "protocols", "gd_mp_api", "custom_headers"), &WebSocketClient::connect_to_url, DEFVAL(Vector<String>()), DEFVAL(false), DEFVAL(Vector<String>())); - ClassDB::bind_method(D_METHOD("disconnect_from_host", "code", "reason"), &WebSocketClient::disconnect_from_host, DEFVAL(1000), DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketClient::get_connected_host); - ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketClient::get_connected_port); - ClassDB::bind_method(D_METHOD("set_verify_tls_enabled", "enabled"), &WebSocketClient::set_verify_tls_enabled); - ClassDB::bind_method(D_METHOD("is_verify_tls_enabled"), &WebSocketClient::is_verify_tls_enabled); - - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "verify_tls", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_verify_tls_enabled", "is_verify_tls_enabled"); - - ClassDB::bind_method(D_METHOD("get_trusted_tls_certificate"), &WebSocketClient::get_trusted_tls_certificate); - ClassDB::bind_method(D_METHOD("set_trusted_tls_certificate", "cert"), &WebSocketClient::set_trusted_tls_certificate); - - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "trusted_tls_certificate", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_trusted_tls_certificate", "get_trusted_tls_certificate"); - - ADD_SIGNAL(MethodInfo("data_received")); - ADD_SIGNAL(MethodInfo("connection_established", PropertyInfo(Variant::STRING, "protocol"))); - ADD_SIGNAL(MethodInfo("server_close_request", PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); - ADD_SIGNAL(MethodInfo("connection_closed", PropertyInfo(Variant::BOOL, "was_clean_close"))); - ADD_SIGNAL(MethodInfo("connection_error")); -} diff --git a/modules/websocket/websocket_client.h b/modules/websocket/websocket_client.h deleted file mode 100644 index e747aee4e4..0000000000 --- a/modules/websocket/websocket_client.h +++ /dev/null @@ -1,75 +0,0 @@ -/*************************************************************************/ -/* websocket_client.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEBSOCKET_CLIENT_H -#define WEBSOCKET_CLIENT_H - -#include "core/crypto/crypto.h" -#include "core/error/error_list.h" -#include "websocket_multiplayer_peer.h" -#include "websocket_peer.h" - -class WebSocketClient : public WebSocketMultiplayerPeer { - GDCLASS(WebSocketClient, WebSocketMultiplayerPeer); - GDCICLASS(WebSocketClient); - -protected: - Ref<WebSocketPeer> _peer; - bool verify_tls = true; - Ref<X509Certificate> tls_cert; - - static void _bind_methods(); - -public: - Error connect_to_url(String p_url, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false, const Vector<String> p_custom_headers = Vector<String>()); - - void set_verify_tls_enabled(bool p_verify_tls); - bool is_verify_tls_enabled() const; - Ref<X509Certificate> get_trusted_tls_certificate() const; - void set_trusted_tls_certificate(Ref<X509Certificate> p_cert); - - virtual Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) = 0; - virtual void disconnect_from_host(int p_code = 1000, String p_reason = "") = 0; - virtual IPAddress get_connected_host() const = 0; - virtual uint16_t get_connected_port() const = 0; - - virtual bool is_server() const override; - - void _on_peer_packet(); - void _on_connect(String p_protocol); - void _on_close_request(int p_code, String p_reason); - void _on_disconnect(bool p_was_clean); - void _on_error(); - - WebSocketClient(); - ~WebSocketClient(); -}; - -#endif // WEBSOCKET_CLIENT_H diff --git a/modules/websocket/websocket_macros.h b/modules/websocket/websocket_macros.h deleted file mode 100644 index b03bd8f45c..0000000000 --- a/modules/websocket/websocket_macros.h +++ /dev/null @@ -1,66 +0,0 @@ -/*************************************************************************/ -/* websocket_macros.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEBSOCKET_MACROS_H -#define WEBSOCKET_MACROS_H - -// Defaults per peer buffers, 1024 packets with a shared 65536 bytes payload. -#define DEF_PKT_SHIFT 10 -#define DEF_BUF_SHIFT 16 - -#define GDCICLASS(CNAME) \ -public: \ - static CNAME *(*_create)(); \ - \ - static Ref<CNAME> create_ref() { \ - if (!_create) \ - return Ref<CNAME>(); \ - return Ref<CNAME>(_create()); \ - } \ - \ - static CNAME *create() { \ - if (!_create) \ - return nullptr; \ - return _create(); \ - } \ - \ -protected: - -#define GDCINULL(CNAME) \ - CNAME *(*CNAME::_create)() = nullptr; - -#define GDCIIMPL(IMPNAME, CNAME) \ -public: \ - static CNAME *_create() { return memnew(IMPNAME); } \ - static void make_default() { CNAME::_create = IMPNAME::_create; } \ - \ -protected: - -#endif // WEBSOCKET_MACROS_H diff --git a/modules/websocket/websocket_multiplayer_peer.cpp b/modules/websocket/websocket_multiplayer_peer.cpp index 7a3bbf1c47..00b96ec587 100644 --- a/modules/websocket/websocket_multiplayer_peer.cpp +++ b/modules/websocket/websocket_multiplayer_peer.cpp @@ -33,70 +33,118 @@ #include "core/os/os.h" WebSocketMultiplayerPeer::WebSocketMultiplayerPeer() { + peer_config = Ref<WebSocketPeer>(WebSocketPeer::create()); } WebSocketMultiplayerPeer::~WebSocketMultiplayerPeer() { _clear(); } +Ref<WebSocketPeer> WebSocketMultiplayerPeer::_create_peer() { + Ref<WebSocketPeer> peer = Ref<WebSocketPeer>(WebSocketPeer::create()); + peer->set_supported_protocols(get_supported_protocols()); + peer->set_handshake_headers(get_handshake_headers()); + peer->set_inbound_buffer_size(get_inbound_buffer_size()); + peer->set_outbound_buffer_size(get_outbound_buffer_size()); + peer->set_max_queued_packets(get_max_queued_packets()); + return peer; +} + void WebSocketMultiplayerPeer::_clear() { - _peer_map.clear(); - if (_current_packet.data != nullptr) { - memfree(_current_packet.data); + connection_status = CONNECTION_DISCONNECTED; + unique_id = 0; + peers_map.clear(); + use_tls = false; + tcp_server.unref(); + pending_peers.clear(); + tls_certificate.unref(); + tls_key.unref(); + if (current_packet.data != nullptr) { + memfree(current_packet.data); } - for (Packet &E : _incoming_packets) { + for (Packet &E : incoming_packets) { memfree(E.data); E.data = nullptr; } - _incoming_packets.clear(); + incoming_packets.clear(); } void WebSocketMultiplayerPeer::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_buffers", "input_buffer_size_kb", "input_max_packets", "output_buffer_size_kb", "output_max_packets"), &WebSocketMultiplayerPeer::set_buffers); + ClassDB::bind_method(D_METHOD("create_client", "url", "verify_tls", "tls_certificate"), &WebSocketMultiplayerPeer::create_client, DEFVAL(true), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("create_server", "port", "bind_address", "tls_key", "tls_certificate"), &WebSocketMultiplayerPeer::create_server, DEFVAL("*"), DEFVAL(Ref<CryptoKey>()), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("close"), &WebSocketMultiplayerPeer::close); + ClassDB::bind_method(D_METHOD("get_peer", "peer_id"), &WebSocketMultiplayerPeer::get_peer); + ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketMultiplayerPeer::get_peer_address); + ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketMultiplayerPeer::get_peer_port); + ClassDB::bind_method(D_METHOD("disconnect_peer", "id", "code", "reason"), &WebSocketMultiplayerPeer::disconnect_peer, DEFVAL(1000), DEFVAL("")); + + ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketMultiplayerPeer::get_supported_protocols); + ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketMultiplayerPeer::set_supported_protocols); + + ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketMultiplayerPeer::get_handshake_headers); + ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketMultiplayerPeer::set_handshake_headers); + + ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketMultiplayerPeer::get_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_inbound_buffer_size); + + ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketMultiplayerPeer::get_outbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_outbound_buffer_size); - ADD_SIGNAL(MethodInfo("peer_packet", PropertyInfo(Variant::INT, "peer_source"))); + ClassDB::bind_method(D_METHOD("get_handshake_timeout"), &WebSocketMultiplayerPeer::get_handshake_timeout); + ClassDB::bind_method(D_METHOD("set_handshake_timeout", "timeout"), &WebSocketMultiplayerPeer::set_handshake_timeout); + + ClassDB::bind_method(D_METHOD("set_max_queued_packets", "max_queued_packets"), &WebSocketMultiplayerPeer::set_max_queued_packets); + ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketMultiplayerPeer::get_max_queued_packets); + + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size"); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "handshake_timeout"), "set_handshake_timeout", "get_handshake_timeout"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets"); } // // PacketPeer // int WebSocketMultiplayerPeer::get_available_packet_count() const { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, 0, "Please use get_peer(ID).get_available_packet_count to get available packet count from peers when not using the MultiplayerAPI."); - - return _incoming_packets.size(); + return incoming_packets.size(); } Error WebSocketMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, ERR_UNCONFIGURED, "Please use get_peer(ID).get_packet/var to communicate with peers when not using the MultiplayerAPI."); + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED); r_buffer_size = 0; - if (_current_packet.data != nullptr) { - memfree(_current_packet.data); - _current_packet.data = nullptr; + if (current_packet.data != nullptr) { + memfree(current_packet.data); + current_packet.data = nullptr; } - ERR_FAIL_COND_V(_incoming_packets.size() == 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V(incoming_packets.size() == 0, ERR_UNAVAILABLE); - _current_packet = _incoming_packets.front()->get(); - _incoming_packets.pop_front(); + current_packet = incoming_packets.front()->get(); + incoming_packets.pop_front(); - *r_buffer = _current_packet.data; - r_buffer_size = _current_packet.size; + *r_buffer = current_packet.data; + r_buffer_size = current_packet.size; return OK; } Error WebSocketMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, ERR_UNCONFIGURED, "Please use get_peer(ID).put_packet/var to communicate with peers when not using the MultiplayerAPI."); + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED); - Vector<uint8_t> buffer = _make_pkt(SYS_NONE, get_unique_id(), _target_peer, p_buffer, p_buffer_size); + Vector<uint8_t> buffer = _make_pkt(SYS_NONE, get_unique_id(), target_peer, p_buffer, p_buffer_size); if (is_server()) { - return _server_relay(1, _target_peer, &(buffer.ptr()[0]), buffer.size()); + return _server_relay(1, target_peer, &(buffer.ptr()[0]), buffer.size()); } else { return get_peer(1)->put_packet(&(buffer.ptr()[0]), buffer.size()); } @@ -106,23 +154,225 @@ Error WebSocketMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer // MultiplayerPeer // void WebSocketMultiplayerPeer::set_target_peer(int p_target_peer) { - _target_peer = p_target_peer; + target_peer = p_target_peer; } int WebSocketMultiplayerPeer::get_packet_peer() const { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, 1, "This function is not available when not using the MultiplayerAPI."); - ERR_FAIL_COND_V(_incoming_packets.size() == 0, 1); + ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); - return _incoming_packets.front()->get().source; + return incoming_packets.front()->get().source; } int WebSocketMultiplayerPeer::get_unique_id() const { - return _peer_id; + return unique_id; +} + +int WebSocketMultiplayerPeer::get_max_packet_size() const { + return get_outbound_buffer_size() - PROTO_SIZE; +} + +Error WebSocketMultiplayerPeer::create_server(int p_port, IPAddress p_bind_ip, Ref<CryptoKey> p_tls_key, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE); + _clear(); + tcp_server.instantiate(); + Error err = tcp_server->listen(p_port, p_bind_ip); + if (err != OK) { + tcp_server.unref(); + return err; + } + unique_id = 1; + connection_status = CONNECTION_CONNECTED; + // TLS config + tls_key = p_tls_key; + tls_certificate = p_tls_certificate; + if (tls_key.is_valid() && tls_certificate.is_valid()) { + use_tls = true; + } + return OK; +} + +Error WebSocketMultiplayerPeer::create_client(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE); + _clear(); + Ref<WebSocketPeer> peer = _create_peer(); + Error err = peer->connect_to_url(p_url, p_verify_tls, p_tls_certificate); + if (err != OK) { + return err; + } + PendingPeer pending; + pending.time = OS::get_singleton()->get_ticks_msec(); + pending_peers[1] = pending; + peers_map[1] = peer; + connection_status = CONNECTION_CONNECTING; + return OK; +} + +bool WebSocketMultiplayerPeer::is_server() const { + return tcp_server.is_valid(); +} + +void WebSocketMultiplayerPeer::_poll_client() { + ERR_FAIL_COND(connection_status == CONNECTION_DISCONNECTED); // Bug. + ERR_FAIL_COND(!peers_map.has(1) || peers_map[1].is_null()); // Bug. + Ref<WebSocketPeer> peer = peers_map[1]; + peer->poll(); // Update state and fetch packets. + WebSocketPeer::State ready_state = peer->get_ready_state(); + if (ready_state == WebSocketPeer::STATE_OPEN) { + while (peer->get_available_packet_count()) { + _process_multiplayer(peer, 1); + } + } else if (peer->get_ready_state() == WebSocketPeer::STATE_CLOSED) { + if (connection_status == CONNECTION_CONNECTED) { + emit_signal(SNAME("server_disconnected")); + } else { + emit_signal(SNAME("connection_failed")); + } + _clear(); + return; + } + if (connection_status == CONNECTION_CONNECTING) { + // Still connecting + ERR_FAIL_COND(!pending_peers.has(1)); // Bug. + if (OS::get_singleton()->get_ticks_msec() - pending_peers[1].time > handshake_timeout) { + print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001)); + emit_signal(SNAME("connection_failed")); + _clear(); + return; + } + } +} + +void WebSocketMultiplayerPeer::_poll_server() { + ERR_FAIL_COND(connection_status != CONNECTION_CONNECTED); // Bug. + ERR_FAIL_COND(tcp_server.is_null() || !tcp_server->is_listening()); // Bug. + + // Accept new connections. + if (!is_refusing_new_connections() && tcp_server->is_connection_available()) { + PendingPeer peer; + peer.time = OS::get_singleton()->get_ticks_msec(); + peer.tcp = tcp_server->take_connection(); + peer.connection = peer.tcp; + pending_peers[generate_unique_id()] = peer; + } + + // Process pending peers. + HashSet<int> to_remove; + for (KeyValue<int, PendingPeer> &E : pending_peers) { + PendingPeer &peer = E.value; + int id = E.key; + + if (OS::get_singleton()->get_ticks_msec() - peer.time > handshake_timeout) { + print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001)); + to_remove.insert(id); + continue; + } + + if (peer.ws.is_valid()) { + peer.ws->poll(); + WebSocketPeer::State state = peer.ws->get_ready_state(); + if (state == WebSocketPeer::STATE_OPEN) { + // Connected. + to_remove.insert(id); + if (is_refusing_new_connections()) { + // The user does not want new connections, dropping it. + continue; + } + peers_map[id] = peer.ws; + _send_ack(peer.ws, id); + emit_signal("peer_connected", id); + continue; + } else if (state == WebSocketPeer::STATE_CONNECTING) { + continue; // Still connecting. + } + to_remove.insert(id); // Error. + continue; + } + if (peer.tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + to_remove.insert(id); // Error. + continue; + } + if (!use_tls) { + peer.ws = _create_peer(); + peer.ws->accept_stream(peer.tcp); + continue; + } else { + if (peer.connection == peer.tcp) { + Ref<StreamPeerTLS> tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); + Error err = tls->accept_stream(peer.tcp, tls_key, tls_certificate); + if (err != OK) { + to_remove.insert(id); + continue; + } + } + Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(peer.connection); + tls->poll(); + if (tls->get_status() == StreamPeerTLS::STATUS_CONNECTED) { + peer.ws = _create_peer(); + peer.ws->accept_stream(peer.connection); + continue; + } else if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + // Still connecting. + continue; + } else { + // Error + to_remove.insert(id); + } + } + } + + // Remove disconnected pending peers. + for (const int &pid : to_remove) { + pending_peers.erase(pid); + } + to_remove.clear(); + + // Process connected peers. + for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { + Ref<WebSocketPeer> ws = E.value; + int id = E.key; + ws->poll(); + if (ws->get_ready_state() != WebSocketPeer::STATE_OPEN) { + to_remove.insert(id); // Disconnected. + continue; + } + // Fetch packets + int pkts = ws->get_available_packet_count(); + while (pkts && ws->get_ready_state() == WebSocketPeer::STATE_OPEN) { + _process_multiplayer(ws, id); + pkts--; + } + } + + // Remove disconnected peers. + for (const int &pid : to_remove) { + emit_signal(SNAME("peer_disconnected"), pid); + peers_map.erase(pid); + } +} + +void WebSocketMultiplayerPeer::poll() { + if (connection_status == CONNECTION_DISCONNECTED) { + return; + } + if (is_server()) { + _poll_server(); + } else { + _poll_client(); + } +} + +MultiplayerPeer::ConnectionStatus WebSocketMultiplayerPeer::get_connection_status() const { + return connection_status; +} + +Ref<WebSocketPeer> WebSocketMultiplayerPeer::get_peer(int p_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_id), Ref<WebSocketPeer>()); + return peers_map[p_id]; } void WebSocketMultiplayerPeer::_send_sys(Ref<WebSocketPeer> p_peer, uint8_t p_type, int32_t p_peer_id) { ERR_FAIL_COND(!p_peer.is_valid()); - ERR_FAIL_COND(!p_peer->is_connected_to_host()); + ERR_FAIL_COND(p_peer->get_ready_state() != WebSocketPeer::STATE_OPEN); Vector<uint8_t> message = _make_pkt(p_type, 1, 0, (uint8_t *)&p_peer_id, 4); p_peer->put_packet(&(message.ptr()[0]), message.size()); @@ -141,31 +391,34 @@ Vector<uint8_t> WebSocketMultiplayerPeer::_make_pkt(uint8_t p_type, int32_t p_fr return out; } -void WebSocketMultiplayerPeer::_send_add(int32_t p_peer_id) { +void WebSocketMultiplayerPeer::_send_ack(Ref<WebSocketPeer> p_peer, int32_t p_peer_id) { + ERR_FAIL_COND(p_peer.is_null()); // First of all, confirm the ID! - _send_sys(get_peer(p_peer_id), SYS_ID, p_peer_id); + _send_sys(p_peer, SYS_ID, p_peer_id); // Then send the server peer (which will trigger connection_succeded in client) - _send_sys(get_peer(p_peer_id), SYS_ADD, 1); + _send_sys(p_peer, SYS_ADD, 1); + + for (const KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { + ERR_CONTINUE(E.value.is_null()); - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { int32_t id = E.key; if (p_peer_id == id) { continue; // Skip the newly added peer (already confirmed) } // Send new peer to others - _send_sys(get_peer(id), SYS_ADD, p_peer_id); + _send_sys(E.value, SYS_ADD, p_peer_id); // Send others to new peer - _send_sys(get_peer(p_peer_id), SYS_ADD, id); + _send_sys(E.value, SYS_ADD, id); } } void WebSocketMultiplayerPeer::_send_del(int32_t p_peer_id) { - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { + for (const KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { int32_t id = E.key; if (p_peer_id != id) { - _send_sys(get_peer(id), SYS_DEL, p_peer_id); + _send_sys(E.value, SYS_DEL, p_peer_id); } } } @@ -177,8 +430,7 @@ void WebSocketMultiplayerPeer::_store_pkt(int32_t p_source, int32_t p_dest, cons packet.source = p_source; packet.destination = p_dest; memcpy(packet.data, &p_data[PROTO_SIZE], p_data_size); - _incoming_packets.push_back(packet); - emit_signal(SNAME("peer_packet"), p_source); + incoming_packets.push_back(packet); } Error WebSocketMultiplayerPeer::_server_relay(int32_t p_from, int32_t p_to, const uint8_t *p_buffer, uint32_t p_buffer_size) { @@ -186,7 +438,7 @@ Error WebSocketMultiplayerPeer::_server_relay(int32_t p_from, int32_t p_to, cons return OK; // Will not send to self } else if (p_to == 0) { - for (KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { + for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { if (E.key != p_from) { E.value->put_packet(p_buffer, p_buffer_size); } @@ -194,7 +446,7 @@ Error WebSocketMultiplayerPeer::_server_relay(int32_t p_from, int32_t p_to, cons return OK; // Sent to all but sender } else if (p_to < 0) { - for (KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { + for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { if (E.key != p_from && E.key != -p_to) { E.value->put_packet(p_buffer, p_buffer_size); } @@ -237,26 +489,24 @@ void WebSocketMultiplayerPeer::_process_multiplayer(Ref<WebSocketPeer> p_peer, u ERR_FAIL_COND(type != SYS_NONE); // Only server sends sys messages ERR_FAIL_COND(from != p_peer_id); // Someone is cheating - if (to == 1) { // This is for the server - + if (to == 1) { + // This is for the server _store_pkt(from, to, in_buffer, data_size); } else if (to == 0) { // Broadcast, for us too _store_pkt(from, to, in_buffer, data_size); - } else if (to < 0) { + } else if (to < -1) { // All but one, for us if not excluded - if (_peer_id != -(int32_t)p_peer_id) { - _store_pkt(from, to, in_buffer, data_size); - } + _store_pkt(from, to, in_buffer, data_size); } // Relay if needed (i.e. "to" includes a peer that is not the server) _server_relay(from, to, in_buffer, size); } else { - if (type == SYS_NONE) { // Payload message - + if (type == SYS_NONE) { + // Payload message _store_pkt(from, to, in_buffer, data_size); return; } @@ -268,7 +518,12 @@ void WebSocketMultiplayerPeer::_process_multiplayer(Ref<WebSocketPeer> p_peer, u switch (type) { case SYS_ADD: // Add peer - _peer_map[id] = Ref<WebSocketPeer>(); + if (id != 1) { + peers_map[id] = Ref<WebSocketPeer>(); + } else { + pending_peers.clear(); + connection_status = CONNECTION_CONNECTED; + } emit_signal(SNAME("peer_connected"), id); if (id == 1) { // We just connected to the server emit_signal(SNAME("connection_succeeded")); @@ -276,11 +531,11 @@ void WebSocketMultiplayerPeer::_process_multiplayer(Ref<WebSocketPeer> p_peer, u break; case SYS_DEL: // Remove peer - _peer_map.erase(id); emit_signal(SNAME("peer_disconnected"), id); + peers_map.erase(id); break; case SYS_ID: // Hello, server assigned ID - _peer_id = id; + unique_id = id; break; default: ERR_FAIL_MSG("Invalid multiplayer message."); @@ -288,3 +543,71 @@ void WebSocketMultiplayerPeer::_process_multiplayer(Ref<WebSocketPeer> p_peer, u } } } + +void WebSocketMultiplayerPeer::set_supported_protocols(const Vector<String> &p_protocols) { + peer_config->set_supported_protocols(p_protocols); +} + +Vector<String> WebSocketMultiplayerPeer::get_supported_protocols() const { + return peer_config->get_supported_protocols(); +} + +void WebSocketMultiplayerPeer::set_handshake_headers(const Vector<String> &p_headers) { + peer_config->set_handshake_headers(p_headers); +} + +Vector<String> WebSocketMultiplayerPeer::get_handshake_headers() const { + return peer_config->get_handshake_headers(); +} + +void WebSocketMultiplayerPeer::set_outbound_buffer_size(int p_buffer_size) { + peer_config->set_outbound_buffer_size(p_buffer_size); +} + +int WebSocketMultiplayerPeer::get_outbound_buffer_size() const { + return peer_config->get_outbound_buffer_size(); +} + +void WebSocketMultiplayerPeer::set_inbound_buffer_size(int p_buffer_size) { + peer_config->set_inbound_buffer_size(p_buffer_size); +} + +int WebSocketMultiplayerPeer::get_inbound_buffer_size() const { + return peer_config->get_inbound_buffer_size(); +} + +void WebSocketMultiplayerPeer::set_max_queued_packets(int p_max_queued_packets) { + peer_config->set_max_queued_packets(p_max_queued_packets); +} + +int WebSocketMultiplayerPeer::get_max_queued_packets() const { + return peer_config->get_max_queued_packets(); +} + +float WebSocketMultiplayerPeer::get_handshake_timeout() const { + return handshake_timeout / 1000.0; +} + +void WebSocketMultiplayerPeer::set_handshake_timeout(float p_timeout) { + ERR_FAIL_COND(p_timeout <= 0.0); + handshake_timeout = p_timeout * 1000; +} + +IPAddress WebSocketMultiplayerPeer::get_peer_address(int p_peer_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_peer_id), IPAddress()); + return peers_map[p_peer_id]->get_connected_host(); +} + +int WebSocketMultiplayerPeer::get_peer_port(int p_peer_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_peer_id), 0); + return peers_map[p_peer_id]->get_connected_port(); +} + +void WebSocketMultiplayerPeer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { + ERR_FAIL_COND(!peers_map.has(p_peer_id)); + peers_map[p_peer_id]->close(p_code, p_reason); +} + +void WebSocketMultiplayerPeer::close() { + _clear(); +} diff --git a/modules/websocket/websocket_multiplayer_peer.h b/modules/websocket/websocket_multiplayer_peer.h index 3259e78b3b..8e7b118faa 100644 --- a/modules/websocket/websocket_multiplayer_peer.h +++ b/modules/websocket/websocket_multiplayer_peer.h @@ -32,6 +32,8 @@ #define WEBSOCKET_MULTIPLAYER_PEER_H #include "core/error/error_list.h" +#include "core/io/stream_peer_tls.h" +#include "core/io/tcp_server.h" #include "core/templates/list.h" #include "scene/main/multiplayer_peer.h" #include "websocket_peer.h" @@ -43,6 +45,7 @@ private: Vector<uint8_t> _make_pkt(uint8_t p_type, int32_t p_from, int32_t p_to, const uint8_t *p_data, uint32_t p_data_size); void _store_pkt(int32_t p_source, int32_t p_dest, const uint8_t *p_data, uint32_t p_data_size); Error _server_relay(int32_t p_from, int32_t p_to, const uint8_t *p_buffer, uint32_t p_buffer_size); + Ref<WebSocketPeer> _create_peer(); protected: enum { @@ -61,19 +64,40 @@ protected: uint32_t size = 0; }; - List<Packet> _incoming_packets; - HashMap<int, Ref<WebSocketPeer>> _peer_map; - Packet _current_packet; + struct PendingPeer { + uint64_t time = 0; + Ref<StreamPeerTCP> tcp; + Ref<StreamPeer> connection; + Ref<WebSocketPeer> ws; + }; + + uint64_t handshake_timeout = 3000; + Ref<WebSocketPeer> peer_config; + HashMap<int, PendingPeer> pending_peers; + Ref<TCPServer> tcp_server; + bool use_tls = false; + Ref<X509Certificate> tls_certificate; + Ref<CryptoKey> tls_key; + + ConnectionStatus connection_status = CONNECTION_DISCONNECTED; - bool _is_multiplayer = false; - int _target_peer = 0; - int _peer_id = 0; + List<Packet> incoming_packets; + HashMap<int, Ref<WebSocketPeer>> peers_map; + Packet current_packet; + + int target_peer = 0; + int unique_id = 0; static void _bind_methods(); - void _send_add(int32_t p_peer_id); + void _send_ack(Ref<WebSocketPeer> p_peer, int32_t p_peer_id); void _send_sys(Ref<WebSocketPeer> p_peer, uint8_t p_type, int32_t p_peer_id); void _send_del(int32_t p_peer_id); + void _process_multiplayer(Ref<WebSocketPeer> p_peer, uint32_t p_peer_id); + + void _poll_client(); + void _poll_server(); + void _clear(); public: /* MultiplayerPeer */ @@ -81,17 +105,44 @@ public: int get_packet_peer() const override; int get_unique_id() const override; + virtual int get_max_packet_size() const override; + virtual bool is_server() const override; + virtual void poll() override; + virtual ConnectionStatus get_connection_status() const override; + /* PacketPeer */ virtual int get_available_packet_count() const override; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override; virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; /* WebSocketPeer */ - virtual Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) = 0; - virtual Ref<WebSocketPeer> get_peer(int p_peer_id) const = 0; + virtual Ref<WebSocketPeer> get_peer(int p_peer_id) const; - void _process_multiplayer(Ref<WebSocketPeer> p_peer, uint32_t p_peer_id); - void _clear(); + Error create_client(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate); + Error create_server(int p_port, IPAddress p_bind_ip, Ref<CryptoKey> p_tls_key, Ref<X509Certificate> p_tls_certificate); + + void set_supported_protocols(const Vector<String> &p_protocols); + Vector<String> get_supported_protocols() const; + + void set_handshake_headers(const Vector<String> &p_headers); + Vector<String> get_handshake_headers() const; + + void set_outbound_buffer_size(int p_buffer_size); + int get_outbound_buffer_size() const; + + void set_inbound_buffer_size(int p_buffer_size); + int get_inbound_buffer_size() const; + + float get_handshake_timeout() const; + void set_handshake_timeout(float p_timeout); + + IPAddress get_peer_address(int p_peer_id) const; + int get_peer_port(int p_peer_id) const; + void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = ""); + void close(); + + void set_max_queued_packets(int p_max_queued_packets); + int get_max_queued_packets() const; WebSocketMultiplayerPeer(); ~WebSocketMultiplayerPeer(); diff --git a/modules/websocket/websocket_peer.cpp b/modules/websocket/websocket_peer.cpp index a0af9303b8..b46b20bef2 100644 --- a/modules/websocket/websocket_peer.cpp +++ b/modules/websocket/websocket_peer.cpp @@ -30,7 +30,7 @@ #include "websocket_peer.h" -GDCINULL(WebSocketPeer); +WebSocketPeer *(*WebSocketPeer::_create)() = nullptr; WebSocketPeer::WebSocketPeer() { } @@ -39,16 +39,115 @@ WebSocketPeer::~WebSocketPeer() { } void WebSocketPeer::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_write_mode"), &WebSocketPeer::get_write_mode); - ClassDB::bind_method(D_METHOD("set_write_mode", "mode"), &WebSocketPeer::set_write_mode); - ClassDB::bind_method(D_METHOD("is_connected_to_host"), &WebSocketPeer::is_connected_to_host); + ClassDB::bind_method(D_METHOD("connect_to_url", "url", "verify_tls", "trusted_tls_certificate"), &WebSocketPeer::connect_to_url, DEFVAL(true), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("accept_stream", "stream"), &WebSocketPeer::accept_stream); + ClassDB::bind_method(D_METHOD("send", "message", "write_mode"), &WebSocketPeer::_send_bind, DEFVAL(WRITE_MODE_BINARY)); + ClassDB::bind_method(D_METHOD("send_text", "message"), &WebSocketPeer::send_text); ClassDB::bind_method(D_METHOD("was_string_packet"), &WebSocketPeer::was_string_packet); + ClassDB::bind_method(D_METHOD("poll"), &WebSocketPeer::poll); ClassDB::bind_method(D_METHOD("close", "code", "reason"), &WebSocketPeer::close, DEFVAL(1000), DEFVAL("")); ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketPeer::get_connected_host); ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketPeer::get_connected_port); + ClassDB::bind_method(D_METHOD("get_selected_protocol"), &WebSocketPeer::get_selected_protocol); + ClassDB::bind_method(D_METHOD("get_requested_url"), &WebSocketPeer::get_requested_url); ClassDB::bind_method(D_METHOD("set_no_delay", "enabled"), &WebSocketPeer::set_no_delay); ClassDB::bind_method(D_METHOD("get_current_outbound_buffered_amount"), &WebSocketPeer::get_current_outbound_buffered_amount); + ClassDB::bind_method(D_METHOD("get_ready_state"), &WebSocketPeer::get_ready_state); + ClassDB::bind_method(D_METHOD("get_close_code"), &WebSocketPeer::get_close_code); + ClassDB::bind_method(D_METHOD("get_close_reason"), &WebSocketPeer::get_close_reason); + + ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketPeer::_get_supported_protocols); + ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketPeer::set_supported_protocols); + ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketPeer::_get_handshake_headers); + ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketPeer::set_handshake_headers); + + ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketPeer::get_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketPeer::set_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketPeer::get_outbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketPeer::set_outbound_buffer_size); + + ClassDB::bind_method(D_METHOD("set_max_queued_packets", "buffer_size"), &WebSocketPeer::set_max_queued_packets); + ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketPeer::get_max_queued_packets); + + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets"); + BIND_ENUM_CONSTANT(WRITE_MODE_TEXT); BIND_ENUM_CONSTANT(WRITE_MODE_BINARY); + + BIND_ENUM_CONSTANT(STATE_CONNECTING); + BIND_ENUM_CONSTANT(STATE_OPEN); + BIND_ENUM_CONSTANT(STATE_CLOSING); + BIND_ENUM_CONSTANT(STATE_CLOSED); +} + +Error WebSocketPeer::_send_bind(const PackedByteArray &p_message, WriteMode p_mode) { + return send(p_message.ptr(), p_message.size(), p_mode); +} + +Error WebSocketPeer::send_text(const String &p_text) { + const CharString cs = p_text.utf8(); + return send((const uint8_t *)cs.ptr(), cs.length(), WRITE_MODE_TEXT); +} + +void WebSocketPeer::set_supported_protocols(const Vector<String> &p_protocols) { + // Strip edges from protocols. + supported_protocols.resize(p_protocols.size()); + for (int i = 0; i < p_protocols.size(); i++) { + supported_protocols.write[i] = p_protocols[i].strip_edges(); + } +} + +const Vector<String> WebSocketPeer::get_supported_protocols() const { + return supported_protocols; +} + +Vector<String> WebSocketPeer::_get_supported_protocols() const { + Vector<String> out; + out.append_array(supported_protocols); + return out; +} + +void WebSocketPeer::set_handshake_headers(const Vector<String> &p_headers) { + handshake_headers = p_headers; +} + +const Vector<String> WebSocketPeer::get_handshake_headers() const { + return handshake_headers; +} + +Vector<String> WebSocketPeer::_get_handshake_headers() const { + Vector<String> out; + out.append_array(handshake_headers); + return out; +} + +void WebSocketPeer::set_outbound_buffer_size(int p_buffer_size) { + outbound_buffer_size = p_buffer_size; +} + +int WebSocketPeer::get_outbound_buffer_size() const { + return outbound_buffer_size; +} + +void WebSocketPeer::set_inbound_buffer_size(int p_buffer_size) { + inbound_buffer_size = p_buffer_size; +} + +int WebSocketPeer::get_inbound_buffer_size() const { + return inbound_buffer_size; +} + +void WebSocketPeer::set_max_queued_packets(int p_max_queued_packets) { + max_queued_packets = p_max_queued_packets; +} + +int WebSocketPeer::get_max_queued_packets() const { + return max_queued_packets; } diff --git a/modules/websocket/websocket_peer.h b/modules/websocket/websocket_peer.h index 22099f7258..db969dd08e 100644 --- a/modules/websocket/websocket_peer.h +++ b/modules/websocket/websocket_peer.h @@ -31,40 +31,97 @@ #ifndef WEBSOCKET_PEER_H #define WEBSOCKET_PEER_H +#include "core/crypto/crypto.h" #include "core/error/error_list.h" #include "core/io/packet_peer.h" -#include "websocket_macros.h" class WebSocketPeer : public PacketPeer { GDCLASS(WebSocketPeer, PacketPeer); - GDCICLASS(WebSocketPeer); public: + enum State { + STATE_CONNECTING, + STATE_OPEN, + STATE_CLOSING, + STATE_CLOSED + }; + enum WriteMode { WRITE_MODE_TEXT, WRITE_MODE_BINARY, }; + enum { + DEFAULT_BUFFER_SIZE = 65535, + }; + +private: + virtual Error _send_bind(const PackedByteArray &p_data, WriteMode p_mode = WRITE_MODE_BINARY); + protected: + static WebSocketPeer *(*_create)(); + static void _bind_methods(); + Vector<String> supported_protocols; + Vector<String> handshake_headers; + + Vector<String> _get_supported_protocols() const; + Vector<String> _get_handshake_headers() const; + + int outbound_buffer_size = DEFAULT_BUFFER_SIZE; + int inbound_buffer_size = DEFAULT_BUFFER_SIZE; + int max_queued_packets = 2048; + public: - virtual WriteMode get_write_mode() const = 0; - virtual void set_write_mode(WriteMode p_mode) = 0; + static WebSocketPeer *create() { + if (!_create) { + return nullptr; + } + return _create(); + } + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) { return ERR_UNAVAILABLE; }; + virtual Error accept_stream(Ref<StreamPeer> p_stream) = 0; + + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) = 0; virtual void close(int p_code = 1000, String p_reason = "") = 0; - virtual bool is_connected_to_host() const = 0; virtual IPAddress get_connected_host() const = 0; virtual uint16_t get_connected_port() const = 0; virtual bool was_string_packet() const = 0; virtual void set_no_delay(bool p_enabled) = 0; virtual int get_current_outbound_buffered_amount() const = 0; + virtual String get_selected_protocol() const = 0; + virtual String get_requested_url() const = 0; + + virtual void poll() = 0; + virtual State get_ready_state() const = 0; + virtual int get_close_code() const = 0; + virtual String get_close_reason() const = 0; + + Error send_text(const String &p_text); + + void set_supported_protocols(const Vector<String> &p_protocols); + const Vector<String> get_supported_protocols() const; + + void set_handshake_headers(const Vector<String> &p_headers); + const Vector<String> get_handshake_headers() const; + + void set_outbound_buffer_size(int p_buffer_size); + int get_outbound_buffer_size() const; + + void set_inbound_buffer_size(int p_buffer_size); + int get_inbound_buffer_size() const; + + void set_max_queued_packets(int p_max_queued_packets); + int get_max_queued_packets() const; WebSocketPeer(); ~WebSocketPeer(); }; VARIANT_ENUM_CAST(WebSocketPeer::WriteMode); +VARIANT_ENUM_CAST(WebSocketPeer::State); #endif // WEBSOCKET_PEER_H diff --git a/modules/websocket/websocket_server.cpp b/modules/websocket/websocket_server.cpp deleted file mode 100644 index 25a6e420fc..0000000000 --- a/modules/websocket/websocket_server.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/*************************************************************************/ -/* websocket_server.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#include "websocket_server.h" - -GDCINULL(WebSocketServer); - -WebSocketServer::WebSocketServer() { - _peer_id = 1; - bind_ip = IPAddress("*"); -} - -WebSocketServer::~WebSocketServer() { -} - -void WebSocketServer::_bind_methods() { - ClassDB::bind_method(D_METHOD("is_listening"), &WebSocketServer::is_listening); - ClassDB::bind_method(D_METHOD("set_extra_headers", "headers"), &WebSocketServer::set_extra_headers, DEFVAL(Vector<String>())); - ClassDB::bind_method(D_METHOD("listen", "port", "protocols", "gd_mp_api"), &WebSocketServer::listen, DEFVAL(Vector<String>()), DEFVAL(false)); - ClassDB::bind_method(D_METHOD("stop"), &WebSocketServer::stop); - ClassDB::bind_method(D_METHOD("has_peer", "id"), &WebSocketServer::has_peer); - ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketServer::get_peer_address); - ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketServer::get_peer_port); - ClassDB::bind_method(D_METHOD("disconnect_peer", "id", "code", "reason"), &WebSocketServer::disconnect_peer, DEFVAL(1000), DEFVAL("")); - - ClassDB::bind_method(D_METHOD("get_bind_ip"), &WebSocketServer::get_bind_ip); - ClassDB::bind_method(D_METHOD("set_bind_ip", "ip"), &WebSocketServer::set_bind_ip); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "bind_ip"), "set_bind_ip", "get_bind_ip"); - - ClassDB::bind_method(D_METHOD("get_private_key"), &WebSocketServer::get_private_key); - ClassDB::bind_method(D_METHOD("set_private_key", "key"), &WebSocketServer::set_private_key); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "private_key", PROPERTY_HINT_RESOURCE_TYPE, "CryptoKey", PROPERTY_USAGE_NONE), "set_private_key", "get_private_key"); - - ClassDB::bind_method(D_METHOD("get_tls_certificate"), &WebSocketServer::get_tls_certificate); - ClassDB::bind_method(D_METHOD("set_tls_certificate", "cert"), &WebSocketServer::set_tls_certificate); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tls_certificate", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_tls_certificate", "get_tls_certificate"); - - ClassDB::bind_method(D_METHOD("get_ca_chain"), &WebSocketServer::get_ca_chain); - ClassDB::bind_method(D_METHOD("set_ca_chain", "ca_chain"), &WebSocketServer::set_ca_chain); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "ca_chain", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_ca_chain", "get_ca_chain"); - - ClassDB::bind_method(D_METHOD("get_handshake_timeout"), &WebSocketServer::get_handshake_timeout); - ClassDB::bind_method(D_METHOD("set_handshake_timeout", "timeout"), &WebSocketServer::set_handshake_timeout); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "handshake_timeout"), "set_handshake_timeout", "get_handshake_timeout"); - - ADD_SIGNAL(MethodInfo("client_close_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); - ADD_SIGNAL(MethodInfo("client_disconnected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::BOOL, "was_clean_close"))); - ADD_SIGNAL(MethodInfo("client_connected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "protocol"), PropertyInfo(Variant::STRING, "resource_name"))); - ADD_SIGNAL(MethodInfo("data_received", PropertyInfo(Variant::INT, "id"))); -} - -IPAddress WebSocketServer::get_bind_ip() const { - return bind_ip; -} - -void WebSocketServer::set_bind_ip(const IPAddress &p_bind_ip) { - ERR_FAIL_COND(is_listening()); - ERR_FAIL_COND(!p_bind_ip.is_valid() && !p_bind_ip.is_wildcard()); - bind_ip = p_bind_ip; -} - -Ref<CryptoKey> WebSocketServer::get_private_key() const { - return private_key; -} - -void WebSocketServer::set_private_key(Ref<CryptoKey> p_key) { - ERR_FAIL_COND(is_listening()); - private_key = p_key; -} - -Ref<X509Certificate> WebSocketServer::get_tls_certificate() const { - return tls_cert; -} - -void WebSocketServer::set_tls_certificate(Ref<X509Certificate> p_cert) { - ERR_FAIL_COND(is_listening()); - tls_cert = p_cert; -} - -Ref<X509Certificate> WebSocketServer::get_ca_chain() const { - return ca_chain; -} - -void WebSocketServer::set_ca_chain(Ref<X509Certificate> p_ca_chain) { - ERR_FAIL_COND(is_listening()); - ca_chain = p_ca_chain; -} - -float WebSocketServer::get_handshake_timeout() const { - return handshake_timeout / 1000.0; -} - -void WebSocketServer::set_handshake_timeout(float p_timeout) { - ERR_FAIL_COND(p_timeout <= 0.0); - handshake_timeout = p_timeout * 1000; -} - -MultiplayerPeer::ConnectionStatus WebSocketServer::get_connection_status() const { - if (is_listening()) { - return CONNECTION_CONNECTED; - } - - return CONNECTION_DISCONNECTED; -} - -bool WebSocketServer::is_server() const { - return true; -} - -void WebSocketServer::_on_peer_packet(int32_t p_peer_id) { - if (_is_multiplayer) { - _process_multiplayer(get_peer(p_peer_id), p_peer_id); - } else { - emit_signal(SNAME("data_received"), p_peer_id); - } -} - -void WebSocketServer::_on_connect(int32_t p_peer_id, String p_protocol, String p_resource_name) { - if (_is_multiplayer) { - // Send add to clients - _send_add(p_peer_id); - emit_signal(SNAME("peer_connected"), p_peer_id); - } else { - emit_signal(SNAME("client_connected"), p_peer_id, p_protocol, p_resource_name); - } -} - -void WebSocketServer::_on_disconnect(int32_t p_peer_id, bool p_was_clean) { - if (_is_multiplayer) { - // Send delete to clients - _send_del(p_peer_id); - emit_signal(SNAME("peer_disconnected"), p_peer_id); - } else { - emit_signal(SNAME("client_disconnected"), p_peer_id, p_was_clean); - } -} - -void WebSocketServer::_on_close_request(int32_t p_peer_id, int p_code, String p_reason) { - emit_signal(SNAME("client_close_request"), p_peer_id, p_code, p_reason); -} diff --git a/modules/websocket/websocket_server.h b/modules/websocket/websocket_server.h deleted file mode 100644 index de23ee884d..0000000000 --- a/modules/websocket/websocket_server.h +++ /dev/null @@ -1,90 +0,0 @@ -/*************************************************************************/ -/* websocket_server.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEBSOCKET_SERVER_H -#define WEBSOCKET_SERVER_H - -#include "core/crypto/crypto.h" -#include "core/object/ref_counted.h" -#include "websocket_multiplayer_peer.h" -#include "websocket_peer.h" - -class WebSocketServer : public WebSocketMultiplayerPeer { - GDCLASS(WebSocketServer, WebSocketMultiplayerPeer); - GDCICLASS(WebSocketServer); - - IPAddress bind_ip; - -protected: - static void _bind_methods(); - - Ref<CryptoKey> private_key; - Ref<X509Certificate> tls_cert; - Ref<X509Certificate> ca_chain; - uint32_t handshake_timeout = 3000; - -public: - virtual void set_extra_headers(const Vector<String> &p_headers) = 0; - virtual Error listen(int p_port, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false) = 0; - virtual void stop() = 0; - virtual bool is_listening() const = 0; - virtual bool has_peer(int p_id) const = 0; - virtual bool is_server() const override; - ConnectionStatus get_connection_status() const override; - - virtual IPAddress get_peer_address(int p_peer_id) const = 0; - virtual int get_peer_port(int p_peer_id) const = 0; - virtual void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = "") = 0; - - void _on_peer_packet(int32_t p_peer_id); - void _on_connect(int32_t p_peer_id, String p_protocol, String p_resource_name); - void _on_disconnect(int32_t p_peer_id, bool p_was_clean); - void _on_close_request(int32_t p_peer_id, int p_code, String p_reason); - - IPAddress get_bind_ip() const; - void set_bind_ip(const IPAddress &p_bind_ip); - - Ref<CryptoKey> get_private_key() const; - void set_private_key(Ref<CryptoKey> p_key); - - Ref<X509Certificate> get_tls_certificate() const; - void set_tls_certificate(Ref<X509Certificate> p_cert); - - Ref<X509Certificate> get_ca_chain() const; - void set_ca_chain(Ref<X509Certificate> p_ca_chain); - - float get_handshake_timeout() const; - void set_handshake_timeout(float p_timeout); - - WebSocketServer(); - ~WebSocketServer(); -}; - -#endif // WEBSOCKET_SERVER_H diff --git a/modules/websocket/wsl_client.cpp b/modules/websocket/wsl_client.cpp deleted file mode 100644 index 50ef53e267..0000000000 --- a/modules/websocket/wsl_client.cpp +++ /dev/null @@ -1,407 +0,0 @@ -/*************************************************************************/ -/* wsl_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEB_ENABLED - -#include "wsl_client.h" -#include "core/config/project_settings.h" -#include "core/io/ip.h" - -void WSLClient::_do_handshake() { - if (_requested < _request.size() - 1) { - int sent = 0; - Error err = _connection->put_partial_data(((const uint8_t *)_request.get_data() + _requested), _request.size() - _requested - 1, sent); - // Sending handshake failed - if (err != OK) { - disconnect_from_host(); - _on_error(); - return; - } - _requested += sent; - - } else { - int read = 0; - while (true) { - if (_resp_pos >= WSL_MAX_HEADER_SIZE) { - // Header is too big - disconnect_from_host(); - _on_error(); - ERR_FAIL_MSG("Response headers too big."); - } - Error err = _connection->get_partial_data(&_resp_buf[_resp_pos], 1, read); - if (err == ERR_FILE_EOF) { - // We got a disconnect. - disconnect_from_host(); - _on_error(); - return; - } else if (err != OK) { - // Got some error. - disconnect_from_host(); - _on_error(); - return; - } else if (read != 1) { - // Busy, wait next poll. - break; - } - // Check "\r\n\r\n" header terminator - char *r = (char *)_resp_buf; - int l = _resp_pos; - if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { - r[l - 3] = '\0'; - String protocol; - // Response is over, verify headers and create peer. - if (!_verify_headers(protocol)) { - disconnect_from_host(); - _on_error(); - ERR_FAIL_MSG("Invalid response headers."); - } - // Create peer. - WSLPeer::PeerData *data = memnew(struct WSLPeer::PeerData); - data->obj = this; - data->conn = _connection; - data->tcp = _tcp; - data->is_server = false; - data->id = 1; - _peer->make_context(data, _in_buf_size, _in_pkt_size, _out_buf_size, _out_pkt_size); - _peer->set_no_delay(true); - _status = CONNECTION_CONNECTED; - _on_connect(protocol); - break; - } - _resp_pos += 1; - } - } -} - -bool WSLClient::_verify_headers(String &r_protocol) { - String s = (char *)_resp_buf; - Vector<String> psa = s.split("\r\n"); - int len = psa.size(); - ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers. Got: " + itos(len) + ", expected >= 4."); - - Vector<String> req = psa[0].split(" ", false); - ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code. Got '" + psa[0] + "', expected 'HTTP/1.1 101'."); - - // Wrong protocol - ERR_FAIL_COND_V_MSG(req[0] != "HTTP/1.1", false, "Invalid protocol. Got: '" + req[0] + "', expected 'HTTP/1.1'."); - ERR_FAIL_COND_V_MSG(req[1] != "101", false, "Invalid status code. Got: '" + req[1] + "', expected '101'."); - - HashMap<String, String> headers; - for (int i = 1; i < len; i++) { - Vector<String> header = psa[i].split(":", false, 1); - ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i] + "."); - String name = header[0].to_lower(); - String value = header[1].strip_edges(); - if (headers.has(name)) { - headers[name] += "," + value; - } else { - headers[name] = value; - } - } - -#define WSL_CHECK(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); -#define WSL_CHECK_NC(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME] != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); - WSL_CHECK("connection", "upgrade"); - WSL_CHECK("upgrade", "websocket"); - WSL_CHECK_NC("sec-websocket-accept", WSLPeer::compute_key_response(_key)); -#undef WSL_CHECK_NC -#undef WSL_CHECK - if (_protocols.size() == 0) { - // We didn't request a custom protocol - ERR_FAIL_COND_V_MSG(headers.has("sec-websocket-protocol"), false, "Received unrequested sub-protocol -> " + headers["sec-websocket-protocol"]); - } else { - // We requested at least one custom protocol but didn't receive one - ERR_FAIL_COND_V_MSG(!headers.has("sec-websocket-protocol"), false, "Requested sub-protocol(s) but received none."); - // Check received sub-protocol was one of those requested. - r_protocol = headers["sec-websocket-protocol"]; - bool valid = false; - for (int i = 0; i < _protocols.size(); i++) { - if (_protocols[i] != r_protocol) { - continue; - } - valid = true; - break; - } - if (!valid) { - ERR_FAIL_V_MSG(false, "Received unrequested sub-protocol -> " + r_protocol); - return false; - } - } - return true; -} - -Error WSLClient::connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocols, const Vector<String> p_custom_headers) { - ERR_FAIL_COND_V(_connection.is_valid(), ERR_ALREADY_IN_USE); - ERR_FAIL_COND_V(p_path.is_empty(), ERR_INVALID_PARAMETER); - - _peer = Ref<WSLPeer>(memnew(WSLPeer)); - - if (p_host.is_valid_ip_address()) { - _ip_candidates.push_back(IPAddress(p_host)); - } else { - // Queue hostname for resolution. - _resolver_id = IP::get_singleton()->resolve_hostname_queue_item(p_host); - ERR_FAIL_COND_V(_resolver_id == IP::RESOLVER_INVALID_ID, ERR_INVALID_PARAMETER); - // Check if it was found in cache. - IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(_resolver_id); - if (ip_status == IP::RESOLVER_STATUS_DONE) { - _ip_candidates = IP::get_singleton()->get_resolve_item_addresses(_resolver_id); - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - } - } - - // We assume OK while hostname resolution is pending. - Error err = _resolver_id != IP::RESOLVER_INVALID_ID ? OK : FAILED; - while (_ip_candidates.size()) { - err = _tcp->connect_to_host(_ip_candidates.pop_front(), p_port); - if (err == OK) { - break; - } - } - if (err != OK) { - _tcp->disconnect_from_host(); - _on_error(); - return err; - } - _connection = _tcp; - _use_tls = p_tls; - _host = p_host; - _port = p_port; - // Strip edges from protocols. - _protocols.resize(p_protocols.size()); - String *pw = _protocols.ptrw(); - for (int i = 0; i < p_protocols.size(); i++) { - pw[i] = p_protocols[i].strip_edges(); - } - - _key = WSLPeer::generate_key(); - String request = "GET " + p_path + " HTTP/1.1\r\n"; - String port = ""; - if ((p_port != 80 && !p_tls) || (p_port != 443 && p_tls)) { - port = ":" + itos(p_port); - } - request += "Host: " + p_host + port + "\r\n"; - request += "Upgrade: websocket\r\n"; - request += "Connection: Upgrade\r\n"; - request += "Sec-WebSocket-Key: " + _key + "\r\n"; - request += "Sec-WebSocket-Version: 13\r\n"; - if (p_protocols.size() > 0) { - request += "Sec-WebSocket-Protocol: "; - for (int i = 0; i < p_protocols.size(); i++) { - if (i != 0) { - request += ","; - } - request += p_protocols[i]; - } - request += "\r\n"; - } - for (int i = 0; i < p_custom_headers.size(); i++) { - request += p_custom_headers[i] + "\r\n"; - } - request += "\r\n"; - _request = request.utf8(); - _status = CONNECTION_CONNECTING; - - return OK; -} - -int WSLClient::get_max_packet_size() const { - return (1 << _out_buf_size) - PROTO_SIZE; -} - -void WSLClient::poll() { - if (_resolver_id != IP::RESOLVER_INVALID_ID) { - IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(_resolver_id); - if (ip_status == IP::RESOLVER_STATUS_WAITING) { - return; - } - // Anything else is either a candidate or a failure. - Error err = FAILED; - if (ip_status == IP::RESOLVER_STATUS_DONE) { - _ip_candidates = IP::get_singleton()->get_resolve_item_addresses(_resolver_id); - while (_ip_candidates.size()) { - err = _tcp->connect_to_host(_ip_candidates.pop_front(), _port); - if (err == OK) { - break; - } - } - } - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - if (err != OK) { - disconnect_from_host(); - _on_error(); - return; - } - } - if (_peer->is_connected_to_host()) { - _peer->poll(); - if (!_peer->is_connected_to_host()) { - disconnect_from_host(); - _on_disconnect(_peer->close_code != -1); - } - return; - } - - if (_connection.is_null()) { - return; // Not connected. - } - - _tcp->poll(); - switch (_tcp->get_status()) { - case StreamPeerTCP::STATUS_NONE: - // Clean close - disconnect_from_host(); - _on_error(); - break; - case StreamPeerTCP::STATUS_CONNECTED: { - _ip_candidates.clear(); - Ref<StreamPeerTLS> tls; - if (_use_tls) { - if (_connection == _tcp) { - // Start SSL handshake - tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); - ERR_FAIL_COND_MSG(tls.is_null(), "SSL is not available in this build."); - tls->set_blocking_handshake_enabled(false); - if (tls->connect_to_stream(_tcp, verify_tls, _host, tls_cert) != OK) { - disconnect_from_host(); - _on_error(); - return; - } - _connection = tls; - } else { - tls = static_cast<Ref<StreamPeerTLS>>(_connection); - ERR_FAIL_COND(tls.is_null()); // Bug? - tls->poll(); - } - if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { - return; // Need more polling. - } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { - disconnect_from_host(); - _on_error(); - return; // Error. - } - } - // Do websocket handshake. - _do_handshake(); - } break; - case StreamPeerTCP::STATUS_ERROR: - while (_ip_candidates.size() > 0) { - _tcp->disconnect_from_host(); - if (_tcp->connect_to_host(_ip_candidates.pop_front(), _port) == OK) { - return; - } - } - disconnect_from_host(); - _on_error(); - break; - case StreamPeerTCP::STATUS_CONNECTING: - break; // Wait for connection - } -} - -Ref<WebSocketPeer> WSLClient::get_peer(int p_peer_id) const { - ERR_FAIL_COND_V(p_peer_id != 1, nullptr); - - return _peer; -} - -MultiplayerPeer::ConnectionStatus WSLClient::get_connection_status() const { - // This is surprising, but keeps the current behaviour to allow clean close requests. - // TODO Refactor WebSocket and split Client/Server/Multiplayer like done in other peers. - if (_peer->is_connected_to_host()) { - return CONNECTION_CONNECTED; - } - return _status; -} - -void WSLClient::disconnect_from_host(int p_code, String p_reason) { - _peer->close(p_code, p_reason); - _connection = Ref<StreamPeer>(nullptr); - _tcp = Ref<StreamPeerTCP>(memnew(StreamPeerTCP)); - _status = CONNECTION_DISCONNECTED; - - _key = ""; - _host = ""; - _protocols.clear(); - _use_tls = false; - - _request = ""; - _requested = 0; - - memset(_resp_buf, 0, sizeof(_resp_buf)); - _resp_pos = 0; - - if (_resolver_id != IP::RESOLVER_INVALID_ID) { - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - } - - _ip_candidates.clear(); -} - -IPAddress WSLClient::get_connected_host() const { - ERR_FAIL_COND_V(!_peer->is_connected_to_host(), IPAddress()); - return _peer->get_connected_host(); -} - -uint16_t WSLClient::get_connected_port() const { - ERR_FAIL_COND_V(!_peer->is_connected_to_host(), 0); - return _peer->get_connected_port(); -} - -Error WSLClient::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - ERR_FAIL_COND_V_MSG(_connection.is_valid(), FAILED, "Buffers sizes can only be set before listening or connecting."); - - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - _out_pkt_size = nearest_shift(p_out_packets - 1); - return OK; -} - -WSLClient::WSLClient() { - _peer.instantiate(); - _tcp.instantiate(); - disconnect_from_host(); -} - -WSLClient::~WSLClient() { - _peer->close_now(); - _peer->invalidate(); - disconnect_from_host(); -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/wsl_client.h b/modules/websocket/wsl_client.h deleted file mode 100644 index dfb989fdd3..0000000000 --- a/modules/websocket/wsl_client.h +++ /dev/null @@ -1,91 +0,0 @@ -/*************************************************************************/ -/* wsl_client.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WSL_CLIENT_H -#define WSL_CLIENT_H - -#ifndef WEB_ENABLED - -#include "core/error/error_list.h" -#include "core/io/stream_peer_tcp.h" -#include "core/io/stream_peer_tls.h" -#include "websocket_client.h" -#include "wsl_peer.h" -#include "wslay/wslay.h" - -class WSLClient : public WebSocketClient { - GDCIIMPL(WSLClient, WebSocketClient); - -private: - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; - int _out_pkt_size = DEF_PKT_SHIFT; - - Ref<WSLPeer> _peer; - Ref<StreamPeerTCP> _tcp; - Ref<StreamPeer> _connection; - ConnectionStatus _status = CONNECTION_DISCONNECTED; - - CharString _request; - int _requested = 0; - - uint8_t _resp_buf[WSL_MAX_HEADER_SIZE]; - int _resp_pos = 0; - - String _key; - String _host; - uint16_t _port = 0; - Array _ip_candidates; - Vector<String> _protocols; - bool _use_tls = false; - IP::ResolverID _resolver_id = IP::RESOLVER_INVALID_ID; - - void _do_handshake(); - bool _verify_headers(String &r_protocol); - -public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) override; - int get_max_packet_size() const override; - Ref<WebSocketPeer> get_peer(int p_peer_id) const override; - void disconnect_from_host(int p_code = 1000, String p_reason = "") override; - IPAddress get_connected_host() const override; - uint16_t get_connected_port() const override; - virtual ConnectionStatus get_connection_status() const override; - virtual void poll() override; - - WSLClient(); - ~WSLClient(); -}; - -#endif // WEB_ENABLED - -#endif // WSL_CLIENT_H diff --git a/modules/websocket/wsl_peer.cpp b/modules/websocket/wsl_peer.cpp index 97bd87a526..4c8e661f67 100644 --- a/modules/websocket/wsl_peer.cpp +++ b/modules/websocket/wsl_peer.cpp @@ -32,71 +32,533 @@ #include "wsl_peer.h" -#include "wsl_client.h" -#include "wsl_server.h" +#include "wsl_peer.h" -#include "core/crypto/crypto_core.h" -#include "core/math/random_number_generator.h" -#include "core/os/os.h" +#include "core/io/stream_peer_tls.h" -String WSLPeer::generate_key() { - // Random key - RandomNumberGenerator rng; - rng.set_seed(OS::get_singleton()->get_unix_time()); - Vector<uint8_t> bkey; - int len = 16; // 16 bytes, as per RFC - bkey.resize(len); - uint8_t *w = bkey.ptrw(); - for (int i = 0; i < len; i++) { - w[i] = (uint8_t)rng.randi_range(0, 255); +CryptoCore::RandomGenerator *WSLPeer::_static_rng = nullptr; + +void WSLPeer::initialize() { + WebSocketPeer::_create = WSLPeer::_create; + _static_rng = memnew(CryptoCore::RandomGenerator); + _static_rng->init(); +} + +void WSLPeer::deinitialize() { + if (_static_rng) { + memdelete(_static_rng); + _static_rng = nullptr; } - return CryptoCore::b64_encode_str(&w[0], len); } -String WSLPeer::compute_key_response(String p_key) { - String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC - Vector<uint8_t> sha = key.sha1_buffer(); - return CryptoCore::b64_encode_str(sha.ptr(), sha.size()); +/// +/// Resolver +/// +void WSLPeer::Resolver::start(const String &p_host, int p_port) { + stop(); + + port = p_port; + if (p_host.is_valid_ip_address()) { + ip_candidates.push_back(IPAddress(p_host)); + } else { + // Queue hostname for resolution. + resolver_id = IP::get_singleton()->resolve_hostname_queue_item(p_host); + ERR_FAIL_COND(resolver_id == IP::RESOLVER_INVALID_ID); + // Check if it was found in cache. + IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id); + if (ip_status == IP::RESOLVER_STATUS_DONE) { + ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id); + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; + } + } } -void WSLPeer::_wsl_destroy(struct PeerData **p_data) { - if (!p_data || !(*p_data)) { - return; +void WSLPeer::Resolver::stop() { + if (resolver_id != IP::RESOLVER_INVALID_ID) { + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; + } + port = 0; +} + +void WSLPeer::Resolver::try_next_candidate(Ref<StreamPeerTCP> &p_tcp) { + // Check if we still need resolving. + if (resolver_id != IP::RESOLVER_INVALID_ID) { + IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id); + if (ip_status == IP::RESOLVER_STATUS_WAITING) { + return; + } + if (ip_status == IP::RESOLVER_STATUS_DONE) { + ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id); + } + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; } - struct PeerData *data = *p_data; - if (data->polling) { - data->destroy = true; + + // Try the current candidate if we have one. + if (p_tcp->get_status() != StreamPeerTCP::STATUS_NONE) { + p_tcp->poll(); + StreamPeerTCP::Status status = p_tcp->get_status(); + if (status == StreamPeerTCP::STATUS_CONNECTED) { + p_tcp->set_no_delay(true); + ip_candidates.clear(); + return; + } else { + p_tcp->disconnect_from_host(); + } + } + + // Keep trying next candidate. + while (ip_candidates.size()) { + Error err = p_tcp->connect_to_host(ip_candidates.pop_front(), port); + if (err == OK) { + return; + } else { + p_tcp->disconnect_from_host(); + } + } +} + +/// +/// Server functions +/// +Error WSLPeer::accept_stream(Ref<StreamPeer> p_stream) { + ERR_FAIL_COND_V(wsl_ctx || tcp.is_valid(), ERR_ALREADY_IN_USE); + ERR_FAIL_COND_V(p_stream.is_null(), ERR_INVALID_PARAMETER); + + _clear(); + + if (p_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static())) { + tcp = p_stream; + connection = p_stream; + use_tls = false; + } else if (p_stream->is_class_ptr(StreamPeerTLS::get_class_ptr_static())) { + Ref<StreamPeer> base_stream = static_cast<Ref<StreamPeerTLS>>(p_stream)->get_stream(); + ERR_FAIL_COND_V(base_stream.is_null() || !base_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static()), ERR_INVALID_PARAMETER); + tcp = static_cast<Ref<StreamPeerTCP>>(base_stream); + connection = p_stream; + use_tls = true; + } + ERR_FAIL_COND_V(connection.is_null() || tcp.is_null(), ERR_INVALID_PARAMETER); + is_server = true; + ready_state = STATE_CONNECTING; + handshake_buffer->resize(WSL_MAX_HEADER_SIZE); + handshake_buffer->seek(0); + return OK; +} + +bool WSLPeer::_parse_client_request() { + Vector<String> psa = String((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4).split("\r\n"); + int len = psa.size(); + ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers, got: " + itos(len) + ", expected >= 4."); + + Vector<String> req = psa[0].split(" ", false); + ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code."); + + // Wrong protocol + ERR_FAIL_COND_V_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", false, "Invalid method or HTTP version."); + + HashMap<String, String> headers; + for (int i = 1; i < len; i++) { + Vector<String> header = psa[i].split(":", false, 1); + ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i]); + String name = header[0].to_lower(); + String value = header[1].strip_edges(); + if (headers.has(name)) { + headers[name] += "," + value; + } else { + headers[name] = value; + } + } + requested_host = headers.has("host") ? headers.get("host") : ""; + requested_url = (use_tls ? "wss://" : "ws://") + requested_host + req[1]; +#define WSL_CHECK(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); +#define WSL_CHECK_EX(NAME) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME), false, "Missing header '" + String(NAME) + "'."); + WSL_CHECK("upgrade", "websocket"); + WSL_CHECK("sec-websocket-version", "13"); + WSL_CHECK_EX("sec-websocket-key"); + WSL_CHECK_EX("connection"); +#undef WSL_CHECK_EX +#undef WSL_CHECK + session_key = headers["sec-websocket-key"]; + if (headers.has("sec-websocket-protocol")) { + Vector<String> protos = headers["sec-websocket-protocol"].split(","); + for (int i = 0; i < protos.size(); i++) { + String proto = protos[i].strip_edges(); + // Check if we have the given protocol + for (int j = 0; j < supported_protocols.size(); j++) { + if (proto != supported_protocols[j]) { + continue; + } + selected_protocol = proto; + break; + } + // Found a protocol + if (!selected_protocol.is_empty()) { + break; + } + } + if (selected_protocol.is_empty()) { // Invalid protocol(s) requested + return false; + } + } else if (supported_protocols.size() > 0) { // No protocol requested, but we need one + return false; + } + return true; +} + +Error WSLPeer::_do_server_handshake() { + if (use_tls) { + Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(connection); + if (tls.is_null()) { + ERR_FAIL_V_MSG(ERR_BUG, "Couldn't get StreamPeerTLS for WebSocket handshake."); + close(-1); + return FAILED; + } + tls->poll(); + if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + return OK; // Pending handshake + } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { + print_verbose(vformat("WebSocket SSL connection error during handshake (StreamPeerTLS status code %d).", tls->get_status())); + close(-1); + return FAILED; + } + } + + if (pending_request) { + int read = 0; + while (true) { + ERR_FAIL_COND_V_MSG(handshake_buffer->get_available_bytes() < 1, ERR_OUT_OF_MEMORY, "WebSocket response headers are too big."); + int pos = handshake_buffer->get_position(); + uint8_t byte; + Error err = connection->get_partial_data(&byte, 1, read); + if (err != OK) { // Got an error + print_verbose(vformat("WebSocket error while getting partial data (StreamPeer error code %d).", err)); + close(-1); + return FAILED; + } else if (read != 1) { // Busy, wait next poll + return OK; + } + handshake_buffer->put_u8(byte); + const char *r = (const char *)handshake_buffer->get_data_array().ptr(); + int l = pos; + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + if (!_parse_client_request()) { + close(-1); + return FAILED; + } + String s = "HTTP/1.1 101 Switching Protocols\r\n"; + s += "Upgrade: websocket\r\n"; + s += "Connection: Upgrade\r\n"; + s += "Sec-WebSocket-Accept: " + _compute_key_response(session_key) + "\r\n"; + if (!selected_protocol.is_empty()) { + s += "Sec-WebSocket-Protocol: " + selected_protocol + "\r\n"; + } + for (int i = 0; i < handshake_headers.size(); i++) { + s += handshake_headers[i] + "\r\n"; + } + s += "\r\n"; + CharString cs = s.utf8(); + handshake_buffer->clear(); + handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length()); + handshake_buffer->seek(0); + pending_request = false; + break; + } + } + } + + if (pending_request) { // Still pending. + return OK; + } + + int left = handshake_buffer->get_available_bytes(); + if (left) { + Vector<uint8_t> data = handshake_buffer->get_data_array(); + int pos = handshake_buffer->get_position(); + int sent = 0; + Error err = connection->put_partial_data(data.ptr() + pos, left, sent); + if (err != OK) { + print_verbose(vformat("WebSocket error while putting partial data (StreamPeer error code %d).", err)); + close(-1); + return err; + } + handshake_buffer->seek(pos + sent); + left -= sent; + if (left == 0) { + resolver.stop(); + // Response sent, initialize wslay context. + wslay_event_context_server_init(&wsl_ctx, &_wsl_callbacks, this); + wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size); + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_OPEN; + } + } + + return OK; +} + +/// +/// Client functions +/// +void WSLPeer::_do_client_handshake() { + ERR_FAIL_COND(tcp.is_null()); + + // Try to connect to candidates. + if (resolver.has_more_candidates()) { + resolver.try_next_candidate(tcp); + if (resolver.has_more_candidates()) { + return; // Still pending. + } + } + + tcp->poll(); + if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + close(-1); // Failed to connect. return; } - wslay_event_context_free(data->ctx); - memdelete(data); - *p_data = nullptr; + + if (use_tls) { + Ref<StreamPeerTLS> tls; + if (connection == tcp) { + // Start SSL handshake + tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); + ERR_FAIL_COND_MSG(tls.is_null(), "SSL is not available in this build."); + tls->set_blocking_handshake_enabled(false); + if (tls->connect_to_stream(tcp, verify_tls, requested_host, tls_cert) != OK) { + close(-1); + return; // Error. + } + connection = tls; + } else { + tls = static_cast<Ref<StreamPeerTLS>>(connection); + ERR_FAIL_COND(tls.is_null()); + tls->poll(); + } + if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + return; // Need more polling. + } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { + close(-1); + return; // Error. + } + } + + // Do websocket handshake. + if (pending_request) { + int left = handshake_buffer->get_available_bytes(); + int pos = handshake_buffer->get_position(); + const Vector<uint8_t> data = handshake_buffer->get_data_array(); + int sent = 0; + Error err = connection->put_partial_data(data.ptr() + pos, left, sent); + // Sending handshake failed + if (err != OK) { + close(-1); + return; // Error. + } + handshake_buffer->seek(pos + sent); + if (handshake_buffer->get_available_bytes() == 0) { + pending_request = false; + handshake_buffer->clear(); + handshake_buffer->resize(WSL_MAX_HEADER_SIZE); + handshake_buffer->seek(0); + } + } else { + int read = 0; + while (true) { + int left = handshake_buffer->get_available_bytes(); + int pos = handshake_buffer->get_position(); + if (left == 0) { + // Header is too big + close(-1); + ERR_FAIL_MSG("Response headers too big."); + return; + } + + uint8_t byte; + Error err = connection->get_partial_data(&byte, 1, read); + if (err != OK) { + // Got some error. + close(-1); + return; + } else if (read != 1) { + // Busy, wait next poll. + break; + } + handshake_buffer->put_u8(byte); + + // Check "\r\n\r\n" header terminator + const char *r = (const char *)handshake_buffer->get_data_array().ptr(); + int l = pos; + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + // Response is over, verify headers and initialize wslay context/ + if (!_verify_server_response()) { + close(-1); + ERR_FAIL_MSG("Invalid response headers."); + return; + } + wslay_event_context_client_init(&wsl_ctx, &_wsl_callbacks, this); + wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size); + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_OPEN; + break; + } + } + } } -bool WSLPeer::_wsl_poll(struct PeerData *p_data) { - p_data->polling = true; - int err = 0; - if ((err = wslay_event_recv(p_data->ctx)) != 0 || (err = wslay_event_send(p_data->ctx)) != 0) { - print_verbose("Websocket (wslay) poll error: " + itos(err)); - p_data->destroy = true; +bool WSLPeer::_verify_server_response() { + Vector<String> psa = String((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4).split("\r\n"); + int len = psa.size(); + ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers. Got: " + itos(len) + ", expected >= 4."); + + Vector<String> req = psa[0].split(" ", false); + ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code. Got '" + psa[0] + "', expected 'HTTP/1.1 101'."); + + // Wrong protocol + ERR_FAIL_COND_V_MSG(req[0] != "HTTP/1.1", false, "Invalid protocol. Got: '" + req[0] + "', expected 'HTTP/1.1'."); + ERR_FAIL_COND_V_MSG(req[1] != "101", false, "Invalid status code. Got: '" + req[1] + "', expected '101'."); + + HashMap<String, String> headers; + for (int i = 1; i < len; i++) { + Vector<String> header = psa[i].split(":", false, 1); + ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i] + "."); + String name = header[0].to_lower(); + String value = header[1].strip_edges(); + if (headers.has(name)) { + headers[name] += "," + value; + } else { + headers[name] = value; + } } - p_data->polling = false; - if (p_data->destroy || (wslay_event_get_close_sent(p_data->ctx) && wslay_event_get_close_received(p_data->ctx))) { - bool valid = p_data->valid; - _wsl_destroy(&p_data); - return valid; +#define WSL_CHECK(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); +#define WSL_CHECK_NC(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME] != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); + WSL_CHECK("connection", "upgrade"); + WSL_CHECK("upgrade", "websocket"); + WSL_CHECK_NC("sec-websocket-accept", _compute_key_response(session_key)); +#undef WSL_CHECK_NC +#undef WSL_CHECK + if (supported_protocols.size() == 0) { + // We didn't request a custom protocol + ERR_FAIL_COND_V_MSG(headers.has("sec-websocket-protocol"), false, "Received unrequested sub-protocol -> " + headers["sec-websocket-protocol"]); + } else { + // We requested at least one custom protocol but didn't receive one + ERR_FAIL_COND_V_MSG(!headers.has("sec-websocket-protocol"), false, "Requested sub-protocol(s) but received none."); + // Check received sub-protocol was one of those requested. + selected_protocol = headers["sec-websocket-protocol"]; + bool valid = false; + for (int i = 0; i < supported_protocols.size(); i++) { + if (supported_protocols[i] != selected_protocol) { + continue; + } + valid = true; + break; + } + if (!valid) { + ERR_FAIL_V_MSG(false, "Received unrequested sub-protocol -> " + selected_protocol); + return false; + } + } + return true; +} + +Error WSLPeer::connect_to_url(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_cert) { + ERR_FAIL_COND_V(wsl_ctx || tcp.is_valid(), ERR_ALREADY_IN_USE); + ERR_FAIL_COND_V(p_url.is_empty(), ERR_INVALID_PARAMETER); + + _clear(); + + String host; + String path; + String scheme; + int port = 0; + Error err = p_url.parse_url(scheme, host, port, path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); + if (scheme.is_empty()) { + scheme = "ws://"; + } + ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme)); + + use_tls = false; + if (scheme == "wss://") { + use_tls = true; + } + if (port == 0) { + port = use_tls ? 443 : 80; + } + if (path.is_empty()) { + path = "/"; + } + + requested_url = p_url; + requested_host = host; + verify_tls = p_verify_tls; + tls_cert = p_cert; + tcp.instantiate(); + + resolver.start(host, port); + resolver.try_next_candidate(tcp); + + if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTING && !resolver.has_more_candidates()) { + _clear(); + return FAILED; + } + connection = tcp; + + // Prepare handshake request. + session_key = _generate_key(); + String request = "GET " + path + " HTTP/1.1\r\n"; + String port_string; + if ((port != 80 && !use_tls) || (port != 443 && use_tls)) { + port_string = ":" + itos(port); + } + request += "Host: " + host + port_string + "\r\n"; + request += "Upgrade: websocket\r\n"; + request += "Connection: Upgrade\r\n"; + request += "Sec-WebSocket-Key: " + session_key + "\r\n"; + request += "Sec-WebSocket-Version: 13\r\n"; + if (supported_protocols.size() > 0) { + request += "Sec-WebSocket-Protocol: "; + for (int i = 0; i < supported_protocols.size(); i++) { + if (i != 0) { + request += ","; + } + request += supported_protocols[i]; + } + request += "\r\n"; } - return false; + for (int i = 0; i < handshake_headers.size(); i++) { + request += handshake_headers[i] + "\r\n"; + } + request += "\r\n"; + CharString cs = request.utf8(); + handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length()); + handshake_buffer->seek(0); + ready_state = STATE_CONNECTING; + is_server = false; + return OK; } -ssize_t wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid) { +/// +/// Callback functions. +/// +ssize_t WSLPeer::_wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + Ref<StreamPeer> conn = peer->connection; + if (conn.is_null()) { wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); return -1; } - Ref<StreamPeer> conn = peer_data->conn; int read = 0; Error err = conn->get_partial_data(data, len, read); if (err != OK) { @@ -111,13 +573,13 @@ ssize_t wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len return read; } -ssize_t wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid) { +ssize_t WSLPeer::_wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + Ref<StreamPeer> conn = peer->connection; + if (conn.is_null()) { wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); return -1; } - Ref<StreamPeer> conn = peer_data->conn; int sent = 0; Error err = conn->put_partial_data(data, len, sent); if (err != OK) { @@ -131,144 +593,142 @@ ssize_t wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size return sent; } -int wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) { - RandomNumberGenerator rng; - // TODO maybe use crypto in the future? - rng.set_seed(OS::get_singleton()->get_unix_time()); - for (unsigned int i = 0; i < len; i++) { - buf[i] = (uint8_t)rng.randi_range(0, 255); - } +int WSLPeer::_wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) { + ERR_FAIL_COND_V(!_static_rng, WSLAY_ERR_CALLBACK_FAILURE); + Error err = _static_rng->get_random_bytes(buf, len); + ERR_FAIL_COND_V(err != OK, WSLAY_ERR_CALLBACK_FAILURE); return 0; } -void wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid || peer_data->closing) { +void WSLPeer::_wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + uint8_t op = arg->opcode; + + if (op == WSLAY_CONNECTION_CLOSE) { + // Close request or confirmation. + peer->close_code = arg->status_code; + size_t len = arg->msg_length; + peer->close_reason = ""; + if (len > 2 /* first 2 bytes = close code */) { + peer->close_reason.parse_utf8((char *)arg->msg + 2, len - 2); + } + if (peer->ready_state == STATE_OPEN) { + peer->ready_state = STATE_CLOSING; + } return; } - WSLPeer *peer = static_cast<WSLPeer *>(peer_data->peer); - if (peer->parse_message(arg) != OK) { + if (peer->ready_state == STATE_CLOSING) { return; } - if (peer_data->is_server) { - WSLServer *helper = static_cast<WSLServer *>(peer_data->obj); - helper->_on_peer_packet(peer_data->id); - } else { - WSLClient *helper = static_cast<WSLClient *>(peer_data->obj); - helper->_on_peer_packet(); + if (op == WSLAY_TEXT_FRAME || op == WSLAY_BINARY_FRAME) { + // Message. + uint8_t is_string = arg->opcode == WSLAY_TEXT_FRAME ? 1 : 0; + peer->in_buffer.write_packet(arg->msg, arg->msg_length, &is_string); } + // Ping or pong. } -wslay_event_callbacks wsl_callbacks = { - wsl_recv_callback, - wsl_send_callback, - wsl_genmask_callback, +wslay_event_callbacks WSLPeer::_wsl_callbacks = { + _wsl_recv_callback, + _wsl_send_callback, + _wsl_genmask_callback, nullptr, /* on_frame_recv_start_callback */ nullptr, /* on_frame_recv_callback */ nullptr, /* on_frame_recv_end_callback */ - wsl_msg_recv_callback + _wsl_msg_recv_callback }; -Error WSLPeer::parse_message(const wslay_event_on_msg_recv_arg *arg) { - uint8_t is_string = 0; - if (arg->opcode == WSLAY_TEXT_FRAME) { - is_string = 1; - } else if (arg->opcode == WSLAY_CONNECTION_CLOSE) { - close_code = arg->status_code; - size_t len = arg->msg_length; - close_reason = ""; - if (len > 2 /* first 2 bytes = close code */) { - close_reason.parse_utf8((char *)arg->msg + 2, len - 2); - } - if (!wslay_event_get_close_sent(_data->ctx)) { - if (_data->is_server) { - WSLServer *helper = static_cast<WSLServer *>(_data->obj); - helper->_on_close_request(_data->id, close_code, close_reason); - } else { - WSLClient *helper = static_cast<WSLClient *>(_data->obj); - helper->_on_close_request(close_code, close_reason); - } - } - return ERR_FILE_EOF; - } else if (arg->opcode != WSLAY_BINARY_FRAME) { - // Ping or pong - return ERR_SKIP; - } - _in_buffer.write_packet(arg->msg, arg->msg_length, &is_string); - return OK; -} - -void WSLPeer::make_context(PeerData *p_data, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size, unsigned int p_out_pkt_size) { - ERR_FAIL_COND(_data != nullptr); - ERR_FAIL_COND(p_data == nullptr); - - _in_buffer.resize(p_in_pkt_size, p_in_buf_size); - _packet_buffer.resize(1 << p_in_buf_size); - _out_buf_size = p_out_buf_size; - _out_pkt_size = p_out_pkt_size; - - _data = p_data; - _data->peer = this; - _data->valid = true; - - if (_data->is_server) { - wslay_event_context_server_init(&(_data->ctx), &wsl_callbacks, _data); - } else { - wslay_event_context_client_init(&(_data->ctx), &wsl_callbacks, _data); - } - wslay_event_config_set_max_recv_msg_length(_data->ctx, (1ULL << p_in_buf_size)); -} - -void WSLPeer::set_write_mode(WriteMode p_mode) { - write_mode = p_mode; +String WSLPeer::_generate_key() { + // Random key + Vector<uint8_t> bkey; + int len = 16; // 16 bytes, as per RFC + bkey.resize(len); + _wsl_genmask_callback(nullptr, bkey.ptrw(), len, nullptr); + return CryptoCore::b64_encode_str(bkey.ptrw(), len); } -WSLPeer::WriteMode WSLPeer::get_write_mode() const { - return write_mode; +String WSLPeer::_compute_key_response(String p_key) { + String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC + Vector<uint8_t> sha = key.sha1_buffer(); + return CryptoCore::b64_encode_str(sha.ptr(), sha.size()); } void WSLPeer::poll() { - if (!_data) { + // Nothing to do. + if (ready_state == STATE_CLOSED) { return; } - if (_wsl_poll(_data)) { - _data = nullptr; + if (ready_state == STATE_CONNECTING) { + if (is_server) { + _do_server_handshake(); + } else { + _do_client_handshake(); + } + } + + if (ready_state == STATE_OPEN || ready_state == STATE_CLOSING) { + ERR_FAIL_COND(!wsl_ctx); + int err = 0; + if ((err = wslay_event_recv(wsl_ctx)) != 0 || (err = wslay_event_send(wsl_ctx)) != 0) { + // Error close. + print_verbose("Websocket (wslay) poll error: " + itos(err)); + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + close(-1); + return; + } + if (wslay_event_get_close_sent(wsl_ctx) && wslay_event_get_close_received(wsl_ctx)) { + // Clean close. + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + close(-1); + return; + } } } -Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V(!is_connected_to_host(), FAILED); - ERR_FAIL_COND_V(_out_pkt_size && (wslay_event_get_queued_msg_count(_data->ctx) >= (1ULL << _out_pkt_size)), ERR_OUT_OF_MEMORY); - ERR_FAIL_COND_V(_out_buf_size && (wslay_event_get_queued_msg_length(_data->ctx) + p_buffer_size >= (1ULL << _out_buf_size)), ERR_OUT_OF_MEMORY); +Error WSLPeer::_send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode) { + ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED); + ERR_FAIL_COND_V(wslay_event_get_queued_msg_count(wsl_ctx) >= (uint32_t)max_queued_packets, ERR_OUT_OF_MEMORY); + ERR_FAIL_COND_V(outbound_buffer_size > 0 && (wslay_event_get_queued_msg_length(wsl_ctx) + p_buffer_size > (uint32_t)outbound_buffer_size), ERR_OUT_OF_MEMORY); struct wslay_event_msg msg; - msg.opcode = write_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME; + msg.opcode = p_opcode; msg.msg = p_buffer; msg.msg_length = p_buffer_size; // Queue & send message. - if (wslay_event_queue_msg(_data->ctx, &msg) != 0 || wslay_event_send(_data->ctx) != 0) { - close_now(); + if (wslay_event_queue_msg(wsl_ctx, &msg) != 0 || wslay_event_send(wsl_ctx) != 0) { + close(-1); return FAILED; } return OK; } +Error WSLPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) { + wslay_opcode opcode = p_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME; + return _send(p_buffer, p_buffer_size, opcode); +} + +Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { + return _send(p_buffer, p_buffer_size, WSLAY_BINARY_FRAME); +} + Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { r_buffer_size = 0; - ERR_FAIL_COND_V(!is_connected_to_host(), FAILED); + ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED); - if (_in_buffer.packets_left() == 0) { + if (in_buffer.packets_left() == 0) { return ERR_UNAVAILABLE; } int read = 0; - uint8_t *rw = _packet_buffer.ptrw(); - _in_buffer.read_packet(rw, _packet_buffer.size(), &_is_string, read); + uint8_t *rw = packet_buffer.ptrw(); + in_buffer.read_packet(rw, packet_buffer.size(), &was_string, read); *r_buffer = rw; r_buffer_size = read; @@ -277,75 +737,106 @@ Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { } int WSLPeer::get_available_packet_count() const { - if (!is_connected_to_host()) { + if (ready_state != STATE_OPEN) { return 0; } - return _in_buffer.packets_left(); + return in_buffer.packets_left(); } int WSLPeer::get_current_outbound_buffered_amount() const { - ERR_FAIL_COND_V(!_data, 0); - - return wslay_event_get_queued_msg_length(_data->ctx); -} - -bool WSLPeer::was_string_packet() const { - return _is_string; -} - -bool WSLPeer::is_connected_to_host() const { - return _data != nullptr; -} + if (ready_state != STATE_OPEN) { + return 0; + } -void WSLPeer::close_now() { - close(1000, ""); - _wsl_destroy(&_data); + return wslay_event_get_queued_msg_length(wsl_ctx); } void WSLPeer::close(int p_code, String p_reason) { - if (_data && !wslay_event_get_close_sent(_data->ctx)) { + if (p_code < 0) { + // Force immediate close. + ready_state = STATE_CLOSED; + } + + if (ready_state == STATE_OPEN && !wslay_event_get_close_sent(wsl_ctx)) { CharString cs = p_reason.utf8(); - wslay_event_queue_close(_data->ctx, p_code, (uint8_t *)cs.ptr(), cs.size()); - wslay_event_send(_data->ctx); - _data->closing = true; + wslay_event_queue_close(wsl_ctx, p_code, (uint8_t *)cs.ptr(), cs.length()); + wslay_event_send(wsl_ctx); + ready_state = STATE_CLOSING; + } else if (ready_state == STATE_CONNECTING || ready_state == STATE_CLOSED) { + ready_state = STATE_CLOSED; + connection.unref(); + if (tcp.is_valid()) { + tcp->disconnect_from_host(); + tcp.unref(); + } } - _in_buffer.clear(); - _packet_buffer.resize(0); + in_buffer.clear(); + packet_buffer.resize(0); } IPAddress WSLPeer::get_connected_host() const { - ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), IPAddress()); - - return _data->tcp->get_connected_host(); + ERR_FAIL_COND_V(tcp.is_null(), IPAddress()); + return tcp->get_connected_host(); } uint16_t WSLPeer::get_connected_port() const { - ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), 0); + ERR_FAIL_COND_V(tcp.is_null(), 0); + return tcp->get_connected_port(); +} + +String WSLPeer::get_selected_protocol() const { + return selected_protocol; +} - return _data->tcp->get_connected_port(); +String WSLPeer::get_requested_url() const { + return requested_url; } void WSLPeer::set_no_delay(bool p_enabled) { - ERR_FAIL_COND(!is_connected_to_host() || _data->tcp.is_null()); - _data->tcp->set_no_delay(p_enabled); + ERR_FAIL_COND(tcp.is_null()); + tcp->set_no_delay(p_enabled); } -void WSLPeer::invalidate() { - if (_data) { - _data->valid = false; +void WSLPeer::_clear() { + // Connection info. + ready_state = STATE_CLOSED; + is_server = false; + connection.unref(); + if (tcp.is_valid()) { + tcp->disconnect_from_host(); + tcp.unref(); } + if (wsl_ctx) { + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + } + + resolver.stop(); + requested_url.clear(); + requested_host.clear(); + pending_request = true; + handshake_buffer->clear(); + selected_protocol.clear(); + session_key.clear(); + + // Pending packets info. + was_string = 0; + in_buffer.clear(); + packet_buffer.clear(); + + // Close code info. + close_code = -1; + close_reason.clear(); } WSLPeer::WSLPeer() { + handshake_buffer.instantiate(); } WSLPeer::~WSLPeer() { - close(); - invalidate(); - _wsl_destroy(&_data); - _data = nullptr; + close(-1); } #endif // WEB_ENABLED diff --git a/modules/websocket/wsl_peer.h b/modules/websocket/wsl_peer.h index 92672eb2c4..379002739c 100644 --- a/modules/websocket/wsl_peer.h +++ b/modules/websocket/wsl_peer.h @@ -33,79 +33,123 @@ #ifndef WEB_ENABLED +#include "websocket_peer.h" + +#include "packet_buffer.h" + +#include "core/crypto/crypto_core.h" #include "core/error/error_list.h" #include "core/io/packet_peer.h" #include "core/io/stream_peer_tcp.h" #include "core/templates/ring_buffer.h" -#include "packet_buffer.h" -#include "websocket_peer.h" #include "wslay/wslay.h" #define WSL_MAX_HEADER_SIZE 4096 class WSLPeer : public WebSocketPeer { - GDCIIMPL(WSLPeer, WebSocketPeer); - -public: - struct PeerData { - bool polling = false; - bool destroy = false; - bool valid = false; - bool is_server = false; - bool closing = false; - void *obj = nullptr; - void *peer = nullptr; - Ref<StreamPeer> conn; - Ref<StreamPeerTCP> tcp; - int id = 1; - wslay_event_context_ptr ctx = nullptr; +private: + static CryptoCore::RandomGenerator *_static_rng; + static WebSocketPeer *_create() { return memnew(WSLPeer); } + + // Callbacks. + static ssize_t _wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data); + static ssize_t _wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data); + static int _wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data); + static void _wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); + + static wslay_event_callbacks _wsl_callbacks; + + // Helpers + static String _compute_key_response(String p_key); + static String _generate_key(); + + // Client IP resolver. + class Resolver { + Array ip_candidates; + IP::ResolverID resolver_id = IP::RESOLVER_INVALID_ID; + int port = 0; + + public: + bool has_more_candidates() { + return ip_candidates.size() > 0 || resolver_id != IP::RESOLVER_INVALID_ID; + } + + void try_next_candidate(Ref<StreamPeerTCP> &p_tcp); + void start(const String &p_host, int p_port); + void stop(); + Resolver() {} }; - static String compute_key_response(String p_key); - static String generate_key(); + Resolver resolver; -private: - static bool _wsl_poll(struct PeerData *p_data); - static void _wsl_destroy(struct PeerData **p_data); + // WebSocket connection state. + WebSocketPeer::State ready_state = WebSocketPeer::STATE_CLOSED; + bool is_server = false; + Ref<StreamPeerTCP> tcp; + Ref<StreamPeer> connection; + wslay_event_context_ptr wsl_ctx = nullptr; + + String requested_url; + String requested_host; + bool pending_request = true; + Ref<StreamPeerBuffer> handshake_buffer; + String selected_protocol; + String session_key; - struct PeerData *_data = nullptr; - uint8_t _is_string = 0; + int close_code = -1; + String close_reason; + uint8_t was_string = 0; + + // WebSocket configuration. + bool use_tls = true; + bool verify_tls = true; + Ref<X509Certificate> tls_cert; + + // Packet buffers. + Vector<uint8_t> packet_buffer; // Our packet info is just a boolean (is_string), using uint8_t for it. - PacketBuffer<uint8_t> _in_buffer; + PacketBuffer<uint8_t> in_buffer; - Vector<uint8_t> _packet_buffer; + Error _send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode); - WriteMode write_mode = WRITE_MODE_BINARY; + Error _do_server_handshake(); + bool _parse_client_request(); - int _out_buf_size = 0; - int _out_pkt_size = 0; + void _do_client_handshake(); + bool _verify_server_response(); + + void _clear(); public: - int close_code = -1; - String close_reason; - void poll(); // Used by client and server. + static void initialize(); + static void deinitialize(); + // PacketPeer virtual int get_available_packet_count() const override; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override; virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; - virtual int get_max_packet_size() const override { return _packet_buffer.size(); }; - virtual int get_current_outbound_buffered_amount() const override; + virtual int get_max_packet_size() const override { return packet_buffer.size(); }; - virtual void close_now(); + // WebSocketPeer + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override; + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) override; + virtual Error accept_stream(Ref<StreamPeer> p_stream) override; virtual void close(int p_code = 1000, String p_reason = "") override; - virtual bool is_connected_to_host() const override; + virtual void poll() override; + + virtual State get_ready_state() const override { return ready_state; } + virtual int get_close_code() const override { return close_code; } + virtual String get_close_reason() const override { return close_reason; } + virtual int get_current_outbound_buffered_amount() const override; + virtual IPAddress get_connected_host() const override; virtual uint16_t get_connected_port() const override; + virtual String get_selected_protocol() const override; + virtual String get_requested_url() const override; - virtual WriteMode get_write_mode() const override; - virtual void set_write_mode(WriteMode p_mode) override; - virtual bool was_string_packet() const override; + virtual bool was_string_packet() const override { return was_string; } virtual void set_no_delay(bool p_enabled) override; - void make_context(PeerData *p_data, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size, unsigned int p_out_pkt_size); - Error parse_message(const wslay_event_on_msg_recv_arg *arg); - void invalidate(); - WSLPeer(); ~WSLPeer(); }; diff --git a/modules/websocket/wsl_server.cpp b/modules/websocket/wsl_server.cpp deleted file mode 100644 index 01dcd53839..0000000000 --- a/modules/websocket/wsl_server.cpp +++ /dev/null @@ -1,329 +0,0 @@ -/*************************************************************************/ -/* wsl_server.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEB_ENABLED - -#include "wsl_server.h" -#include "core/config/project_settings.h" -#include "core/os/os.h" - -bool WSLServer::PendingPeer::_parse_request(const Vector<String> p_protocols, String &r_resource_name) { - Vector<String> psa = String((char *)req_buf).split("\r\n"); - int len = psa.size(); - ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers, got: " + itos(len) + ", expected >= 4."); - - Vector<String> req = psa[0].split(" ", false); - ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code."); - - // Wrong protocol - ERR_FAIL_COND_V_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", false, "Invalid method or HTTP version."); - - r_resource_name = req[1]; - HashMap<String, String> headers; - for (int i = 1; i < len; i++) { - Vector<String> header = psa[i].split(":", false, 1); - ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i]); - String name = header[0].to_lower(); - String value = header[1].strip_edges(); - if (headers.has(name)) { - headers[name] += "," + value; - } else { - headers[name] = value; - } - } -#define WSL_CHECK(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); -#define WSL_CHECK_EX(NAME) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME), false, "Missing header '" + String(NAME) + "'."); - WSL_CHECK("upgrade", "websocket"); - WSL_CHECK("sec-websocket-version", "13"); - WSL_CHECK_EX("sec-websocket-key"); - WSL_CHECK_EX("connection"); -#undef WSL_CHECK_EX -#undef WSL_CHECK - key = headers["sec-websocket-key"]; - if (headers.has("sec-websocket-protocol")) { - Vector<String> protos = headers["sec-websocket-protocol"].split(","); - for (int i = 0; i < protos.size(); i++) { - String proto = protos[i].strip_edges(); - // Check if we have the given protocol - for (int j = 0; j < p_protocols.size(); j++) { - if (proto != p_protocols[j]) { - continue; - } - protocol = proto; - break; - } - // Found a protocol - if (!protocol.is_empty()) { - break; - } - } - if (protocol.is_empty()) { // Invalid protocol(s) requested - return false; - } - } else if (p_protocols.size() > 0) { // No protocol requested, but we need one - return false; - } - return true; -} - -Error WSLServer::PendingPeer::do_handshake(const Vector<String> p_protocols, uint64_t p_timeout, String &r_resource_name, const Vector<String> &p_extra_headers) { - if (OS::get_singleton()->get_ticks_msec() - time > p_timeout) { - print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", p_timeout * 0.001)); - return ERR_TIMEOUT; - } - - if (use_tls) { - Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(connection); - if (tls.is_null()) { - ERR_FAIL_V_MSG(ERR_BUG, "Couldn't get StreamPeerTLS for WebSocket handshake."); - } - tls->poll(); - if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { - return ERR_BUSY; - } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { - print_verbose(vformat("WebSocket SSL connection error during handshake (StreamPeerTLS status code %d).", tls->get_status())); - return FAILED; - } - } - - if (!has_request) { - int read = 0; - while (true) { - ERR_FAIL_COND_V_MSG(req_pos >= WSL_MAX_HEADER_SIZE, ERR_OUT_OF_MEMORY, "WebSocket response headers are too big."); - Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); - if (err != OK) { // Got an error - print_verbose(vformat("WebSocket error while getting partial data (StreamPeer error code %d).", err)); - return FAILED; - } else if (read != 1) { // Busy, wait next poll - return ERR_BUSY; - } - char *r = (char *)req_buf; - int l = req_pos; - if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { - r[l - 3] = '\0'; - if (!_parse_request(p_protocols, r_resource_name)) { - return FAILED; - } - String s = "HTTP/1.1 101 Switching Protocols\r\n"; - s += "Upgrade: websocket\r\n"; - s += "Connection: Upgrade\r\n"; - s += "Sec-WebSocket-Accept: " + WSLPeer::compute_key_response(key) + "\r\n"; - if (!protocol.is_empty()) { - s += "Sec-WebSocket-Protocol: " + protocol + "\r\n"; - } - for (int i = 0; i < p_extra_headers.size(); i++) { - s += p_extra_headers[i] + "\r\n"; - } - s += "\r\n"; - response = s.utf8(); - has_request = true; - break; - } - req_pos += 1; - } - } - - if (has_request && response_sent < response.size() - 1) { - int sent = 0; - Error err = connection->put_partial_data((const uint8_t *)response.get_data() + response_sent, response.size() - response_sent - 1, sent); - if (err != OK) { - print_verbose(vformat("WebSocket error while putting partial data (StreamPeer error code %d).", err)); - return err; - } - response_sent += sent; - } - - if (response_sent < response.size() - 1) { - return ERR_BUSY; - } - - return OK; -} - -void WSLServer::set_extra_headers(const Vector<String> &p_headers) { - _extra_headers = p_headers; -} - -Error WSLServer::listen(int p_port, const Vector<String> p_protocols, bool gd_mp_api) { - ERR_FAIL_COND_V(is_listening(), ERR_ALREADY_IN_USE); - - _is_multiplayer = gd_mp_api; - // Strip edges from protocols. - _protocols.resize(p_protocols.size()); - String *pw = _protocols.ptrw(); - for (int i = 0; i < p_protocols.size(); i++) { - pw[i] = p_protocols[i].strip_edges(); - } - return _server->listen(p_port, bind_ip); -} - -void WSLServer::poll() { - List<int> remove_ids; - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - Ref<WSLPeer> peer = const_cast<WSLPeer *>(static_cast<const WSLPeer *>(E.value.ptr())); - peer->poll(); - if (!peer->is_connected_to_host()) { - _on_disconnect(E.key, peer->close_code != -1); - remove_ids.push_back(E.key); - } - } - for (int &E : remove_ids) { - _peer_map.erase(E); - } - remove_ids.clear(); - - List<Ref<PendingPeer>> remove_peers; - for (const Ref<PendingPeer> &E : _pending) { - String resource_name; - Ref<PendingPeer> ppeer = E; - Error err = ppeer->do_handshake(_protocols, handshake_timeout, resource_name, _extra_headers); - if (err == ERR_BUSY) { - continue; - } else if (err != OK) { - remove_peers.push_back(ppeer); - continue; - } - // Creating new peer - int32_t id = generate_unique_id(); - - WSLPeer::PeerData *data = memnew(struct WSLPeer::PeerData); - data->obj = this; - data->conn = ppeer->connection; - data->tcp = ppeer->tcp; - data->is_server = true; - data->id = id; - - Ref<WSLPeer> ws_peer = memnew(WSLPeer); - ws_peer->make_context(data, _in_buf_size, _in_pkt_size, _out_buf_size, _out_pkt_size); - ws_peer->set_no_delay(true); - - _peer_map[id] = ws_peer; - remove_peers.push_back(ppeer); - _on_connect(id, ppeer->protocol, resource_name); - } - for (const Ref<PendingPeer> &E : remove_peers) { - _pending.erase(E); - } - remove_peers.clear(); - - if (!_server->is_listening()) { - return; - } - - while (_server->is_connection_available()) { - Ref<StreamPeerTCP> conn = _server->take_connection(); - if (is_refusing_new_connections()) { - continue; // Conn will go out-of-scope and be closed. - } - - Ref<PendingPeer> peer = memnew(PendingPeer); - if (private_key.is_valid() && tls_cert.is_valid()) { - Ref<StreamPeerTLS> tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); - tls->set_blocking_handshake_enabled(false); - tls->accept_stream(conn, private_key, tls_cert, ca_chain); - peer->connection = tls; - peer->use_tls = true; - } else { - peer->connection = conn; - } - peer->tcp = conn; - peer->time = OS::get_singleton()->get_ticks_msec(); - _pending.push_back(peer); - } -} - -bool WSLServer::is_listening() const { - return _server->is_listening(); -} - -int WSLServer::get_max_packet_size() const { - return (1 << _out_buf_size) - PROTO_SIZE; -} - -void WSLServer::stop() { - _server->stop(); - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - Ref<WSLPeer> peer = const_cast<WSLPeer *>(static_cast<const WSLPeer *>(E.value.ptr())); - peer->close_now(); - } - _pending.clear(); - _peer_map.clear(); - _protocols.clear(); -} - -bool WSLServer::has_peer(int p_id) const { - return _peer_map.has(p_id); -} - -Ref<WebSocketPeer> WSLServer::get_peer(int p_id) const { - ERR_FAIL_COND_V(!has_peer(p_id), nullptr); - return _peer_map[p_id]; -} - -IPAddress WSLServer::get_peer_address(int p_peer_id) const { - ERR_FAIL_COND_V(!has_peer(p_peer_id), IPAddress()); - - return _peer_map[p_peer_id]->get_connected_host(); -} - -int WSLServer::get_peer_port(int p_peer_id) const { - ERR_FAIL_COND_V(!has_peer(p_peer_id), 0); - - return _peer_map[p_peer_id]->get_connected_port(); -} - -void WSLServer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { - ERR_FAIL_COND(!has_peer(p_peer_id)); - - get_peer(p_peer_id)->close(p_code, p_reason); -} - -Error WSLServer::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - ERR_FAIL_COND_V_MSG(_server->is_listening(), FAILED, "Buffers sizes can only be set before listening or connecting."); - - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - _out_pkt_size = nearest_shift(p_out_packets - 1); - return OK; -} - -WSLServer::WSLServer() { - _server.instantiate(); -} - -WSLServer::~WSLServer() { - stop(); -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/wsl_server.h b/modules/websocket/wsl_server.h deleted file mode 100644 index df0c1dc68a..0000000000 --- a/modules/websocket/wsl_server.h +++ /dev/null @@ -1,98 +0,0 @@ -/*************************************************************************/ -/* wsl_server.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WSL_SERVER_H -#define WSL_SERVER_H - -#ifndef WEB_ENABLED - -#include "websocket_server.h" -#include "wsl_peer.h" - -#include "core/io/stream_peer_tcp.h" -#include "core/io/stream_peer_tls.h" -#include "core/io/tcp_server.h" - -class WSLServer : public WebSocketServer { - GDCIIMPL(WSLServer, WebSocketServer); - -private: - class PendingPeer : public RefCounted { - private: - bool _parse_request(const Vector<String> p_protocols, String &r_resource_name); - - public: - Ref<StreamPeerTCP> tcp; - Ref<StreamPeer> connection; - bool use_tls = false; - - uint64_t time = 0; - uint8_t req_buf[WSL_MAX_HEADER_SIZE] = {}; - int req_pos = 0; - String key; - String protocol; - bool has_request = false; - CharString response; - int response_sent = 0; - - Error do_handshake(const Vector<String> p_protocols, uint64_t p_timeout, String &r_resource_name, const Vector<String> &p_extra_headers); - }; - - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; - int _out_pkt_size = DEF_PKT_SHIFT; - - List<Ref<PendingPeer>> _pending; - Ref<TCPServer> _server; - Vector<String> _protocols; - Vector<String> _extra_headers; - -public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - void set_extra_headers(const Vector<String> &p_headers) override; - Error listen(int p_port, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false) override; - void stop() override; - bool is_listening() const override; - int get_max_packet_size() const override; - bool has_peer(int p_id) const override; - Ref<WebSocketPeer> get_peer(int p_id) const override; - IPAddress get_peer_address(int p_peer_id) const override; - int get_peer_port(int p_peer_id) const override; - void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = "") override; - virtual void poll() override; - - WSLServer(); - ~WSLServer(); -}; - -#endif // WEB_ENABLED - -#endif // WSL_SERVER_H diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 366bd1c48c..ef3b79b630 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -2639,8 +2639,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } if (user_data.libs.size() > 0) { Ref<FileAccess> fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); - JSON json; - fa->store_string(json.stringify(user_data.libs, "\t")); + fa->store_string(JSON::stringify(user_data.libs, "\t")); } } else { print_verbose("Saving apk expansion file.."); diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 4cbd9722ad..995a904398 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -34,6 +34,11 @@ #include "main/main.h" #include "servers/display_server.h" +#include "modules/modules_enabled.gen.h" // For regex. +#ifdef MODULE_REGEX_ENABLED +#include "modules/regex/regex.h" +#endif + #ifdef X11_ENABLED #include "display_server_x11.h" #endif @@ -240,6 +245,200 @@ String OS_LinuxBSD::get_version() const { return uts.version; } +Vector<String> OS_LinuxBSD::get_video_adapter_driver_info() const { + const String rendering_device_name = RenderingServer::get_singleton()->get_rendering_device()->get_device_name(); // e.g. `NVIDIA GeForce GTX 970` + const String rendering_device_vendor = RenderingServer::get_singleton()->get_rendering_device()->get_device_vendor_name(); // e.g. `NVIDIA` + const String card_name = rendering_device_name.trim_prefix(rendering_device_vendor).strip_edges(); // -> `GeForce GTX 970` + + String vendor_device_id_mappings; + List<String> lspci_args; + lspci_args.push_back("-n"); + Error err = const_cast<OS_LinuxBSD *>(this)->execute("lspci", lspci_args, &vendor_device_id_mappings); + if (err != OK || vendor_device_id_mappings.is_empty()) { + return Vector<String>(); + } + + // Usually found under "VGA", but for example NVIDIA mobile/laptop adapters are often listed under "3D" and some AMD adapters are under "Display". + const String dc_vga = "0300"; // VGA compatible controller + const String dc_display = "0302"; // Display controller + const String dc_3d = "0380"; // 3D controller + + // splitting results by device class allows prioritizing, if multiple devices are found. + Vector<String> class_vga_device_candidates; + Vector<String> class_display_device_candidates; + Vector<String> class_3d_device_candidates; + +#ifdef MODULE_REGEX_ENABLED + RegEx regex_id_format = RegEx(); + regex_id_format.compile("^[a-f0-9]{4}:[a-f0-9]{4}$"); // e.g. `10de:13c2`; IDs are always in hexadecimal +#endif + + Vector<String> value_lines = vendor_device_id_mappings.split("\n", false); // example: `02:00.0 0300: 10de:13c2 (rev a1)` + for (const String &line : value_lines) { + Vector<String> columns = line.split(" ", false); + if (columns.size() < 3) { + continue; + } + String device_class = columns[1].trim_suffix(":"); + String vendor_device_id_mapping = columns[2]; + +#ifdef MODULE_REGEX_ENABLED + if (regex_id_format.search(vendor_device_id_mapping).is_null()) { + continue; + } +#endif + + if (device_class == dc_vga) { + class_vga_device_candidates.push_back(vendor_device_id_mapping); + } else if (device_class == dc_display) { + class_display_device_candidates.push_back(vendor_device_id_mapping); + } else if (device_class == dc_3d) { + class_3d_device_candidates.push_back(vendor_device_id_mapping); + } + } + + // Check results against currently used device (`card_name`), in case the user has multiple graphics cards. + const String device_lit = "Device"; // line of interest + class_vga_device_candidates = OS_LinuxBSD::lspci_device_filter(class_vga_device_candidates, dc_vga, device_lit, card_name); + class_display_device_candidates = OS_LinuxBSD::lspci_device_filter(class_display_device_candidates, dc_display, device_lit, card_name); + class_3d_device_candidates = OS_LinuxBSD::lspci_device_filter(class_3d_device_candidates, dc_3d, device_lit, card_name); + + // Get driver names and filter out invalid ones, because some adapters are dummys used only for passthrough. + // And they have no indicator besides certain driver names. + const String kernel_lit = "Kernel driver in use"; // line of interest + const String dummys = "vfio"; // for e.g. pci passthrough dummy kernel driver `vfio-pci` + Vector<String> class_vga_device_drivers = OS_LinuxBSD::lspci_get_device_value(class_vga_device_candidates, kernel_lit, dummys); + Vector<String> class_display_device_drivers = OS_LinuxBSD::lspci_get_device_value(class_display_device_candidates, kernel_lit, dummys); + Vector<String> class_3d_device_drivers = OS_LinuxBSD::lspci_get_device_value(class_3d_device_candidates, kernel_lit, dummys); + + static String driver_name; + static String driver_version; + + // Use first valid value: + for (const String &driver : class_3d_device_drivers) { + driver_name = driver; + break; + } + if (driver_name.is_empty()) { + for (const String &driver : class_display_device_drivers) { + driver_name = driver; + break; + } + } + if (driver_name.is_empty()) { + for (const String &driver : class_vga_device_drivers) { + driver_name = driver; + break; + } + } + + Vector<String> info; + info.push_back(driver_name); + + String modinfo; + List<String> modinfo_args; + modinfo_args.push_back(driver_name); + err = const_cast<OS_LinuxBSD *>(this)->execute("modinfo", modinfo_args, &modinfo); + if (err != OK || modinfo.is_empty()) { + info.push_back(""); // So that this method always either returns an empty array, or an array of length 2. + return info; + } + Vector<String> lines = modinfo.split("\n", false); + for (const String &line : lines) { + Vector<String> columns = line.split(":", false, 1); + if (columns.size() < 2) { + continue; + } + if (columns[0].strip_edges() == "version") { + driver_version = columns[1].strip_edges(); // example value: `510.85.02` on Linux/BSD + break; + } + } + + info.push_back(driver_version); + + return info; +} + +Vector<String> OS_LinuxBSD::lspci_device_filter(Vector<String> vendor_device_id_mapping, String class_suffix, String check_column, String whitelist) const { + // NOTE: whitelist can be changed to `Vector<String>`, if the need arises. + const String sep = ":"; + Vector<String> devices; + for (const String &mapping : vendor_device_id_mapping) { + String device; + List<String> d_args; + d_args.push_back("-d"); + d_args.push_back(mapping + sep + class_suffix); + d_args.push_back("-vmm"); + Error err = const_cast<OS_LinuxBSD *>(this)->execute("lspci", d_args, &device); // e.g. `lspci -d 10de:13c2:0300 -vmm` + if (err != OK) { + return Vector<String>(); + } else if (device.is_empty()) { + continue; + } + + Vector<String> device_lines = device.split("\n", false); + for (const String &line : device_lines) { + Vector<String> columns = line.split(":", false, 1); + if (columns.size() < 2) { + continue; + } + if (columns[0].strip_edges() == check_column) { + // for `column[0] == "Device"` this may contain `GM204 [GeForce GTX 970]` + bool is_valid = true; + if (!whitelist.is_empty()) { + is_valid = columns[1].strip_edges().contains(whitelist); + } + if (is_valid) { + devices.push_back(mapping); + } + break; + } + } + } + return devices; +} + +Vector<String> OS_LinuxBSD::lspci_get_device_value(Vector<String> vendor_device_id_mapping, String check_column, String blacklist) const { + // NOTE: blacklist can be changed to `Vector<String>`, if the need arises. + const String sep = ":"; + Vector<String> values; + for (const String &mapping : vendor_device_id_mapping) { + String device; + List<String> d_args; + d_args.push_back("-d"); + d_args.push_back(mapping); + d_args.push_back("-k"); + Error err = const_cast<OS_LinuxBSD *>(this)->execute("lspci", d_args, &device); // e.g. `lspci -d 10de:13c2 -k` + if (err != OK) { + return Vector<String>(); + } else if (device.is_empty()) { + continue; + } + + Vector<String> device_lines = device.split("\n", false); + for (const String &line : device_lines) { + Vector<String> columns = line.split(":", false, 1); + if (columns.size() < 2) { + continue; + } + if (columns[0].strip_edges() == check_column) { + // for `column[0] == "Kernel driver in use"` this may contain `nvidia` + bool is_valid = true; + const String value = columns[1].strip_edges(); + if (!blacklist.is_empty()) { + is_valid = !value.contains(blacklist); + } + if (is_valid) { + values.push_back(value); + } + break; + } + } + } + return values; +} + Error OS_LinuxBSD::shell_open(String p_uri) { Error ok; int err_code; diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 722d83ba19..aea04c1363 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -69,6 +69,9 @@ class OS_LinuxBSD : public OS_Unix { String get_systemd_os_release_info_value(const String &key) const; + Vector<String> lspci_device_filter(Vector<String> vendor_device_id_mapping, String class_suffix, String check_column, String whitelist) const; + Vector<String> lspci_get_device_value(Vector<String> vendor_device_id_mapping, String check_column, String blacklist) const; + protected: virtual void initialize() override; virtual void finalize() override; @@ -82,6 +85,8 @@ public: virtual String get_distribution_name() const override; virtual String get_version() const override; + virtual Vector<String> get_video_adapter_driver_info() const override; + virtual MainLoop *get_main_loop() const override; virtual uint64_t get_embedded_pck_offset() const override; diff --git a/platform/macos/joypad_macos.h b/platform/macos/joypad_macos.h index 4b14fed6d5..8743fc91a9 100644 --- a/platform/macos/joypad_macos.h +++ b/platform/macos/joypad_macos.h @@ -31,14 +31,10 @@ #ifndef JOYPAD_MACOS_H #define JOYPAD_MACOS_H -#ifdef MACOS_10_0_4 -#import <IOKit/hidsystem/IOHIDUsageTables.h> -#else -#import <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> -#endif #import <ForceFeedback/ForceFeedback.h> #import <ForceFeedback/ForceFeedbackConstants.h> #import <IOKit/hid/IOHIDLib.h> +#import <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> #include "core/input/input.h" diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 8050d299f0..141c28c713 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -159,7 +159,7 @@ Error OS_UWP::initialize(const VideoMode &p_desired, int p_video_driver, int p_a outside = true; // FIXME: Hardcoded for now, add Vulkan support. - p_video_driver = VIDEO_DRIVER_OPENGL; + p_video_driver = RENDERING_DRIVER_OPENGL3; ContextEGL_UWP::Driver opengl_api_type = ContextEGL_UWP::GLES_2_0; bool gl_initialization_error = false; @@ -826,10 +826,6 @@ OS_UWP::OS_UWP() { pressrc = 0; old_invalid = true; mouse_mode = MOUSE_MODE_VISIBLE; -#ifdef STDOUT_FILE - stdo = fopen("stdout.txt", "wb"); -#endif - gl_context = nullptr; display_request = ref new Windows::System::Display::DisplayRequest(); @@ -847,7 +843,4 @@ OS_UWP::OS_UWP() { } OS_UWP::~OS_UWP() { -#ifdef STDOUT_FILE - fclose(stdo); -#endif } diff --git a/platform/windows/detect.py b/platform/windows/detect.py index a5d8d0344b..74868fc6a2 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -401,6 +401,7 @@ def configure_msvc(env, vcvars_msvc_config): "Avrt", "dwmapi", "dwrite", + "wbemuuid", ] if env["vulkan"]: @@ -577,6 +578,7 @@ def configure_mingw(env): "uuid", "dwmapi", "dwrite", + "wbemuuid", ] ) diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 241e0b382e..5e4ba4a9e3 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -53,6 +53,7 @@ #include <process.h> #include <regstr.h> #include <shlobj.h> +#include <wbemcli.h> extern "C" { __declspec(dllexport) DWORD NvOptimusEnablement = 1; @@ -309,6 +310,97 @@ String OS_Windows::get_version() const { return ""; } +Vector<String> OS_Windows::get_video_adapter_driver_info() const { + REFCLSID clsid = CLSID_WbemLocator; // Unmarshaler CLSID + REFIID uuid = IID_IWbemLocator; // Interface UUID + IWbemLocator *wbemLocator = NULL; // to get the services + IWbemServices *wbemServices = NULL; // to get the class + IEnumWbemClassObject *iter = NULL; + IWbemClassObject *pnpSDriverObject[1]; // contains driver name, version, etc. + static String driver_name; + static String driver_version; + + const String device_name = RenderingServer::get_singleton()->get_rendering_device()->get_device_name(); + if (device_name.is_empty()) { + return Vector<String>(); + } + + CoInitialize(nullptr); + + HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); + if (hr != S_OK) { + return Vector<String>(); + } + + hr = wbemLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, 0, NULL, 0, 0, &wbemServices); + SAFE_RELEASE(wbemLocator) // from now on, use `wbemServices` + if (hr != S_OK) { + SAFE_RELEASE(wbemServices) + return Vector<String>(); + } + + const String gpu_device_class_query = vformat("SELECT * FROM Win32_PnPSignedDriver WHERE DeviceName = \"%s\"", device_name); + BSTR query = SysAllocString((const WCHAR *)gpu_device_class_query.utf16().get_data()); + BSTR query_lang = SysAllocString(L"WQL"); + hr = wbemServices->ExecQuery(query_lang, query, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &iter); + SysFreeString(query_lang); + SysFreeString(query); + if (hr == S_OK) { + ULONG resultCount; + hr = iter->Next(5000, 1, pnpSDriverObject, &resultCount); // Get exactly 1. Wait max 5 seconds. + + if (hr == S_OK && resultCount > 0) { + VARIANT dn; + VariantInit(&dn); + + BSTR object_name = SysAllocString(L"DriverName"); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + SysFreeString(object_name); + if (hr == S_OK) { + String d_name = String(V_BSTR(&dn)); + if (d_name.is_empty()) { + object_name = SysAllocString(L"DriverProviderName"); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + SysFreeString(object_name); + if (hr == S_OK) { + driver_name = String(V_BSTR(&dn)); + } + } else { + driver_name = d_name; + } + } else { + object_name = SysAllocString(L"DriverProviderName"); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + SysFreeString(object_name); + if (hr == S_OK) { + driver_name = String(V_BSTR(&dn)); + } + } + + VARIANT dv; + VariantInit(&dv); + object_name = SysAllocString(L"DriverVersion"); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dv, NULL, NULL); + SysFreeString(object_name); + if (hr == S_OK) { + driver_version = String(V_BSTR(&dv)); + } + for (ULONG i = 0; i < resultCount; i++) { + SAFE_RELEASE(pnpSDriverObject[i]) + } + } + } + + SAFE_RELEASE(wbemServices) + SAFE_RELEASE(iter) + + Vector<String> info; + info.push_back(driver_name); + info.push_back(driver_version); + + return info; +} + OS::DateTime OS_Windows::get_datetime(bool p_utc) const { SYSTEMTIME systemtime; if (p_utc) { @@ -1123,15 +1215,7 @@ Error OS_Windows::move_to_trash(const String &p_path) { } OS_Windows::OS_Windows(HINSTANCE _hInstance) { - ticks_per_second = 0; - ticks_start = 0; - main_loop = nullptr; - process_map = nullptr; - hInstance = _hInstance; -#ifdef STDOUT_FILE - stdo = fopen("stdout.txt", "wb"); -#endif #ifdef WASAPI_ENABLED AudioDriverManager::add_driver(&driver_wasapi); @@ -1147,11 +1231,8 @@ OS_Windows::OS_Windows(HINSTANCE _hInstance) { // // NOTE: The engine does not use ANSI escape codes to color error/warning messages; it uses Windows API calls instead. // Therefore, error/warning messages are still colored on Windows versions older than 10. - HANDLE stdoutHandle; - stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - DWORD outMode = 0; - outMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - + HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD outMode = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(stdoutHandle, outMode)) { // Windows 8.1 or below, or Windows 10 prior to Anniversary Update. print_verbose("Can't set the ENABLE_VIRTUAL_TERMINAL_PROCESSING Windows console mode. `print_rich()` will not work as expected."); @@ -1163,7 +1244,4 @@ OS_Windows::OS_Windows(HINSTANCE _hInstance) { } OS_Windows::~OS_Windows() { -#ifdef STDOUT_FILE - fclose(stdo); -#endif } diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index b792f6fa44..bf934bce64 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -84,13 +84,10 @@ public: }; class JoypadWindows; -class OS_Windows : public OS { -#ifdef STDOUT_FILE - FILE *stdo = nullptr; -#endif - uint64_t ticks_start; - uint64_t ticks_per_second; +class OS_Windows : public OS { + uint64_t ticks_start = 0; + uint64_t ticks_per_second = 0; HINSTANCE hInstance; MainLoop *main_loop = nullptr; @@ -130,7 +127,7 @@ protected: STARTUPINFO si; PROCESS_INFORMATION pi; }; - HashMap<ProcessID, ProcessInfo> *process_map; + HashMap<ProcessID, ProcessInfo> *process_map = nullptr; public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; @@ -147,6 +144,8 @@ public: virtual String get_distribution_name() const override; virtual String get_version() const override; + virtual Vector<String> get_video_adapter_driver_info() const override; + virtual void initialize_joypads() override {} virtual DateTime get_datetime(bool p_utc) const override; diff --git a/scene/2d/light_2d.cpp b/scene/2d/light_2d.cpp index 90402260ed..80169bc80c 100644 --- a/scene/2d/light_2d.cpp +++ b/scene/2d/light_2d.cpp @@ -454,7 +454,7 @@ void DirectionalLight2D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_max_distance", "pixels"), &DirectionalLight2D::set_max_distance); ClassDB::bind_method(D_METHOD("get_max_distance"), &DirectionalLight2D::get_max_distance); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height", PROPERTY_HINT_RANGE, "0,1,0.01,suffix:px"), "set_height", "get_height"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_height", "get_height"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_distance", PROPERTY_HINT_RANGE, "0,16384.0,1.0,or_greater,suffix:px"), "set_max_distance", "get_max_distance"); } diff --git a/scene/2d/sprite_2d.cpp b/scene/2d/sprite_2d.cpp index 08068acf7d..0784318442 100644 --- a/scene/2d/sprite_2d.cpp +++ b/scene/2d/sprite_2d.cpp @@ -444,7 +444,7 @@ void Sprite2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "hframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_hframes", "get_hframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_vframes", "get_vframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "frame"), "set_frame", "get_frame"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "frame_coords", PROPERTY_HINT_NONE, "suffix:px", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); ADD_GROUP("Region", "region_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "region_enabled"), "set_region_enabled", "is_region_enabled"); diff --git a/scene/3d/area_3d.cpp b/scene/3d/area_3d.cpp index cefa9eceff..c6433e773d 100644 --- a/scene/3d/area_3d.cpp +++ b/scene/3d/area_3d.cpp @@ -578,11 +578,11 @@ bool Area3D::is_using_reverb_bus() const { return use_reverb_bus; } -void Area3D::set_reverb_bus(const StringName &p_audio_bus) { +void Area3D::set_reverb_bus_name(const StringName &p_audio_bus) { reverb_bus = p_audio_bus; } -StringName Area3D::get_reverb_bus() const { +StringName Area3D::get_reverb_bus_name() const { for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) { if (AudioServer::get_singleton()->get_bus_name(i) == reverb_bus) { return reverb_bus; @@ -711,8 +711,8 @@ void Area3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_use_reverb_bus", "enable"), &Area3D::set_use_reverb_bus); ClassDB::bind_method(D_METHOD("is_using_reverb_bus"), &Area3D::is_using_reverb_bus); - ClassDB::bind_method(D_METHOD("set_reverb_bus", "name"), &Area3D::set_reverb_bus); - ClassDB::bind_method(D_METHOD("get_reverb_bus"), &Area3D::get_reverb_bus); + ClassDB::bind_method(D_METHOD("set_reverb_bus_name", "name"), &Area3D::set_reverb_bus_name); + ClassDB::bind_method(D_METHOD("get_reverb_bus_name"), &Area3D::get_reverb_bus_name); ClassDB::bind_method(D_METHOD("set_reverb_amount", "amount"), &Area3D::set_reverb_amount); ClassDB::bind_method(D_METHOD("get_reverb_amount"), &Area3D::get_reverb_amount); @@ -760,8 +760,8 @@ void Area3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "audio_bus_name", PROPERTY_HINT_ENUM, ""), "set_audio_bus_name", "get_audio_bus_name"); ADD_GROUP("Reverb Bus", "reverb_bus_"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reverb_bus_enable"), "set_use_reverb_bus", "is_using_reverb_bus"); - ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "reverb_bus_name", PROPERTY_HINT_ENUM, ""), "set_reverb_bus", "get_reverb_bus"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reverb_bus_enabled"), "set_use_reverb_bus", "is_using_reverb_bus"); + ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "reverb_bus_name", PROPERTY_HINT_ENUM, ""), "set_reverb_bus_name", "get_reverb_bus_name"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "reverb_bus_amount", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_reverb_amount", "get_reverb_amount"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "reverb_bus_uniformity", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_reverb_uniformity", "get_reverb_uniformity"); diff --git a/scene/3d/area_3d.h b/scene/3d/area_3d.h index 2125c35f67..195f9f0d9e 100644 --- a/scene/3d/area_3d.h +++ b/scene/3d/area_3d.h @@ -215,8 +215,8 @@ public: void set_use_reverb_bus(bool p_enable); bool is_using_reverb_bus() const; - void set_reverb_bus(const StringName &p_audio_bus); - StringName get_reverb_bus() const; + void set_reverb_bus_name(const StringName &p_audio_bus); + StringName get_reverb_bus_name() const; void set_reverb_amount(float p_amount); float get_reverb_amount() const; diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp index 76cf0d2fd0..40afbdf2ed 100644 --- a/scene/3d/audio_stream_player_3d.cpp +++ b/scene/3d/audio_stream_player_3d.cpp @@ -485,7 +485,7 @@ Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() { } if (area->is_using_reverb_bus()) { - StringName reverb_bus_name = area->get_reverb_bus(); + StringName reverb_bus_name = area->get_reverb_bus_name(); Vector<AudioFrame> reverb_vol; _calc_reverb_vol(area, listener_area_pos, output_volume_vector, reverb_vol); bus_volumes[reverb_bus_name] = reverb_vol; diff --git a/scene/3d/light_3d.cpp b/scene/3d/light_3d.cpp index 23fd091be6..198dba7811 100644 --- a/scene/3d/light_3d.cpp +++ b/scene/3d/light_3d.cpp @@ -165,6 +165,16 @@ AABB Light3D::get_aabb() const { return AABB(); } +PackedStringArray Light3D::get_configuration_warnings() const { + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); + + if (!get_scale().is_equal_approx(Vector3(1, 1, 1))) { + warnings.push_back(RTR("A light's scale does not affect the visual size of the light.")); + } + + return warnings; +} + void Light3D::set_bake_mode(BakeMode p_mode) { bake_mode = p_mode; RS::get_singleton()->light_set_bake_mode(light, RS::LightBakeMode(p_mode)); @@ -579,7 +589,7 @@ OmniLight3D::ShadowMode OmniLight3D::get_shadow_mode() const { } PackedStringArray OmniLight3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Light3D::get_configuration_warnings(); if (!has_shadow() && get_projector().is_valid()) { warnings.push_back(RTR("Projector texture only works with shadows active.")); @@ -609,7 +619,7 @@ OmniLight3D::OmniLight3D() : } PackedStringArray SpotLight3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Light3D::get_configuration_warnings(); if (has_shadow() && get_param(PARAM_SPOT_ANGLE) >= 90.0) { warnings.push_back(RTR("A SpotLight3D with an angle wider than 90 degrees cannot cast shadows.")); diff --git a/scene/3d/light_3d.h b/scene/3d/light_3d.h index 8da45bee79..84d214030b 100644 --- a/scene/3d/light_3d.h +++ b/scene/3d/light_3d.h @@ -147,6 +147,7 @@ public: Color get_correlated_color() const; virtual AABB get_aabb() const override; + virtual PackedStringArray get_configuration_warnings() const override; Light3D(); ~Light3D(); diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index cc69a1cc51..be6eab2178 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -810,7 +810,7 @@ void Sprite3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "hframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_hframes", "get_hframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_vframes", "get_vframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "frame"), "set_frame", "get_frame"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "frame_coords", PROPERTY_HINT_NONE, "suffix:px", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); ADD_GROUP("Region", "region_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "region_enabled"), "set_region_enabled", "is_region_enabled"); ADD_PROPERTY(PropertyInfo(Variant::RECT2, "region_rect", PROPERTY_HINT_NONE, "suffix:px"), "set_region_rect", "get_region_rect"); diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index 2e3d0a26c2..e306d00a51 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -1399,7 +1399,7 @@ Error AnimationPlayer::add_animation_library(const StringName &p_name, const Ref animation_libraries.insert(insert_pos, ald); ald.library->connect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added).bind(p_name)); - ald.library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added).bind(p_name)); + ald.library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_removed).bind(p_name)); ald.library->connect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed).bind(p_name)); for (const KeyValue<StringName, Ref<Animation>> &K : ald.library->animations) { @@ -1465,11 +1465,11 @@ void AnimationPlayer::rename_animation_library(const StringName &p_name, const S animation_libraries[i].name = p_new_name; // rename connections animation_libraries[i].library->disconnect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added)); - animation_libraries[i].library->disconnect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added)); + animation_libraries[i].library->disconnect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_removed)); animation_libraries[i].library->disconnect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed)); animation_libraries[i].library->connect(SNAME("animation_added"), callable_mp(this, &AnimationPlayer::_animation_added).bind(p_new_name)); - animation_libraries[i].library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_added).bind(p_new_name)); + animation_libraries[i].library->connect(SNAME("animation_removed"), callable_mp(this, &AnimationPlayer::_animation_removed).bind(p_new_name)); animation_libraries[i].library->connect(SNAME("animation_renamed"), callable_mp(this, &AnimationPlayer::_animation_renamed).bind(p_new_name)); for (const KeyValue<StringName, Ref<Animation>> &K : animation_libraries[i].library->animations) { diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 5060bacba5..9217f31310 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -2956,7 +2956,7 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub memdelete(p_item); } -void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment) { +void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -2969,7 +2969,15 @@ void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, ERR_FAIL_COND(p_image->get_height() == 0); ItemImage *item = memnew(ItemImage); - item->image = p_image; + if (p_region.has_area()) { + Ref<AtlasTexture> atlas_tex = memnew(AtlasTexture); + atlas_tex->set_atlas(p_image); + atlas_tex->set_region(p_region); + item->image = atlas_tex; + } else { + item->image = p_image; + } + item->color = p_color; item->inline_align = p_alignment; @@ -2981,17 +2989,30 @@ void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, item->size.height = p_height; } else { // calculate height to keep aspect ratio - item->size.height = p_image->get_height() * p_width / p_image->get_width(); + if (p_region.has_area()) { + item->size.height = p_region.get_size().height * p_width / p_region.get_size().width; + } else { + item->size.height = p_image->get_height() * p_width / p_image->get_width(); + } } } else { if (p_height > 0) { // custom height item->size.height = p_height; // calculate width to keep aspect ratio - item->size.width = p_image->get_width() * p_height / p_image->get_height(); + if (p_region.has_area()) { + item->size.width = p_region.get_size().width * p_height / p_region.get_size().height; + } else { + item->size.width = p_image->get_width() * p_height / p_image->get_height(); + } } else { - // keep original width and height - item->size = p_image->get_size(); + if (p_region.has_area()) { + // if the image has a region, keep the region size + item->size = p_region.get_size(); + } else { + // keep original width and height + item->size = p_image->get_size(); + } } } @@ -4126,6 +4147,18 @@ void RichTextLabel::append_text(const String &p_bbcode) { Ref<Texture2D> texture = ResourceLoader::load(image, "Texture2D"); if (texture.is_valid()) { + Rect2 region; + OptionMap::Iterator region_option = bbcode_options.find("region"); + if (region_option) { + Vector<String> region_values = region_option->value.split(",", false); + if (region_values.size() == 4) { + region.position.x = region_values[0].to_float(); + region.position.y = region_values[1].to_float(); + region.size.x = region_values[2].to_float(); + region.size.y = region_values[3].to_float(); + } + } + Color color = Color(1.0, 1.0, 1.0); OptionMap::Iterator color_option = bbcode_options.find("color"); if (color_option) { @@ -4154,7 +4187,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { } } - add_image(texture, width, height, color, (InlineAlignment)alignment); + add_image(texture, width, height, color, (InlineAlignment)alignment, region); } pos = end; @@ -5209,7 +5242,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_parsed_text"), &RichTextLabel::get_parsed_text); ClassDB::bind_method(D_METHOD("add_text", "text"), &RichTextLabel::add_text); ClassDB::bind_method(D_METHOD("set_text", "text"), &RichTextLabel::set_text); - ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER)); + ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2(0, 0, 0, 0))); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); ClassDB::bind_method(D_METHOD("remove_line", "line"), &RichTextLabel::remove_line); ClassDB::bind_method(D_METHOD("push_font", "font", "font_size"), &RichTextLabel::push_font); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 73b7676c22..04a682349d 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -569,7 +569,7 @@ private: public: String get_parsed_text() const; void add_text(const String &p_text); - void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER); + void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(0, 0, 0, 0)); void add_newline(); bool remove_line(const int p_line); void push_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins = Rect2(), const Color &p_color = Color(1, 1, 1), int p_ol_size = 0, const Color &p_ol_color = Color(0, 0, 0, 0)); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 598420da37..4cd244306c 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -4825,10 +4825,11 @@ void TextEdit::select_word_under_caret(int p_caret) { continue; } - select(get_caret_line(c), begin, get_caret_column(c), end, c); + select(get_caret_line(c), begin, get_caret_line(c), end, c); // Move the caret to the end of the word for easier editing. set_caret_column(end, false, c); } + merge_overlapping_carets(); } void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) { diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 101a63db1b..6ab27853f1 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -1024,11 +1024,9 @@ String increase_numeric_string(const String &s) { void Node::_generate_serial_child_name(const Node *p_child, StringName &name) const { if (name == StringName()) { - //no name and a new name is needed, create one. + // No name and a new name is needed, create one. name = p_child->get_class(); - // Adjust casing according to project setting. - name = adjust_name_casing(name); } //quickly test if proposed name exists diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index af481749d5..64eb3b879b 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -1390,6 +1390,9 @@ SceneTree::SceneTree() { ProjectSettings::get_singleton()->set_custom_property_info("rendering/anti_aliasing/quality/msaa_3d", PropertyInfo(Variant::INT, "rendering/anti_aliasing/quality/msaa_3d", PROPERTY_HINT_ENUM, String::utf8("Disabled (Fastest),2× (Average),4× (Slow),8× (Slowest)"))); root->set_msaa_3d(Viewport::MSAA(msaa_mode_3d)); + const bool transparent_background = GLOBAL_DEF("rendering/transparent_background", false); + root->set_transparent_background(transparent_background); + const int ssaa_mode = GLOBAL_DEF_BASIC("rendering/anti_aliasing/quality/screen_space_aa", 0); ProjectSettings::get_singleton()->set_custom_property_info("rendering/anti_aliasing/quality/screen_space_aa", PropertyInfo(Variant::INT, "rendering/anti_aliasing/quality/screen_space_aa", PROPERTY_HINT_ENUM, "Disabled (Fastest),FXAA (Fast)")); root->set_screen_space_aa(Viewport::ScreenSpaceAA(ssaa_mode)); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 7fb3f32d36..9df8ed40e9 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -651,9 +651,9 @@ void Window::_update_window_size() { DisplayServer::get_singleton()->window_set_min_size(Size2i(), window_id); } - DisplayServer::get_singleton()->window_set_size(size, window_id); DisplayServer::get_singleton()->window_set_max_size(max_size_valid ? max_size : Size2i(), window_id); DisplayServer::get_singleton()->window_set_min_size(size_limit, window_id); + DisplayServer::get_singleton()->window_set_size(size, window_id); } //update the viewport diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 838927e34f..8ae217dd1f 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -2348,7 +2348,7 @@ void BaseMaterial3D::set_on_top_of_alpha() { set_flag(FLAG_DISABLE_DEPTH_TEST, true); } -void BaseMaterial3D::set_proximity_fade(bool p_enable) { +void BaseMaterial3D::set_proximity_fade_enabled(bool p_enable) { proximity_fade_enabled = p_enable; _queue_shader_change(); notify_property_list_changed(); @@ -2624,7 +2624,7 @@ void BaseMaterial3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_refraction_texture_channel", "channel"), &BaseMaterial3D::set_refraction_texture_channel); ClassDB::bind_method(D_METHOD("get_refraction_texture_channel"), &BaseMaterial3D::get_refraction_texture_channel); - ClassDB::bind_method(D_METHOD("set_proximity_fade", "enabled"), &BaseMaterial3D::set_proximity_fade); + ClassDB::bind_method(D_METHOD("set_proximity_fade_enabled", "enabled"), &BaseMaterial3D::set_proximity_fade_enabled); ClassDB::bind_method(D_METHOD("is_proximity_fade_enabled"), &BaseMaterial3D::is_proximity_fade_enabled); ClassDB::bind_method(D_METHOD("set_proximity_fade_distance", "distance"), &BaseMaterial3D::set_proximity_fade_distance); @@ -2808,7 +2808,7 @@ void BaseMaterial3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "point_size", PROPERTY_HINT_RANGE, "0.1,128,0.1,suffix:px"), "set_point_size", "get_point_size"); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "use_particle_trails"), "set_flag", "get_flag", FLAG_PARTICLE_TRAILS_MODE); ADD_GROUP("Proximity Fade", "proximity_fade_"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "proximity_fade_enable"), "set_proximity_fade", "is_proximity_fade_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "proximity_fade_enabled"), "set_proximity_fade_enabled", "is_proximity_fade_enabled"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "proximity_fade_distance", PROPERTY_HINT_RANGE, "0,4096,0.01,suffix:m"), "set_proximity_fade_distance", "get_proximity_fade_distance"); ADD_GROUP("MSDF", "msdf_"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "msdf_pixel_range", PROPERTY_HINT_RANGE, "1,100,1"), "set_msdf_pixel_range", "get_msdf_pixel_range"); diff --git a/scene/resources/material.h b/scene/resources/material.h index dd9589c577..b3c2159e70 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -719,7 +719,7 @@ public: void set_on_top_of_alpha(); - void set_proximity_fade(bool p_enable); + void set_proximity_fade_enabled(bool p_enable); bool is_proximity_fade_enabled() const; void set_proximity_fade_distance(float p_distance); diff --git a/scene/resources/texture.h b/scene/resources/texture.h index 4e529de8ee..9a9f0ad1af 100644 --- a/scene/resources/texture.h +++ b/scene/resources/texture.h @@ -238,7 +238,7 @@ private: Error _load_data(const String &p_path, int &r_width, int &r_height, Ref<Image> &image, bool &r_request_3d, bool &r_request_normal, bool &r_request_roughness, int &mipmap_limit, int p_size_limit = 0); String path_to_file; mutable RID texture; - Image::Format format = Image::FORMAT_MAX; + Image::Format format = Image::FORMAT_L8; int w = 0; int h = 0; mutable Ref<BitMap> alpha_cache; @@ -415,7 +415,7 @@ class ImageTextureLayered : public TextureLayered { LayeredType layered_type; mutable RID texture; - Image::Format format = Image::FORMAT_MAX; + Image::Format format = Image::FORMAT_L8; int width = 0; int height = 0; @@ -495,7 +495,7 @@ private: Error _load_data(const String &p_path, Vector<Ref<Image>> &images, int &mipmap_limit, int p_size_limit = 0); String path_to_file; mutable RID texture; - Image::Format format = Image::FORMAT_MAX; + Image::Format format = Image::FORMAT_L8; int w = 0; int h = 0; int layers = 0; @@ -587,7 +587,7 @@ class ImageTexture3D : public Texture3D { mutable RID texture; - Image::Format format = Image::FORMAT_MAX; + Image::Format format = Image::FORMAT_L8; int width = 1; int height = 1; int depth = 1; @@ -641,7 +641,7 @@ private: Error _load_data(const String &p_path, Vector<Ref<Image>> &r_data, Image::Format &r_format, int &r_width, int &r_height, int &r_depth, bool &r_mipmaps); String path_to_file; mutable RID texture; - Image::Format format = Image::FORMAT_MAX; + Image::Format format = Image::FORMAT_L8; int w = 0; int h = 0; int d = 0; diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index 84d2ad328c..039a2d2bf4 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -754,7 +754,7 @@ void RenderForwardClustered::_fill_instance_data(RenderListType p_render_list, i bool cant_repeat = instance_data.flags & INSTANCE_DATA_FLAG_MULTIMESH || inst->mesh_instance.is_valid(); - if (prev_surface != nullptr && !cant_repeat && prev_surface->sort.sort_key1 == surface->sort.sort_key1 && prev_surface->sort.sort_key2 == surface->sort.sort_key2 && repeats < RenderElementInfo::MAX_REPEATS) { + if (prev_surface != nullptr && !cant_repeat && prev_surface->sort.sort_key1 == surface->sort.sort_key1 && prev_surface->sort.sort_key2 == surface->sort.sort_key2 && inst->mirror == prev_surface->owner->mirror && repeats < RenderElementInfo::MAX_REPEATS) { //this element is the same as the previous one, count repeats to draw it using instancing repeats++; } else { diff --git a/servers/rendering/renderer_rd/shader_rd.cpp b/servers/rendering/renderer_rd/shader_rd.cpp index 0f2dea6fe9..5e9eadadd9 100644 --- a/servers/rendering/renderer_rd/shader_rd.cpp +++ b/servers/rendering/renderer_rd/shader_rd.cpp @@ -180,6 +180,7 @@ void ShaderRD::_build_variant_code(StringBuilder &builder, uint32_t p_variant, c #if defined(MACOS_ENABLED) || defined(IOS_ENABLED) builder.append("#define MOLTENVK_USED\n"); #endif + builder.append(String("#define RENDER_DRIVER_") + OS::get_singleton()->get_current_rendering_driver_name().to_upper() + "\n"); } break; case StageTemplate::Chunk::TYPE_MATERIAL_UNIFORMS: { builder.append(p_version->uniforms.get_data()); //uniforms (same for vertex and fragment) diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index 1fecac9045..f0b8d006cb 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -903,7 +903,9 @@ _FORCE_INLINE_ static void _fill_std140_ubo_empty(ShaderLanguage::DataType type, case ShaderLanguage::TYPE_BVEC3: case ShaderLanguage::TYPE_IVEC3: case ShaderLanguage::TYPE_UVEC3: - case ShaderLanguage::TYPE_VEC3: + case ShaderLanguage::TYPE_VEC3: { + memset(data, 0, 12 * p_array_size); + } break; case ShaderLanguage::TYPE_BVEC4: case ShaderLanguage::TYPE_IVEC4: case ShaderLanguage::TYPE_UVEC4: diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index add3e03389..d5285c07f4 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -2461,6 +2461,7 @@ void TextureStorage::_update_render_target(RenderTarget *rt) { RD::TEXTURE_SAMPLES_8, }; rd_color_multisample_format.samples = texture_samples[rt->msaa]; + rd_color_multisample_format.usage_bits = RD::TEXTURE_USAGE_COLOR_ATTACHMENT_BIT; RD::TextureView rd_view_multisample; rd_color_multisample_format.is_resolve_buffer = false; rt->color_multisample = RD::get_singleton()->texture_create(rd_color_multisample_format, rd_view_multisample); diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index 2b25e8962f..d0cb46dee9 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -869,12 +869,7 @@ void RendererSceneCull::instance_set_transform(RID p_instance, const Transform3D for (int i = 0; i < 4; i++) { const Vector3 &v = i < 3 ? p_transform.basis.rows[i] : p_transform.origin; - ERR_FAIL_COND(Math::is_inf(v.x)); - ERR_FAIL_COND(Math::is_nan(v.x)); - ERR_FAIL_COND(Math::is_inf(v.y)); - ERR_FAIL_COND(Math::is_nan(v.y)); - ERR_FAIL_COND(Math::is_inf(v.z)); - ERR_FAIL_COND(Math::is_nan(v.z)); + ERR_FAIL_COND(!v.is_finite()); } #endif diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index 29e5c9cd77..ca30a30786 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -958,7 +958,7 @@ public: bool wireframe; PolygonCullMode cull_mode; PolygonFrontFace front_face; - bool depth_bias_enable; + bool depth_bias_enabled; float depth_bias_constant_factor; float depth_bias_clamp; float depth_bias_slope_factor; @@ -970,7 +970,7 @@ public: wireframe = false; cull_mode = POLYGON_CULL_DISABLED; front_face = POLYGON_FRONT_FACE_CLOCKWISE; - depth_bias_enable = false; + depth_bias_enabled = false; depth_bias_constant_factor = 0; depth_bias_clamp = 0; depth_bias_slope_factor = 0; diff --git a/servers/rendering/rendering_device_binds.h b/servers/rendering/rendering_device_binds.h index d95b46933c..c710bd0a10 100644 --- a/servers/rendering/rendering_device_binds.h +++ b/servers/rendering/rendering_device_binds.h @@ -517,7 +517,7 @@ public: RD_SETGET(bool, wireframe) RD_SETGET(RD::PolygonCullMode, cull_mode) RD_SETGET(RD::PolygonFrontFace, front_face) - RD_SETGET(bool, depth_bias_enable) + RD_SETGET(bool, depth_bias_enabled) RD_SETGET(float, depth_bias_constant_factor) RD_SETGET(float, depth_bias_clamp) RD_SETGET(float, depth_bias_slope_factor) @@ -531,7 +531,7 @@ protected: RD_BIND(Variant::BOOL, RDPipelineRasterizationState, wireframe); RD_BIND(Variant::INT, RDPipelineRasterizationState, cull_mode); RD_BIND(Variant::INT, RDPipelineRasterizationState, front_face); - RD_BIND(Variant::BOOL, RDPipelineRasterizationState, depth_bias_enable); + RD_BIND(Variant::BOOL, RDPipelineRasterizationState, depth_bias_enabled); RD_BIND(Variant::FLOAT, RDPipelineRasterizationState, depth_bias_constant_factor); RD_BIND(Variant::FLOAT, RDPipelineRasterizationState, depth_bias_clamp); RD_BIND(Variant::FLOAT, RDPipelineRasterizationState, depth_bias_slope_factor); diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index 627cd9f062..e12c4fc79a 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -2868,12 +2868,13 @@ void RenderingServer::init() { GLOBAL_DEF("rendering/2d/shadow_atlas/size", 2048); - // Already defined in RenderingDeviceVulkan::initialize which runs before this code. + // Already defined in some RenderingDevice*::initialize, which run before this code. // We re-define them here just for doctool's sake. Make sure to keep default values in sync. GLOBAL_DEF("rendering/rendering_device/staging_buffer/block_size_kb", 256); GLOBAL_DEF("rendering/rendering_device/staging_buffer/max_size_mb", 128); GLOBAL_DEF("rendering/rendering_device/staging_buffer/texture_upload_region_size_px", 64); - GLOBAL_DEF("rendering/rendering_device/descriptor_pools/max_descriptors_per_pool", 64); + // Vulkan-specific. + GLOBAL_DEF("rendering/rendering_device/vulkan/max_descriptors_per_pool", 64); // Number of commands that can be drawn per frame. GLOBAL_DEF_RST("rendering/gl_compatibility/item_buffer_size", 16384); diff --git a/tests/core/math/test_aabb.h b/tests/core/math/test_aabb.h index d5f54a139e..ebaf441abf 100644 --- a/tests/core/math/test_aabb.h +++ b/tests/core/math/test_aabb.h @@ -389,6 +389,27 @@ TEST_CASE("[AABB] Expanding") { aabb.expand(Vector3(-20, 0, 0)).is_equal_approx(AABB(Vector3(-20, 0, -2.5), Vector3(22.5, 7, 6))), "expand() with non-contained point should return the expected AABB."); } + +TEST_CASE("[AABB] Finite number checks") { + const Vector3 x(0, 1, 2); + const Vector3 infinite(NAN, NAN, NAN); + + CHECK_MESSAGE( + AABB(x, x).is_finite(), + "AABB with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + AABB(infinite, x).is_finite(), + "AABB with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + AABB(x, infinite).is_finite(), + "AABB with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + AABB(infinite, infinite).is_finite(), + "AABB with two components infinite should not be finite."); +} + } // namespace TestAABB #endif // TEST_AABB_H diff --git a/tests/core/math/test_basis.h b/tests/core/math/test_basis.h index b6493c5726..a65020597a 100644 --- a/tests/core/math/test_basis.h +++ b/tests/core/math/test_basis.h @@ -334,6 +334,40 @@ TEST_CASE("[Basis] Set axis angle") { bugNan.get_axis_angle(axis, angle); CHECK(!Math::is_nan(angle)); } + +TEST_CASE("[Basis] Finite number checks") { + const Vector3 x(0, 1, 2); + const Vector3 infinite(NAN, NAN, NAN); + + CHECK_MESSAGE( + Basis(x, x, x).is_finite(), + "Basis with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Basis(infinite, x, x).is_finite(), + "Basis with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Basis(x, infinite, x).is_finite(), + "Basis with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Basis(x, x, infinite).is_finite(), + "Basis with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Basis(infinite, infinite, x).is_finite(), + "Basis with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Basis(infinite, x, infinite).is_finite(), + "Basis with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Basis(x, infinite, infinite).is_finite(), + "Basis with two components infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Basis(infinite, infinite, infinite).is_finite(), + "Basis with three components infinite should not be finite."); +} + } // namespace TestBasis #endif // TEST_BASIS_H diff --git a/tests/core/math/test_plane.h b/tests/core/math/test_plane.h index d81a5af1ce..84d9a0ff7d 100644 --- a/tests/core/math/test_plane.h +++ b/tests/core/math/test_plane.h @@ -167,6 +167,29 @@ TEST_CASE("[Plane] Intersection") { vec_out.is_equal_approx(Vector3(1, 1, 1)), "intersects_segment() should modify vec_out to the expected result."); } + +TEST_CASE("[Plane] Finite number checks") { + const Vector3 x(0, 1, 2); + const Vector3 infinite_vec(NAN, NAN, NAN); + const real_t y = 0; + const real_t infinite_y = NAN; + + CHECK_MESSAGE( + Plane(x, y).is_finite(), + "Plane with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Plane(x, infinite_y).is_finite(), + "Plane with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Plane(infinite_vec, y).is_finite(), + "Plane with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Plane(infinite_vec, infinite_y).is_finite(), + "Plane with two components infinite should not be finite."); +} + } // namespace TestPlane #endif // TEST_PLANE_H diff --git a/tests/core/math/test_quaternion.h b/tests/core/math/test_quaternion.h index 63d30759bb..d1912cbf42 100644 --- a/tests/core/math/test_quaternion.h +++ b/tests/core/math/test_quaternion.h @@ -384,6 +384,63 @@ TEST_CASE("[Stress][Quaternion] Many vector xforms") { } } +TEST_CASE("[Quaternion] Finite number checks") { + const real_t x = NAN; + + CHECK_MESSAGE( + Quaternion(0, 1, 2, 3).is_finite(), + "Quaternion with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Quaternion(x, 1, 2, 3).is_finite(), + "Quaternion with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, x, 2, 3).is_finite(), + "Quaternion with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, 1, x, 3).is_finite(), + "Quaternion with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, 1, 2, x).is_finite(), + "Quaternion with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Quaternion(x, x, 2, 3).is_finite(), + "Quaternion with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(x, 1, x, 3).is_finite(), + "Quaternion with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(x, 1, 2, x).is_finite(), + "Quaternion with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, x, x, 3).is_finite(), + "Quaternion with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, x, 2, x).is_finite(), + "Quaternion with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(0, 1, x, x).is_finite(), + "Quaternion with two components infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Quaternion(0, x, x, x).is_finite(), + "Quaternion with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(x, 1, x, x).is_finite(), + "Quaternion with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(x, x, 2, x).is_finite(), + "Quaternion with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Quaternion(x, x, x, 3).is_finite(), + "Quaternion with three components infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Quaternion(x, x, x, x).is_finite(), + "Quaternion with four components infinite should not be finite."); +} + } // namespace TestQuaternion #endif // TEST_QUATERNION_H diff --git a/tests/core/math/test_rect2.h b/tests/core/math/test_rect2.h index 6323b214db..d784875c1c 100644 --- a/tests/core/math/test_rect2.h +++ b/tests/core/math/test_rect2.h @@ -300,6 +300,27 @@ TEST_CASE("[Rect2] Merging") { Rect2(0, 100, 1280, 720).merge(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2(-4000, -4000, 5280, 4820)), "merge() with non-enclosed Rect2 should return the expected result."); } + +TEST_CASE("[Rect2] Finite number checks") { + const Vector2 x(0, 1); + const Vector2 infinite(NAN, NAN); + + CHECK_MESSAGE( + Rect2(x, x).is_finite(), + "Rect2 with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Rect2(infinite, x).is_finite(), + "Rect2 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Rect2(x, infinite).is_finite(), + "Rect2 with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Rect2(infinite, infinite).is_finite(), + "Rect2 with two components infinite should not be finite."); +} + } // namespace TestRect2 #endif // TEST_RECT2_H diff --git a/tests/core/math/test_transform_2d.h b/tests/core/math/test_transform_2d.h index 697bf63fc5..ab51bcd7da 100644 --- a/tests/core/math/test_transform_2d.h +++ b/tests/core/math/test_transform_2d.h @@ -83,6 +83,40 @@ TEST_CASE("[Transform2D] rotation") { CHECK(orig.rotated(phi) == R * orig); CHECK(orig.rotated_local(phi) == orig * R); } + +TEST_CASE("[Transform2D] Finite number checks") { + const Vector2 x(0, 1); + const Vector2 infinite(NAN, NAN); + + CHECK_MESSAGE( + Transform2D(x, x, x).is_finite(), + "Transform2D with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Transform2D(infinite, x, x).is_finite(), + "Transform2D with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Transform2D(x, infinite, x).is_finite(), + "Transform2D with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Transform2D(x, x, infinite).is_finite(), + "Transform2D with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Transform2D(infinite, infinite, x).is_finite(), + "Transform2D with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Transform2D(infinite, x, infinite).is_finite(), + "Transform2D with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Transform2D(x, infinite, infinite).is_finite(), + "Transform2D with two components infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Transform2D(infinite, infinite, infinite).is_finite(), + "Transform2D with three components infinite should not be finite."); +} + } // namespace TestTransform2D #endif // TEST_TRANSFORM_2D_H diff --git a/tests/core/math/test_transform_3d.h b/tests/core/math/test_transform_3d.h index da166b43f7..d2730f3577 100644 --- a/tests/core/math/test_transform_3d.h +++ b/tests/core/math/test_transform_3d.h @@ -84,6 +84,29 @@ TEST_CASE("[Transform3D] rotation") { CHECK(orig.rotated(axis, phi) == R * orig); CHECK(orig.rotated_local(axis, phi) == orig * R); } + +TEST_CASE("[Transform3D] Finite number checks") { + const Vector3 y(0, 1, 2); + const Vector3 infinite_vec(NAN, NAN, NAN); + const Basis x(y, y, y); + const Basis infinite_basis(infinite_vec, infinite_vec, infinite_vec); + + CHECK_MESSAGE( + Transform3D(x, y).is_finite(), + "Transform3D with all components finite should be finite"); + + CHECK_FALSE_MESSAGE( + Transform3D(x, infinite_vec).is_finite(), + "Transform3D with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Transform3D(infinite_basis, y).is_finite(), + "Transform3D with one component infinite should not be finite."); + + CHECK_FALSE_MESSAGE( + Transform3D(infinite_basis, infinite_vec).is_finite(), + "Transform3D with two components infinite should not be finite."); +} + } // namespace TestTransform3D #endif // TEST_TRANSFORM_3D_H diff --git a/tests/core/math/test_vector2.h b/tests/core/math/test_vector2.h index 0d7f1163e4..a87b9ffc02 100644 --- a/tests/core/math/test_vector2.h +++ b/tests/core/math/test_vector2.h @@ -465,6 +465,32 @@ TEST_CASE("[Vector2] Linear algebra methods") { Math::is_equal_approx(Vector2(-a.x, a.y).dot(Vector2(b.x, -b.y)), (real_t)-57.3), "Vector2 dot should return expected value."); } + +TEST_CASE("[Vector2] Finite number checks") { + const double infinite[] = { NAN, INFINITY, -INFINITY }; + + CHECK_MESSAGE( + Vector2(0, 1).is_finite(), + "Vector2(0, 1) should be finite"); + + for (double x : infinite) { + CHECK_FALSE_MESSAGE( + Vector2(x, 1).is_finite(), + "Vector2 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector2(0, x).is_finite(), + "Vector2 with one component infinite should not be finite."); + } + + for (double x : infinite) { + for (double y : infinite) { + CHECK_FALSE_MESSAGE( + Vector2(x, y).is_finite(), + "Vector2 with two components infinite should not be finite."); + } + } +} + } // namespace TestVector2 #endif // TEST_VECTOR2_H diff --git a/tests/core/math/test_vector3.h b/tests/core/math/test_vector3.h index be271bad1f..4932cd04db 100644 --- a/tests/core/math/test_vector3.h +++ b/tests/core/math/test_vector3.h @@ -479,6 +479,51 @@ TEST_CASE("[Vector3] Linear algebra methods") { Math::is_equal_approx(Vector3(-a.x, a.y, -a.z).dot(Vector3(b.x, -b.y, b.z)), (real_t)-75.24), "Vector3 dot should return expected value."); } + +TEST_CASE("[Vector3] Finite number checks") { + const double infinite[] = { NAN, INFINITY, -INFINITY }; + + CHECK_MESSAGE( + Vector3(0, 1, 2).is_finite(), + "Vector3(0, 1, 2) should be finite"); + + for (double x : infinite) { + CHECK_FALSE_MESSAGE( + Vector3(x, 1, 2).is_finite(), + "Vector3 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector3(0, x, 2).is_finite(), + "Vector3 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector3(0, 1, x).is_finite(), + "Vector3 with one component infinite should not be finite."); + } + + for (double x : infinite) { + for (double y : infinite) { + CHECK_FALSE_MESSAGE( + Vector3(x, y, 2).is_finite(), + "Vector3 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector3(x, 1, y).is_finite(), + "Vector3 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector3(0, x, y).is_finite(), + "Vector3 with two components infinite should not be finite."); + } + } + + for (double x : infinite) { + for (double y : infinite) { + for (double z : infinite) { + CHECK_FALSE_MESSAGE( + Vector3(x, y, z).is_finite(), + "Vector3 with three components infinite should not be finite."); + } + } + } +} + } // namespace TestVector3 #endif // TEST_VECTOR3_H diff --git a/tests/core/math/test_vector4.h b/tests/core/math/test_vector4.h index 3f50f16635..b31db56f67 100644 --- a/tests/core/math/test_vector4.h +++ b/tests/core/math/test_vector4.h @@ -314,6 +314,84 @@ TEST_CASE("[Vector4] Linear algebra methods") { Math::is_equal_approx((vector1 * 2).dot(vector2 * 4), (real_t)-25.9 * 8), "Vector4 dot product should work as expected."); } + +TEST_CASE("[Vector4] Finite number checks") { + const double infinite[] = { NAN, INFINITY, -INFINITY }; + + CHECK_MESSAGE( + Vector4(0, 1, 2, 3).is_finite(), + "Vector4(0, 1, 2, 3) should be finite"); + + for (double x : infinite) { + CHECK_FALSE_MESSAGE( + Vector4(x, 1, 2, 3).is_finite(), + "Vector4 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, x, 2, 3).is_finite(), + "Vector4 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, 1, x, 3).is_finite(), + "Vector4 with one component infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, 1, 2, x).is_finite(), + "Vector4 with one component infinite should not be finite."); + } + + for (double x : infinite) { + for (double y : infinite) { + CHECK_FALSE_MESSAGE( + Vector4(x, y, 2, 3).is_finite(), + "Vector4 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(x, 1, y, 3).is_finite(), + "Vector4 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(x, 1, 2, y).is_finite(), + "Vector4 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, x, y, 3).is_finite(), + "Vector4 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, x, 2, y).is_finite(), + "Vector4 with two components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(0, 1, x, y).is_finite(), + "Vector4 with two components infinite should not be finite."); + } + } + + for (double x : infinite) { + for (double y : infinite) { + for (double z : infinite) { + CHECK_FALSE_MESSAGE( + Vector4(0, x, y, z).is_finite(), + "Vector4 with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(x, 1, y, z).is_finite(), + "Vector4 with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(x, y, 2, z).is_finite(), + "Vector4 with three components infinite should not be finite."); + CHECK_FALSE_MESSAGE( + Vector4(x, y, z, 3).is_finite(), + "Vector4 with three components infinite should not be finite."); + } + } + } + + for (double x : infinite) { + for (double y : infinite) { + for (double z : infinite) { + for (double w : infinite) { + CHECK_FALSE_MESSAGE( + Vector4(x, y, z, w).is_finite(), + "Vector4 with four components infinite should not be finite."); + } + } + } + } +} + } // namespace TestVector4 #endif // TEST_VECTOR4_H diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 9514178c61..3a20ac123b 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -637,17 +637,42 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] select word under caret") { - text_edit->set_text("test test"); + text_edit->set_text("\ntest test\ntest test"); + text_edit->set_caret_column(0); + text_edit->set_caret_line(1); + + text_edit->add_caret(2, 0); + text_edit->add_caret(2, 2); + CHECK(text_edit->get_caret_count() == 3); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + text_edit->select_word_under_caret(); - CHECK(text_edit->get_selected_text() == "test"); - CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selection_from_line() == 0); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 0); - CHECK(text_edit->get_selection_to_column() == 4); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_selection_from_line(0) == 1); + CHECK(text_edit->get_selection_from_column(0) == 0); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 2); + CHECK(text_edit->get_selection_from_column(1) == 0); + CHECK(text_edit->get_selection_to_line(1) == 2); + CHECK(text_edit->get_selection_to_column(1) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + + CHECK(text_edit->get_caret_count() == 2); text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); @@ -656,27 +681,44 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_select_word_under_caret"); CHECK(text_edit->get_viewport()->is_input_handled()); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "test"); - CHECK(text_edit->get_selection_from_line() == 0); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 0); - CHECK(text_edit->get_selection_to_column() == 4); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_selection_from_line(0) == 1); + CHECK(text_edit->get_selection_from_column(0) == 0); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 2); + CHECK(text_edit->get_selection_from_column(1) == 0); + CHECK(text_edit->get_selection_to_line(1) == 2); + CHECK(text_edit->get_selection_to_column(1) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + + CHECK(text_edit->get_selected_text() == "test\ntest"); SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->set_selecting_enabled(false); text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); SIGNAL_CHECK_FALSE("caret_changed"); text_edit->set_selecting_enabled(true); - text_edit->set_caret_line(0); - text_edit->set_caret_column(5); + text_edit->set_caret_line(1, false, true, 0, 0); + text_edit->set_caret_column(5, false, 0); + + text_edit->set_caret_line(2, false, true, 0, 1); + text_edit->set_caret_column(5, false, 1); + text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); @@ -684,8 +726,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); SIGNAL_CHECK_FALSE("caret_changed"); } |