diff options
177 files changed, 4566 insertions, 2614 deletions
diff --git a/.github/workflows/macos_builds.yml b/.github/workflows/macos_builds.yml index aede3f8d49..68623f2770 100644 --- a/.github/workflows/macos_builds.yml +++ b/.github/workflows/macos_builds.yml @@ -4,7 +4,7 @@ on: [push, pull_request] # Global Settings env: # Only used for the cache key. Increment version to force clean build. - GODOT_BASE_BRANCH: master-v2 + GODOT_BASE_BRANCH: master-v3 SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes concurrency: diff --git a/core/io/file_access_network.cpp b/core/io/file_access_network.cpp index 307004b1c2..cb38ac0928 100644 --- a/core/io/file_access_network.cpp +++ b/core/io/file_access_network.cpp @@ -487,7 +487,6 @@ FileAccessNetwork::~FileAccessNetwork() { FileAccessNetworkClient *nc = FileAccessNetworkClient::singleton; nc->lock_mutex(); - id = nc->last_id++; nc->accesses.erase(id); nc->unlock_mutex(); } diff --git a/core/io/http_client_tcp.cpp b/core/io/http_client_tcp.cpp index e61833ce7c..f920799677 100644 --- a/core/io/http_client_tcp.cpp +++ b/core/io/http_client_tcp.cpp @@ -614,7 +614,7 @@ PackedByteArray HTTPClientTCP::read_response_body_chunk() { for (int i = 0; i < chunk.size() - 2; i++) { char c = chunk[i]; int v = 0; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a' + 10; diff --git a/core/io/image.cpp b/core/io/image.cpp index 9df2b6835c..4f72599faf 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -2056,7 +2056,7 @@ void Image::create(const char **p_xpm) { for (int i = 0; i < 6; i++) { char v = line_ptr[i]; - if (v >= '0' && v <= '9') { + if (is_digit(v)) { v -= '0'; } else if (v >= 'A' && v <= 'F') { v = (v - 'A') + 10; diff --git a/core/io/ip_address.cpp b/core/io/ip_address.cpp index 38f99a08a4..d183c60798 100644 --- a/core/io/ip_address.cpp +++ b/core/io/ip_address.cpp @@ -71,7 +71,7 @@ static void _parse_hex(const String &p_string, int p_start, uint8_t *p_dst) { int n = 0; char32_t c = p_string[i]; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { n = c - '0'; } else if (c >= 'a' && c <= 'f') { n = 10 + (c - 'a'); @@ -113,7 +113,7 @@ void IPAddress::_parse_ipv6(const String &p_string) { } else if (c == '.') { part_ipv4 = true; - } else if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + } else if (is_hex_digit(c)) { if (!part_found) { parts[parts_idx++] = i; part_found = true; diff --git a/core/io/json.cpp b/core/io/json.cpp index 7b642f6a59..4b745dff44 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -229,12 +229,12 @@ Error JSON::_get_token(const char32_t *p_str, int &index, int p_len, Token &r_to r_err_str = "Unterminated String"; return ERR_PARSE_ERROR; } - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + if (!is_hex_digit(c)) { r_err_str = "Malformed hex constant in string"; return ERR_PARSE_ERROR; } char32_t v; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a'; @@ -265,12 +265,12 @@ Error JSON::_get_token(const char32_t *p_str, int &index, int p_len, Token &r_to r_err_str = "Unterminated String"; return ERR_PARSE_ERROR; } - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + if (!is_hex_digit(c)) { r_err_str = "Malformed hex constant in string"; return ERR_PARSE_ERROR; } char32_t v; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a'; @@ -326,7 +326,7 @@ Error JSON::_get_token(const char32_t *p_str, int &index, int p_len, Token &r_to break; } - if (p_str[index] == '-' || (p_str[index] >= '0' && p_str[index] <= '9')) { + if (p_str[index] == '-' || is_digit(p_str[index])) { //a number const char32_t *rptr; double number = String::to_float(&p_str[index], &rptr); @@ -335,10 +335,10 @@ Error JSON::_get_token(const char32_t *p_str, int &index, int p_len, Token &r_to r_token.value = number; return OK; - } else if ((p_str[index] >= 'A' && p_str[index] <= 'Z') || (p_str[index] >= 'a' && p_str[index] <= 'z')) { + } else if (is_ascii_char(p_str[index])) { String id; - while ((p_str[index] >= 'A' && p_str[index] <= 'Z') || (p_str[index] >= 'a' && p_str[index] <= 'z')) { + while (is_ascii_char(p_str[index])) { id += p_str[index]; index++; } diff --git a/core/io/pck_packer.cpp b/core/io/pck_packer.cpp index 221a680130..272ace3438 100644 --- a/core/io/pck_packer.cpp +++ b/core/io/pck_packer.cpp @@ -62,7 +62,7 @@ Error PCKPacker::pck_start(const String &p_file, int p_alignment, const String & int v = 0; if (i * 2 < _key.length()) { char32_t ct = _key[i * 2]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; @@ -72,7 +72,7 @@ Error PCKPacker::pck_start(const String &p_file, int p_alignment, const String & if (i * 2 + 1 < _key.length()) { char32_t ct = _key[i * 2 + 1]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; diff --git a/core/io/resource_uid.cpp b/core/io/resource_uid.cpp index 1a16d5b47a..776756e64e 100644 --- a/core/io/resource_uid.cpp +++ b/core/io/resource_uid.cpp @@ -71,9 +71,9 @@ ResourceUID::ID ResourceUID::text_to_id(const String &p_text) const { for (uint32_t i = 6; i < l; i++) { uid *= base; uint32_t c = p_text[i]; - if (c >= 'a' && c <= 'z') { + if (is_ascii_lower_case(c)) { uid += c - 'a'; - } else if (c >= '0' && c <= '9') { + } else if (is_digit(c)) { uid += c - '0' + char_count; } else { return INVALID_ID; diff --git a/core/math/aabb.h b/core/math/aabb.h index 3d19410ddf..cb6f05e9ea 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -36,13 +36,13 @@ #include "core/math/vector3.h" /** - * AABB / AABB (Axis Aligned Bounding Box) - * This is implemented by a point (position) and the box size + * AABB (Axis Aligned Bounding Box) + * This is implemented by a point (position) and the box size. */ + class Variant; -class _NO_DISCARD_ AABB { -public: +struct _NO_DISCARD_ AABB { Vector3 position; Vector3 size; diff --git a/core/math/basis.h b/core/math/basis.h index 802da82089..683f05150c 100644 --- a/core/math/basis.h +++ b/core/math/basis.h @@ -34,11 +34,7 @@ #include "core/math/quaternion.h" #include "core/math/vector3.h" -class _NO_DISCARD_ Basis { -private: - void _set_diagonal(const Vector3 &p_diag); - -public: +struct _NO_DISCARD_ Basis { Vector3 elements[3] = { Vector3(1, 0, 0), Vector3(0, 1, 0), @@ -263,6 +259,10 @@ public: } _FORCE_INLINE_ Basis() {} + +private: + // Helper method. + void _set_diagonal(const Vector3 &p_diag); }; _FORCE_INLINE_ void Basis::operator*=(const Basis &p_matrix) { @@ -334,4 +334,5 @@ real_t Basis::determinant() const { elements[1][0] * (elements[0][1] * elements[2][2] - elements[2][1] * elements[0][2]) + elements[2][0] * (elements[0][1] * elements[1][2] - elements[1][1] * elements[0][2]); } + #endif // BASIS_H diff --git a/core/math/camera_matrix.cpp b/core/math/camera_matrix.cpp index 2902ca59b9..f5d746ef0f 100644 --- a/core/math/camera_matrix.cpp +++ b/core/math/camera_matrix.cpp @@ -30,7 +30,11 @@ #include "camera_matrix.h" +#include "core/math/aabb.h" #include "core/math/math_funcs.h" +#include "core/math/plane.h" +#include "core/math/rect2.h" +#include "core/math/transform_3d.h" #include "core/string/print_string.h" float CameraMatrix::determinant() const { diff --git a/core/math/camera_matrix.h b/core/math/camera_matrix.h index da1aba7562..285d2ae384 100644 --- a/core/math/camera_matrix.h +++ b/core/math/camera_matrix.h @@ -31,8 +31,14 @@ #ifndef CAMERA_MATRIX_H #define CAMERA_MATRIX_H -#include "core/math/rect2.h" -#include "core/math/transform_3d.h" +#include "core/math/math_defs.h" +#include "core/math/vector3.h" + +struct AABB; +struct Plane; +struct Rect2; +struct Transform3D; +struct Vector2; struct CameraMatrix { enum Planes { diff --git a/core/math/delaunay_2d.h b/core/math/delaunay_2d.h index 08f5df8472..c39997d6a9 100644 --- a/core/math/delaunay_2d.h +++ b/core/math/delaunay_2d.h @@ -32,6 +32,7 @@ #define DELAUNAY_2D_H #include "core/math/rect2.h" +#include "core/templates/vector.h" class Delaunay2D { public: diff --git a/core/math/dynamic_bvh.h b/core/math/dynamic_bvh.h index 3041cdf268..50ec2c2b30 100644 --- a/core/math/dynamic_bvh.h +++ b/core/math/dynamic_bvh.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef DYNAMICBVH_H -#define DYNAMICBVH_H +#ifndef DYNAMIC_BVH_H +#define DYNAMIC_BVH_H #include "core/math/aabb.h" #include "core/templates/list.h" @@ -474,4 +474,4 @@ void DynamicBVH::ray_query(const Vector3 &p_from, const Vector3 &p_to, QueryResu } while (depth > 0); } -#endif // DYNAMICBVH_H +#endif // DYNAMIC_BVH_H diff --git a/core/math/expression.cpp b/core/math/expression.cpp index 87f8a95970..0ddac9744e 100644 --- a/core/math/expression.cpp +++ b/core/math/expression.cpp @@ -37,18 +37,6 @@ #include "core/os/os.h" #include "core/variant/variant_parser.h" -static bool _is_number(char32_t c) { - return (c >= '0' && c <= '9'); -} - -static bool _is_hex_digit(char32_t c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} - -static bool _is_binary_digit(char32_t c) { - return (c == '0' || c == '1'); -} - Error Expression::_get_token(Token &r_token) { while (true) { #define GET_CHAR() (str_ofs >= expression.length() ? 0 : expression[str_ofs++]) @@ -96,7 +84,7 @@ Error Expression::_get_token(Token &r_token) { r_token.type = TK_INPUT; int index = 0; do { - if (!_is_number(expression[str_ofs])) { + if (!is_digit(expression[str_ofs])) { _set_error("Expected number after '$'"); r_token.type = TK_ERROR; return ERR_PARSE_ERROR; @@ -105,7 +93,7 @@ Error Expression::_get_token(Token &r_token) { index += expression[str_ofs] - '0'; str_ofs++; - } while (_is_number(expression[str_ofs])); + } while (is_digit(expression[str_ofs])); r_token.value = index; return OK; @@ -255,13 +243,13 @@ Error Expression::_get_token(Token &r_token) { r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } - if (!(_is_number(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + if (!is_hex_digit(c)) { _set_error("Malformed hex constant in string"); r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } char32_t v; - if (_is_number(c)) { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a'; @@ -336,7 +324,7 @@ Error Expression::_get_token(Token &r_token) { } char32_t next_char = (str_ofs >= expression.length()) ? 0 : expression[str_ofs]; - if (_is_number(cchar) || (cchar == '.' && _is_number(next_char))) { + if (is_digit(cchar) || (cchar == '.' && is_digit(next_char))) { //a number String num; @@ -360,7 +348,7 @@ Error Expression::_get_token(Token &r_token) { while (true) { switch (reading) { case READING_INT: { - if (_is_number(c)) { + if (is_digit(c)) { if (is_first_char && c == '0') { if (next_char == 'b') { reading = READING_BIN; @@ -380,7 +368,7 @@ Error Expression::_get_token(Token &r_token) { } break; case READING_BIN: { - if (bin_beg && !_is_binary_digit(c)) { + if (bin_beg && !is_binary_digit(c)) { reading = READING_DONE; } else if (c == 'b') { bin_beg = true; @@ -388,7 +376,7 @@ Error Expression::_get_token(Token &r_token) { } break; case READING_HEX: { - if (hex_beg && !_is_hex_digit(c)) { + if (hex_beg && !is_hex_digit(c)) { reading = READING_DONE; } else if (c == 'x') { hex_beg = true; @@ -396,7 +384,7 @@ Error Expression::_get_token(Token &r_token) { } break; case READING_DEC: { - if (_is_number(c)) { + if (is_digit(c)) { } else if (c == 'e') { reading = READING_EXP; @@ -406,7 +394,7 @@ Error Expression::_get_token(Token &r_token) { } break; case READING_EXP: { - if (_is_number(c)) { + if (is_digit(c)) { exp_beg = true; } else if ((c == '-' || c == '+') && !exp_sign && !exp_beg) { @@ -441,11 +429,11 @@ Error Expression::_get_token(Token &r_token) { } return OK; - } else if ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_') { + } else if (is_ascii_char(cchar) || is_underscore(cchar)) { String id; bool first = true; - while ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_' || (!first && _is_number(cchar))) { + while (is_ascii_char(cchar) || is_underscore(cchar) || (!first && is_digit(cchar))) { id += String::chr(cchar); cchar = GET_CHAR(); first = false; diff --git a/core/math/face3.h b/core/math/face3.h index 3dbbca09e0..8b123f078c 100644 --- a/core/math/face3.h +++ b/core/math/face3.h @@ -36,8 +36,7 @@ #include "core/math/transform_3d.h" #include "core/math/vector3.h" -class _NO_DISCARD_ Face3 { -public: +struct _NO_DISCARD_ Face3 { enum Side { SIDE_OVER, SIDE_UNDER, @@ -48,14 +47,11 @@ public: Vector3 vertex[3]; /** - * * @param p_plane plane used to split the face * @param p_res array of at least 3 faces, amount used in function return * @param p_is_point_over array of at least 3 booleans, determining which face is over the plane, amount used in function return - * @param _epsilon constant used for numerical error rounding, to add "thickness" to the plane (so coplanar points can happen) * @return amount of faces generated by the split, either 0 (means no split possible), 2 or 3 */ - int split_by_plane(const Plane &p_plane, Face3 *p_res, bool *p_is_point_over) const; Plane get_plane(ClockDirection p_dir = CLOCKWISE) const; diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h index 7385dba438..a2881d5f60 100644 --- a/core/math/geometry_2d.h +++ b/core/math/geometry_2d.h @@ -32,7 +32,11 @@ #define GEOMETRY_2D_H #include "core/math/delaunay_2d.h" +#include "core/math/math_funcs.h" #include "core/math/triangulate.h" +#include "core/math/vector2.h" +#include "core/math/vector2i.h" +#include "core/math/vector3.h" #include "core/math/vector3i.h" #include "core/templates/vector.h" diff --git a/core/math/plane.h b/core/math/plane.h index 8cb6f62b3b..66c1741662 100644 --- a/core/math/plane.h +++ b/core/math/plane.h @@ -35,13 +35,12 @@ class Variant; -class _NO_DISCARD_ Plane { -public: +struct _NO_DISCARD_ Plane { Vector3 normal; real_t d = 0; void set_normal(const Vector3 &p_normal); - _FORCE_INLINE_ Vector3 get_normal() const { return normal; }; ///Point is coplanar, CMP_EPSILON for precision + _FORCE_INLINE_ Vector3 get_normal() const { return normal; }; void normalize(); Plane normalized() const; diff --git a/core/math/quaternion.h b/core/math/quaternion.h index 2575d7d229..7874e4f428 100644 --- a/core/math/quaternion.h +++ b/core/math/quaternion.h @@ -36,8 +36,7 @@ #include "core/math/vector3.h" #include "core/string/ustring.h" -class _NO_DISCARD_ Quaternion { -public: +struct _NO_DISCARD_ Quaternion { union { struct { real_t x; diff --git a/core/math/rect2.cpp b/core/math/rect2.cpp index 9047c19434..d6e20bdc3c 100644 --- a/core/math/rect2.cpp +++ b/core/math/rect2.cpp @@ -28,7 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "core/math/transform_2d.h" // Includes rect2.h but Rect2 needs Transform2D +#include "rect2.h" + +#include "core/math/rect2i.h" +#include "core/math/transform_2d.h" +#include "core/string/ustring.h" 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); @@ -278,6 +282,6 @@ Rect2::operator String() const { return "[P: " + position.operator String() + ", S: " + size + "]"; } -Rect2i::operator String() const { - return "[P: " + position.operator String() + ", S: " + size + "]"; +Rect2::operator Rect2i() const { + return Rect2i(position, size); } diff --git a/core/math/rect2.h b/core/math/rect2.h index b14c69302c..6ecc02336c 100644 --- a/core/math/rect2.h +++ b/core/math/rect2.h @@ -31,8 +31,11 @@ #ifndef RECT2_H #define RECT2_H -#include "core/math/vector2.h" // also includes math_funcs and ustring +#include "core/error/error_macros.h" +#include "core/math/vector2.h" +class String; +struct Rect2i; struct Transform2D; struct _NO_DISCARD_ Rect2 { @@ -179,6 +182,7 @@ struct _NO_DISCARD_ Rect2 { return new_rect; } + inline bool has_point(const Point2 &p_point) const { #ifdef MATH_CHECKS if (unlikely(size.x < 0 || size.y < 0)) { @@ -201,6 +205,7 @@ struct _NO_DISCARD_ Rect2 { return true; } + bool is_equal_approx(const Rect2 &p_rect) const; bool operator==(const Rect2 &p_rect) const { return position == p_rect.position && size == p_rect.size; } @@ -351,6 +356,7 @@ struct _NO_DISCARD_ Rect2 { } operator String() const; + operator Rect2i() const; Rect2() {} Rect2(real_t p_x, real_t p_y, real_t p_width, real_t p_height) : @@ -363,214 +369,4 @@ struct _NO_DISCARD_ Rect2 { } }; -struct _NO_DISCARD_ Rect2i { - Point2i position; - Size2i size; - - const Point2i &get_position() const { return position; } - void set_position(const Point2i &p_position) { position = p_position; } - const Size2i &get_size() const { return size; } - void set_size(const Size2i &p_size) { size = p_size; } - - int get_area() const { return size.width * size.height; } - - _FORCE_INLINE_ Vector2i get_center() const { return position + (size / 2); } - - inline bool intersects(const Rect2i &p_rect) const { -#ifdef MATH_CHECKS - if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { - ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); - } -#endif - if (position.x >= (p_rect.position.x + p_rect.size.width)) { - return false; - } - if ((position.x + size.width) <= p_rect.position.x) { - return false; - } - if (position.y >= (p_rect.position.y + p_rect.size.height)) { - return false; - } - if ((position.y + size.height) <= p_rect.position.y) { - return false; - } - - return true; - } - - inline bool encloses(const Rect2i &p_rect) const { -#ifdef MATH_CHECKS - if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { - ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); - } -#endif - return (p_rect.position.x >= position.x) && (p_rect.position.y >= position.y) && - ((p_rect.position.x + p_rect.size.x) <= (position.x + size.x)) && - ((p_rect.position.y + p_rect.size.y) <= (position.y + size.y)); - } - - _FORCE_INLINE_ bool has_no_area() const { - return (size.x <= 0 || size.y <= 0); - } - - // Returns the instersection between two Rect2is or an empty Rect2i if there is no intersection - inline Rect2i intersection(const Rect2i &p_rect) const { - Rect2i new_rect = p_rect; - - if (!intersects(new_rect)) { - return Rect2i(); - } - - new_rect.position.x = MAX(p_rect.position.x, position.x); - new_rect.position.y = MAX(p_rect.position.y, position.y); - - Point2i p_rect_end = p_rect.position + p_rect.size; - Point2i end = position + size; - - new_rect.size.x = MIN(p_rect_end.x, end.x) - new_rect.position.x; - new_rect.size.y = MIN(p_rect_end.y, end.y) - new_rect.position.y; - - return new_rect; - } - - inline Rect2i merge(const Rect2i &p_rect) const { ///< return a merged rect -#ifdef MATH_CHECKS - if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { - ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); - } -#endif - Rect2i new_rect; - - new_rect.position.x = MIN(p_rect.position.x, position.x); - new_rect.position.y = MIN(p_rect.position.y, position.y); - - new_rect.size.x = MAX(p_rect.position.x + p_rect.size.x, position.x + size.x); - new_rect.size.y = MAX(p_rect.position.y + p_rect.size.y, position.y + size.y); - - new_rect.size = new_rect.size - new_rect.position; //make relative again - - return new_rect; - } - bool has_point(const Point2i &p_point) const { -#ifdef MATH_CHECKS - if (unlikely(size.x < 0 || size.y < 0)) { - ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); - } -#endif - if (p_point.x < position.x) { - return false; - } - if (p_point.y < position.y) { - return false; - } - - if (p_point.x >= (position.x + size.x)) { - return false; - } - if (p_point.y >= (position.y + size.y)) { - return false; - } - - return true; - } - - bool operator==(const Rect2i &p_rect) const { return position == p_rect.position && size == p_rect.size; } - bool operator!=(const Rect2i &p_rect) const { return position != p_rect.position || size != p_rect.size; } - - Rect2i grow(int p_amount) const { - Rect2i g = *this; - g.position.x -= p_amount; - g.position.y -= p_amount; - g.size.width += p_amount * 2; - g.size.height += p_amount * 2; - return g; - } - - inline Rect2i grow_side(Side p_side, int p_amount) const { - Rect2i g = *this; - g = g.grow_individual((SIDE_LEFT == p_side) ? p_amount : 0, - (SIDE_TOP == p_side) ? p_amount : 0, - (SIDE_RIGHT == p_side) ? p_amount : 0, - (SIDE_BOTTOM == p_side) ? p_amount : 0); - return g; - } - - inline Rect2i grow_side_bind(uint32_t p_side, int p_amount) const { - return grow_side(Side(p_side), p_amount); - } - - inline Rect2i grow_individual(int p_left, int p_top, int p_right, int p_bottom) const { - Rect2i g = *this; - g.position.x -= p_left; - g.position.y -= p_top; - g.size.width += p_left + p_right; - g.size.height += p_top + p_bottom; - - return g; - } - - _FORCE_INLINE_ Rect2i expand(const Vector2i &p_vector) const { - Rect2i r = *this; - r.expand_to(p_vector); - return r; - } - - inline void expand_to(const Point2i &p_vector) { -#ifdef MATH_CHECKS - if (unlikely(size.x < 0 || size.y < 0)) { - ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); - } -#endif - Point2i begin = position; - Point2i end = position + size; - - if (p_vector.x < begin.x) { - begin.x = p_vector.x; - } - if (p_vector.y < begin.y) { - begin.y = p_vector.y; - } - - if (p_vector.x > end.x) { - end.x = p_vector.x; - } - if (p_vector.y > end.y) { - end.y = p_vector.y; - } - - position = begin; - size = end - begin; - } - - _FORCE_INLINE_ Rect2i abs() const { - return Rect2i(Point2i(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0)), size.abs()); - } - - _FORCE_INLINE_ void set_end(const Vector2i &p_end) { - size = p_end - position; - } - - _FORCE_INLINE_ Vector2i get_end() const { - return position + size; - } - - operator String() const; - - operator Rect2() const { return Rect2(position, size); } - - Rect2i() {} - Rect2i(const Rect2 &p_r2) : - position(p_r2.position), - size(p_r2.size) { - } - Rect2i(int p_x, int p_y, int p_width, int p_height) : - position(Point2i(p_x, p_y)), - size(Size2i(p_width, p_height)) { - } - Rect2i(const Point2i &p_pos, const Size2i &p_size) : - position(p_pos), - size(p_size) { - } -}; - #endif // RECT2_H diff --git a/core/math/rect2i.cpp b/core/math/rect2i.cpp new file mode 100644 index 0000000000..0782c450d0 --- /dev/null +++ b/core/math/rect2i.cpp @@ -0,0 +1,42 @@ +/*************************************************************************/ +/* rect2i.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 "rect2i.h" + +#include "core/math/rect2.h" +#include "core/string/ustring.h" + +Rect2i::operator String() const { + return "[P: " + position.operator String() + ", S: " + size + "]"; +} + +Rect2i::operator Rect2() const { + return Rect2(position, size); +} diff --git a/core/math/rect2i.h b/core/math/rect2i.h new file mode 100644 index 0000000000..db1459a3e6 --- /dev/null +++ b/core/math/rect2i.h @@ -0,0 +1,245 @@ +/*************************************************************************/ +/* rect2i.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 RECT2I_H +#define RECT2I_H + +#include "core/error/error_macros.h" +#include "core/math/vector2i.h" + +class String; +struct Rect2; + +struct _NO_DISCARD_ Rect2i { + Point2i position; + Size2i size; + + const Point2i &get_position() const { return position; } + void set_position(const Point2i &p_position) { position = p_position; } + const Size2i &get_size() const { return size; } + void set_size(const Size2i &p_size) { size = p_size; } + + int get_area() const { return size.width * size.height; } + + _FORCE_INLINE_ Vector2i get_center() const { return position + (size / 2); } + + inline bool intersects(const Rect2i &p_rect) const { +#ifdef MATH_CHECKS + if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { + ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); + } +#endif + if (position.x >= (p_rect.position.x + p_rect.size.width)) { + return false; + } + if ((position.x + size.width) <= p_rect.position.x) { + return false; + } + if (position.y >= (p_rect.position.y + p_rect.size.height)) { + return false; + } + if ((position.y + size.height) <= p_rect.position.y) { + return false; + } + + return true; + } + + inline bool encloses(const Rect2i &p_rect) const { +#ifdef MATH_CHECKS + if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { + ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); + } +#endif + return (p_rect.position.x >= position.x) && (p_rect.position.y >= position.y) && + ((p_rect.position.x + p_rect.size.x) <= (position.x + size.x)) && + ((p_rect.position.y + p_rect.size.y) <= (position.y + size.y)); + } + + _FORCE_INLINE_ bool has_no_area() const { + return (size.x <= 0 || size.y <= 0); + } + + // Returns the instersection between two Rect2is or an empty Rect2i if there is no intersection + inline Rect2i intersection(const Rect2i &p_rect) const { + Rect2i new_rect = p_rect; + + if (!intersects(new_rect)) { + return Rect2i(); + } + + new_rect.position.x = MAX(p_rect.position.x, position.x); + new_rect.position.y = MAX(p_rect.position.y, position.y); + + Point2i p_rect_end = p_rect.position + p_rect.size; + Point2i end = position + size; + + new_rect.size.x = MIN(p_rect_end.x, end.x) - new_rect.position.x; + new_rect.size.y = MIN(p_rect_end.y, end.y) - new_rect.position.y; + + return new_rect; + } + + inline Rect2i merge(const Rect2i &p_rect) const { ///< return a merged rect +#ifdef MATH_CHECKS + if (unlikely(size.x < 0 || size.y < 0 || p_rect.size.x < 0 || p_rect.size.y < 0)) { + ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); + } +#endif + Rect2i new_rect; + + new_rect.position.x = MIN(p_rect.position.x, position.x); + new_rect.position.y = MIN(p_rect.position.y, position.y); + + new_rect.size.x = MAX(p_rect.position.x + p_rect.size.x, position.x + size.x); + new_rect.size.y = MAX(p_rect.position.y + p_rect.size.y, position.y + size.y); + + new_rect.size = new_rect.size - new_rect.position; //make relative again + + return new_rect; + } + bool has_point(const Point2i &p_point) const { +#ifdef MATH_CHECKS + if (unlikely(size.x < 0 || size.y < 0)) { + ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); + } +#endif + if (p_point.x < position.x) { + return false; + } + if (p_point.y < position.y) { + return false; + } + + if (p_point.x >= (position.x + size.x)) { + return false; + } + if (p_point.y >= (position.y + size.y)) { + return false; + } + + return true; + } + + bool operator==(const Rect2i &p_rect) const { return position == p_rect.position && size == p_rect.size; } + bool operator!=(const Rect2i &p_rect) const { return position != p_rect.position || size != p_rect.size; } + + Rect2i grow(int p_amount) const { + Rect2i g = *this; + g.position.x -= p_amount; + g.position.y -= p_amount; + g.size.width += p_amount * 2; + g.size.height += p_amount * 2; + return g; + } + + inline Rect2i grow_side(Side p_side, int p_amount) const { + Rect2i g = *this; + g = g.grow_individual((SIDE_LEFT == p_side) ? p_amount : 0, + (SIDE_TOP == p_side) ? p_amount : 0, + (SIDE_RIGHT == p_side) ? p_amount : 0, + (SIDE_BOTTOM == p_side) ? p_amount : 0); + return g; + } + + inline Rect2i grow_side_bind(uint32_t p_side, int p_amount) const { + return grow_side(Side(p_side), p_amount); + } + + inline Rect2i grow_individual(int p_left, int p_top, int p_right, int p_bottom) const { + Rect2i g = *this; + g.position.x -= p_left; + g.position.y -= p_top; + g.size.width += p_left + p_right; + g.size.height += p_top + p_bottom; + + return g; + } + + _FORCE_INLINE_ Rect2i expand(const Vector2i &p_vector) const { + Rect2i r = *this; + r.expand_to(p_vector); + return r; + } + + inline void expand_to(const Point2i &p_vector) { +#ifdef MATH_CHECKS + if (unlikely(size.x < 0 || size.y < 0)) { + ERR_PRINT("Rect2i size is negative, this is not supported. Use Rect2i.abs() to get a Rect2i with a positive size."); + } +#endif + Point2i begin = position; + Point2i end = position + size; + + if (p_vector.x < begin.x) { + begin.x = p_vector.x; + } + if (p_vector.y < begin.y) { + begin.y = p_vector.y; + } + + if (p_vector.x > end.x) { + end.x = p_vector.x; + } + if (p_vector.y > end.y) { + end.y = p_vector.y; + } + + position = begin; + size = end - begin; + } + + _FORCE_INLINE_ Rect2i abs() const { + return Rect2i(Point2i(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0)), size.abs()); + } + + _FORCE_INLINE_ void set_end(const Vector2i &p_end) { + size = p_end - position; + } + + _FORCE_INLINE_ Vector2i get_end() const { + return position + size; + } + + operator String() const; + operator Rect2() const; + + Rect2i() {} + Rect2i(int p_x, int p_y, int p_width, int p_height) : + position(Point2i(p_x, p_y)), + size(Size2i(p_width, p_height)) { + } + Rect2i(const Point2i &p_pos, const Size2i &p_size) : + position(p_pos), + size(p_size) { + } +}; + +#endif // RECT2I_H diff --git a/core/math/transform_2d.cpp b/core/math/transform_2d.cpp index 0201cf575c..e6e24e9b32 100644 --- a/core/math/transform_2d.cpp +++ b/core/math/transform_2d.cpp @@ -30,6 +30,8 @@ #include "transform_2d.h" +#include "core/string/ustring.h" + void Transform2D::invert() { // FIXME: this function assumes the basis is a rotation matrix, with no scaling. // Transform2D::affine_inverse can handle matrices with scaling, so GDScript should eventually use that. diff --git a/core/math/transform_2d.h b/core/math/transform_2d.h index 6c2d51bd9b..f4546c13c8 100644 --- a/core/math/transform_2d.h +++ b/core/math/transform_2d.h @@ -31,7 +31,12 @@ #ifndef TRANSFORM_2D_H #define TRANSFORM_2D_H -#include "core/math/rect2.h" // also includes vector2, math_funcs, and ustring +#include "core/math/math_funcs.h" +#include "core/math/rect2.h" +#include "core/math/vector2.h" +#include "core/templates/vector.h" + +class String; struct _NO_DISCARD_ Transform2D { // Warning #1: basis of Transform2D is stored differently from Basis. In terms of elements array, the basis matrix looks like "on paper": diff --git a/core/math/transform_3d.h b/core/math/transform_3d.h index c16c278e74..3b4762e221 100644 --- a/core/math/transform_3d.h +++ b/core/math/transform_3d.h @@ -28,15 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TRANSFORM_H -#define TRANSFORM_H +#ifndef TRANSFORM_3D_H +#define TRANSFORM_3D_H #include "core/math/aabb.h" #include "core/math/basis.h" #include "core/math/plane.h" -class _NO_DISCARD_ Transform3D { -public: +struct _NO_DISCARD_ Transform3D { Basis basis; Vector3 origin; @@ -265,4 +264,4 @@ _FORCE_INLINE_ Plane Transform3D::xform_inv_fast(const Plane &p_plane, const Tra return Plane(normal, d); } -#endif // TRANSFORM_H +#endif // TRANSFORM_3D_H diff --git a/core/math/triangulate.h b/core/math/triangulate.h index d96bdb8cab..0bfcfcb978 100644 --- a/core/math/triangulate.h +++ b/core/math/triangulate.h @@ -32,6 +32,7 @@ #define TRIANGULATE_H #include "core/math/vector2.h" +#include "core/templates/vector.h" /* https://www.flipcode.com/archives/Efficient_Polygon_Triangulation.shtml diff --git a/core/math/vector2.cpp b/core/math/vector2.cpp index 676a0004ea..40149e8cc1 100644 --- a/core/math/vector2.cpp +++ b/core/math/vector2.cpp @@ -30,6 +30,9 @@ #include "vector2.h" +#include "core/math/vector2i.h" +#include "core/string/ustring.h" + real_t Vector2::angle() const { return Math::atan2(y, x); } @@ -202,91 +205,6 @@ Vector2::operator String() const { return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ")"; } -/* Vector2i */ - -Vector2i Vector2i::clamp(const Vector2i &p_min, const Vector2i &p_max) const { - return Vector2i( - CLAMP(x, p_min.x, p_max.x), - CLAMP(y, p_min.y, p_max.y)); -} - -int64_t Vector2i::length_squared() const { - return x * (int64_t)x + y * (int64_t)y; -} - -double Vector2i::length() const { - return Math::sqrt((double)length_squared()); -} - -Vector2i Vector2i::operator+(const Vector2i &p_v) const { - return Vector2i(x + p_v.x, y + p_v.y); -} - -void Vector2i::operator+=(const Vector2i &p_v) { - x += p_v.x; - y += p_v.y; -} - -Vector2i Vector2i::operator-(const Vector2i &p_v) const { - return Vector2i(x - p_v.x, y - p_v.y); -} - -void Vector2i::operator-=(const Vector2i &p_v) { - x -= p_v.x; - y -= p_v.y; -} - -Vector2i Vector2i::operator*(const Vector2i &p_v1) const { - return Vector2i(x * p_v1.x, y * p_v1.y); -} - -Vector2i Vector2i::operator*(const int32_t &rvalue) const { - return Vector2i(x * rvalue, y * rvalue); -} - -void Vector2i::operator*=(const int32_t &rvalue) { - x *= rvalue; - y *= rvalue; -} - -Vector2i Vector2i::operator/(const Vector2i &p_v1) const { - return Vector2i(x / p_v1.x, y / p_v1.y); -} - -Vector2i Vector2i::operator/(const int32_t &rvalue) const { - return Vector2i(x / rvalue, y / rvalue); -} - -void Vector2i::operator/=(const int32_t &rvalue) { - x /= rvalue; - y /= rvalue; -} - -Vector2i Vector2i::operator%(const Vector2i &p_v1) const { - return Vector2i(x % p_v1.x, y % p_v1.y); -} - -Vector2i Vector2i::operator%(const int32_t &rvalue) const { - return Vector2i(x % rvalue, y % rvalue); -} - -void Vector2i::operator%=(const int32_t &rvalue) { - x %= rvalue; - y %= rvalue; -} - -Vector2i Vector2i::operator-() const { - return Vector2i(-x, -y); -} - -bool Vector2i::operator==(const Vector2i &p_vec2) const { - return x == p_vec2.x && y == p_vec2.y; -} - -bool Vector2i::operator!=(const Vector2i &p_vec2) const { - return x != p_vec2.x || y != p_vec2.y; -} - -Vector2i::operator String() const { - return "(" + itos(x) + ", " + itos(y) + ")"; +Vector2::operator Vector2i() const { + return Vector2i(x, y); } diff --git a/core/math/vector2.h b/core/math/vector2.h index af40b9e68d..9edaaebf89 100644 --- a/core/math/vector2.h +++ b/core/math/vector2.h @@ -32,8 +32,8 @@ #define VECTOR2_H #include "core/math/math_funcs.h" -#include "core/string/ustring.h" +class String; struct Vector2i; struct _NO_DISCARD_ Vector2 { @@ -167,6 +167,7 @@ struct _NO_DISCARD_ Vector2 { real_t aspect() const { return width / height; } operator String() const; + operator Vector2i() const; _FORCE_INLINE_ Vector2() {} _FORCE_INLINE_ Vector2(const real_t p_x, const real_t p_y) { @@ -282,113 +283,4 @@ Vector2 Vector2::direction_to(const Vector2 &p_to) const { typedef Vector2 Size2; typedef Vector2 Point2; -/* INTEGER STUFF */ - -struct _NO_DISCARD_ Vector2i { - enum Axis { - AXIS_X, - AXIS_Y, - }; - - union { - int32_t x = 0; - int32_t width; - }; - union { - int32_t y = 0; - int32_t height; - }; - - _FORCE_INLINE_ int32_t &operator[](int p_idx) { - return p_idx ? y : x; - } - _FORCE_INLINE_ const int32_t &operator[](int p_idx) const { - return p_idx ? y : x; - } - - _FORCE_INLINE_ Vector2i::Axis min_axis_index() const { - return x < y ? Vector2i::AXIS_X : Vector2i::AXIS_Y; - } - - _FORCE_INLINE_ Vector2i::Axis max_axis_index() const { - return x < y ? Vector2i::AXIS_Y : Vector2i::AXIS_X; - } - - Vector2i min(const Vector2i &p_vector2i) const { - return Vector2(MIN(x, p_vector2i.x), MIN(y, p_vector2i.y)); - } - - Vector2i max(const Vector2i &p_vector2i) const { - return Vector2(MAX(x, p_vector2i.x), MAX(y, p_vector2i.y)); - } - - Vector2i operator+(const Vector2i &p_v) const; - void operator+=(const Vector2i &p_v); - Vector2i operator-(const Vector2i &p_v) const; - void operator-=(const Vector2i &p_v); - Vector2i operator*(const Vector2i &p_v1) const; - - Vector2i operator*(const int32_t &rvalue) const; - void operator*=(const int32_t &rvalue); - - Vector2i operator/(const Vector2i &p_v1) const; - Vector2i operator/(const int32_t &rvalue) const; - void operator/=(const int32_t &rvalue); - - Vector2i operator%(const Vector2i &p_v1) const; - Vector2i operator%(const int32_t &rvalue) const; - void operator%=(const int32_t &rvalue); - - Vector2i operator-() const; - bool operator<(const Vector2i &p_vec2) const { return (x == p_vec2.x) ? (y < p_vec2.y) : (x < p_vec2.x); } - bool operator>(const Vector2i &p_vec2) const { return (x == p_vec2.x) ? (y > p_vec2.y) : (x > p_vec2.x); } - - bool operator<=(const Vector2i &p_vec2) const { return x == p_vec2.x ? (y <= p_vec2.y) : (x < p_vec2.x); } - bool operator>=(const Vector2i &p_vec2) const { return x == p_vec2.x ? (y >= p_vec2.y) : (x > p_vec2.x); } - - bool operator==(const Vector2i &p_vec2) const; - bool operator!=(const Vector2i &p_vec2) const; - - int64_t length_squared() const; - double length() const; - - real_t aspect() const { return width / (real_t)height; } - Vector2i sign() const { return Vector2i(SIGN(x), SIGN(y)); } - Vector2i abs() const { return Vector2i(ABS(x), ABS(y)); } - Vector2i clamp(const Vector2i &p_min, const Vector2i &p_max) const; - - operator String() const; - - operator Vector2() const { return Vector2(x, y); } - - inline Vector2i() {} - inline Vector2i(const Vector2 &p_vec2) { - x = (int32_t)p_vec2.x; - y = (int32_t)p_vec2.y; - } - inline Vector2i(const int32_t p_x, const int32_t p_y) { - x = p_x; - y = p_y; - } -}; - -_FORCE_INLINE_ Vector2i operator*(const int32_t &p_scalar, const Vector2i &p_vector) { - return p_vector * p_scalar; -} - -_FORCE_INLINE_ Vector2i operator*(const int64_t &p_scalar, const Vector2i &p_vector) { - return p_vector * p_scalar; -} - -_FORCE_INLINE_ Vector2i operator*(const float &p_scalar, const Vector2i &p_vector) { - return p_vector * p_scalar; -} - -_FORCE_INLINE_ Vector2i operator*(const double &p_scalar, const Vector2i &p_vector) { - return p_vector * p_scalar; -} - -typedef Vector2i Size2i; -typedef Vector2i Point2i; - #endif // VECTOR2_H diff --git a/core/math/vector2i.cpp b/core/math/vector2i.cpp new file mode 100644 index 0000000000..dfed42e4d6 --- /dev/null +++ b/core/math/vector2i.cpp @@ -0,0 +1,125 @@ +/*************************************************************************/ +/* vector2i.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 "vector2i.h" + +#include "core/math/vector2.h" +#include "core/string/ustring.h" + +Vector2i Vector2i::clamp(const Vector2i &p_min, const Vector2i &p_max) const { + return Vector2i( + CLAMP(x, p_min.x, p_max.x), + CLAMP(y, p_min.y, p_max.y)); +} + +int64_t Vector2i::length_squared() const { + return x * (int64_t)x + y * (int64_t)y; +} + +double Vector2i::length() const { + return Math::sqrt((double)length_squared()); +} + +Vector2i Vector2i::operator+(const Vector2i &p_v) const { + return Vector2i(x + p_v.x, y + p_v.y); +} + +void Vector2i::operator+=(const Vector2i &p_v) { + x += p_v.x; + y += p_v.y; +} + +Vector2i Vector2i::operator-(const Vector2i &p_v) const { + return Vector2i(x - p_v.x, y - p_v.y); +} + +void Vector2i::operator-=(const Vector2i &p_v) { + x -= p_v.x; + y -= p_v.y; +} + +Vector2i Vector2i::operator*(const Vector2i &p_v1) const { + return Vector2i(x * p_v1.x, y * p_v1.y); +} + +Vector2i Vector2i::operator*(const int32_t &rvalue) const { + return Vector2i(x * rvalue, y * rvalue); +} + +void Vector2i::operator*=(const int32_t &rvalue) { + x *= rvalue; + y *= rvalue; +} + +Vector2i Vector2i::operator/(const Vector2i &p_v1) const { + return Vector2i(x / p_v1.x, y / p_v1.y); +} + +Vector2i Vector2i::operator/(const int32_t &rvalue) const { + return Vector2i(x / rvalue, y / rvalue); +} + +void Vector2i::operator/=(const int32_t &rvalue) { + x /= rvalue; + y /= rvalue; +} + +Vector2i Vector2i::operator%(const Vector2i &p_v1) const { + return Vector2i(x % p_v1.x, y % p_v1.y); +} + +Vector2i Vector2i::operator%(const int32_t &rvalue) const { + return Vector2i(x % rvalue, y % rvalue); +} + +void Vector2i::operator%=(const int32_t &rvalue) { + x %= rvalue; + y %= rvalue; +} + +Vector2i Vector2i::operator-() const { + return Vector2i(-x, -y); +} + +bool Vector2i::operator==(const Vector2i &p_vec2) const { + return x == p_vec2.x && y == p_vec2.y; +} + +bool Vector2i::operator!=(const Vector2i &p_vec2) const { + return x != p_vec2.x || y != p_vec2.y; +} + +Vector2i::operator String() const { + return "(" + itos(x) + ", " + itos(y) + ")"; +} + +Vector2i::operator Vector2() const { + return Vector2((int32_t)x, (int32_t)y); +} diff --git a/core/math/vector2i.h b/core/math/vector2i.h new file mode 100644 index 0000000000..446e05f5dd --- /dev/null +++ b/core/math/vector2i.h @@ -0,0 +1,141 @@ +/*************************************************************************/ +/* vector2i.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 VECTOR2I_H +#define VECTOR2I_H + +#include "core/math/math_funcs.h" + +class String; +struct Vector2; + +struct _NO_DISCARD_ Vector2i { + enum Axis { + AXIS_X, + AXIS_Y, + }; + + union { + int32_t x = 0; + int32_t width; + }; + union { + int32_t y = 0; + int32_t height; + }; + + _FORCE_INLINE_ int32_t &operator[](int p_idx) { + return p_idx ? y : x; + } + _FORCE_INLINE_ const int32_t &operator[](int p_idx) const { + return p_idx ? y : x; + } + + _FORCE_INLINE_ Vector2i::Axis min_axis_index() const { + return x < y ? Vector2i::AXIS_X : Vector2i::AXIS_Y; + } + + _FORCE_INLINE_ Vector2i::Axis max_axis_index() const { + return x < y ? Vector2i::AXIS_Y : Vector2i::AXIS_X; + } + + Vector2i min(const Vector2i &p_vector2i) const { + return Vector2i(MIN(x, p_vector2i.x), MIN(y, p_vector2i.y)); + } + + Vector2i max(const Vector2i &p_vector2i) const { + return Vector2i(MAX(x, p_vector2i.x), MAX(y, p_vector2i.y)); + } + + Vector2i operator+(const Vector2i &p_v) const; + void operator+=(const Vector2i &p_v); + Vector2i operator-(const Vector2i &p_v) const; + void operator-=(const Vector2i &p_v); + Vector2i operator*(const Vector2i &p_v1) const; + + Vector2i operator*(const int32_t &rvalue) const; + void operator*=(const int32_t &rvalue); + + Vector2i operator/(const Vector2i &p_v1) const; + Vector2i operator/(const int32_t &rvalue) const; + void operator/=(const int32_t &rvalue); + + Vector2i operator%(const Vector2i &p_v1) const; + Vector2i operator%(const int32_t &rvalue) const; + void operator%=(const int32_t &rvalue); + + Vector2i operator-() const; + bool operator<(const Vector2i &p_vec2) const { return (x == p_vec2.x) ? (y < p_vec2.y) : (x < p_vec2.x); } + bool operator>(const Vector2i &p_vec2) const { return (x == p_vec2.x) ? (y > p_vec2.y) : (x > p_vec2.x); } + + bool operator<=(const Vector2i &p_vec2) const { return x == p_vec2.x ? (y <= p_vec2.y) : (x < p_vec2.x); } + bool operator>=(const Vector2i &p_vec2) const { return x == p_vec2.x ? (y >= p_vec2.y) : (x > p_vec2.x); } + + bool operator==(const Vector2i &p_vec2) const; + bool operator!=(const Vector2i &p_vec2) const; + + int64_t length_squared() const; + double length() const; + + real_t aspect() const { return width / (real_t)height; } + Vector2i sign() const { return Vector2i(SIGN(x), SIGN(y)); } + Vector2i abs() const { return Vector2i(ABS(x), ABS(y)); } + Vector2i clamp(const Vector2i &p_min, const Vector2i &p_max) const; + + operator String() const; + operator Vector2() const; + + inline Vector2i() {} + inline Vector2i(const int32_t p_x, const int32_t p_y) { + x = p_x; + y = p_y; + } +}; + +_FORCE_INLINE_ Vector2i operator*(const int32_t &p_scalar, const Vector2i &p_vector) { + return p_vector * p_scalar; +} + +_FORCE_INLINE_ Vector2i operator*(const int64_t &p_scalar, const Vector2i &p_vector) { + return p_vector * p_scalar; +} + +_FORCE_INLINE_ Vector2i operator*(const float &p_scalar, const Vector2i &p_vector) { + return p_vector * p_scalar; +} + +_FORCE_INLINE_ Vector2i operator*(const double &p_scalar, const Vector2i &p_vector) { + return p_vector * p_scalar; +} + +typedef Vector2i Size2i; +typedef Vector2i Point2i; + +#endif // VECTOR2I_H diff --git a/core/math/vector3.h b/core/math/vector3.h index b62edef40f..79ba5c4f15 100644 --- a/core/math/vector3.h +++ b/core/math/vector3.h @@ -35,7 +35,8 @@ #include "core/math/vector2.h" #include "core/math/vector3i.h" #include "core/string/ustring.h" -class Basis; + +struct Basis; struct _NO_DISCARD_ Vector3 { static const int AXIS_COUNT = 3; diff --git a/core/multiplayer/multiplayer_api.cpp b/core/multiplayer/multiplayer_api.cpp index 627825246a..41d6d14696 100644 --- a/core/multiplayer/multiplayer_api.cpp +++ b/core/multiplayer/multiplayer_api.cpp @@ -32,7 +32,6 @@ #include "core/debugger/engine_debugger.h" #include "core/io/marshalls.h" -#include "core/multiplayer/multiplayer_replicator.h" #include "core/multiplayer/rpc_manager.h" #include "scene/main/node.h" @@ -42,6 +41,8 @@ #include "core/os/os.h" #endif +MultiplayerReplicationInterface *(*MultiplayerAPI::create_default_replication_interface)(MultiplayerAPI *p_multiplayer) = nullptr; + #ifdef DEBUG_ENABLED void MultiplayerAPI::profile_bandwidth(const String &p_inout, int p_size) { if (EngineDebugger::is_profiling("multiplayer")) { @@ -74,7 +75,7 @@ void MultiplayerAPI::poll() { Error err = multiplayer_peer->get_packet(&packet, len); if (err != OK) { ERR_PRINT("Error getting packet!"); - break; // Something is wrong! + return; // Something is wrong! } remote_sender_id = sender; @@ -82,16 +83,13 @@ void MultiplayerAPI::poll() { remote_sender_id = 0; if (!multiplayer_peer.is_valid()) { - break; // It's also possible that a packet or RPC caused a disconnection, so also check here. + return; // It's also possible that a packet or RPC caused a disconnection, so also check here. } } - if (multiplayer_peer.is_valid() && multiplayer_peer->get_connection_status() == MultiplayerPeer::CONNECTION_CONNECTED) { - replicator->poll(); - } + replicator->on_network_process(); } void MultiplayerAPI::clear() { - replicator->clear(); connected_peers.clear(); path_get_cache.clear(); path_send_cache.clear(); @@ -133,6 +131,7 @@ void MultiplayerAPI::set_multiplayer_peer(const Ref<MultiplayerPeer> &p_peer) { multiplayer_peer->connect("connection_failed", callable_mp(this, &MultiplayerAPI::_connection_failed)); multiplayer_peer->connect("server_disconnected", callable_mp(this, &MultiplayerAPI::_server_disconnected)); } + replicator->on_reset(); } Ref<MultiplayerPeer> MultiplayerAPI::get_multiplayer_peer() const { @@ -167,13 +166,13 @@ void MultiplayerAPI::_process_packet(int p_from, const uint8_t *p_packet, int p_ _process_raw(p_from, p_packet, p_packet_len); } break; case NETWORK_COMMAND_SPAWN: { - replicator->process_spawn_despawn(p_from, p_packet, p_packet_len, true); + replicator->on_spawn_receive(p_from, p_packet, p_packet_len); } break; case NETWORK_COMMAND_DESPAWN: { - replicator->process_spawn_despawn(p_from, p_packet, p_packet_len, false); + replicator->on_despawn_receive(p_from, p_packet, p_packet_len); } break; case NETWORK_COMMAND_SYNC: { - replicator->process_sync(p_from, p_packet, p_packet_len); + replicator->on_sync_receive(p_from, p_packet, p_packet_len); } break; } } @@ -324,7 +323,7 @@ bool MultiplayerAPI::_send_confirm_path(Node *p_node, NodePath p_path, PathSentC #define ENCODE_16 1 << 5 #define ENCODE_32 2 << 5 #define ENCODE_64 3 << 5 -Error MultiplayerAPI::encode_and_compress_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len) { +Error MultiplayerAPI::encode_and_compress_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bool p_allow_object_decoding) { // Unreachable because `VARIANT_MAX` == 27 and `ENCODE_VARIANT_MASK` == 31 CRASH_COND(p_variant.get_type() > VARIANT_META_TYPE_MASK); @@ -385,7 +384,7 @@ Error MultiplayerAPI::encode_and_compress_variant(const Variant &p_variant, uint } break; default: // Any other case is not yet compressed. - Error err = encode_variant(p_variant, r_buffer, r_len, allow_object_decoding); + Error err = encode_variant(p_variant, r_buffer, r_len, p_allow_object_decoding); if (err != OK) { return err; } @@ -399,7 +398,7 @@ Error MultiplayerAPI::encode_and_compress_variant(const Variant &p_variant, uint return OK; } -Error MultiplayerAPI::decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len) { +Error MultiplayerAPI::decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len, bool p_allow_object_decoding) { const uint8_t *buf = p_buffer; int len = p_len; @@ -458,7 +457,7 @@ Error MultiplayerAPI::decode_and_decompress_variant(Variant &r_variant, const ui } } break; default: - Error err = decode_variant(r_variant, p_buffer, p_len, r_len, allow_object_decoding); + Error err = decode_variant(r_variant, p_buffer, p_len, r_len, p_allow_object_decoding); if (err != OK) { return err; } @@ -467,17 +466,84 @@ Error MultiplayerAPI::decode_and_decompress_variant(Variant &r_variant, const ui return OK; } +Error MultiplayerAPI::encode_and_compress_variants(const Variant **p_variants, int p_count, uint8_t *p_buffer, int &r_len, bool *r_raw, bool p_allow_object_decoding) { + r_len = 0; + int size = 0; + + if (p_count == 0) { + if (r_raw) { + *r_raw = true; + } + return OK; + } + + // Try raw encoding optimization. + if (r_raw && p_count == 1) { + *r_raw = false; + const Variant &v = *(p_variants[0]); + if (v.get_type() == Variant::PACKED_BYTE_ARRAY) { + *r_raw = true; + const PackedByteArray pba = v; + if (p_buffer) { + memcpy(p_buffer, pba.ptr(), pba.size()); + } + r_len += pba.size(); + } else { + encode_and_compress_variant(v, p_buffer, size, p_allow_object_decoding); + r_len += size; + } + return OK; + } + + // Regular encoding. + for (int i = 0; i < p_count; i++) { + const Variant &v = *(p_variants[i]); + encode_and_compress_variant(v, p_buffer ? p_buffer + r_len : nullptr, size, p_allow_object_decoding); + r_len += size; + } + return OK; +} + +Error MultiplayerAPI::decode_and_decompress_variants(Vector<Variant> &r_variants, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw, bool p_allow_object_decoding) { + r_len = 0; + int argc = r_variants.size(); + if (argc == 0 && p_raw) { + return OK; + } + ERR_FAIL_COND_V(p_raw && argc != 1, ERR_INVALID_DATA); + if (p_raw) { + r_len = p_len; + PackedByteArray pba; + pba.resize(p_len); + memcpy(pba.ptrw(), p_buffer, p_len); + r_variants.write[0] = pba; + return OK; + } + + Vector<Variant> args; + Vector<const Variant *> argp; + args.resize(argc); + + for (int i = 0; i < argc; i++) { + ERR_FAIL_COND_V_MSG(r_len >= p_len, ERR_INVALID_DATA, "Invalid packet received. Size too small."); + + int vlen; + Error err = MultiplayerAPI::decode_and_decompress_variant(r_variants.write[i], &p_buffer[r_len], p_len - r_len, &vlen, p_allow_object_decoding); + ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid packet received. Unable to decode state variable."); + r_len += vlen; + } + return OK; +} + void MultiplayerAPI::_add_peer(int p_id) { connected_peers.insert(p_id); path_get_cache.insert(p_id, PathGetCache()); - if (is_server()) { - replicator->spawn_all(p_id); - } + replicator->on_peer_change(p_id, true); emit_signal(SNAME("peer_connected"), p_id); } void MultiplayerAPI::_del_peer(int p_id) { - connected_peers.erase(p_id); + replicator->on_peer_change(p_id, false); // Cleanup get cache. path_get_cache.erase(p_id); // Cleanup sent cache. @@ -488,6 +554,7 @@ void MultiplayerAPI::_del_peer(int p_id) { PathSentCache *psc = path_send_cache.getptr(E); psc->confirmed_peers.erase(p_id); } + connected_peers.erase(p_id); emit_signal(SNAME("peer_disconnected"), p_id); } @@ -500,6 +567,7 @@ void MultiplayerAPI::_connection_failed() { } void MultiplayerAPI::_server_disconnected() { + replicator->on_reset(); emit_signal(SNAME("server_disconnected")); } @@ -612,14 +680,26 @@ bool MultiplayerAPI::is_object_decoding_allowed() const { return allow_object_decoding; } -void MultiplayerAPI::scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter) { - replicator->scene_enter_exit_notify(p_scene, p_node, p_enter); -} - void MultiplayerAPI::rpcp(Node *p_node, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount) { rpc_manager->rpcp(p_node, p_peer_id, p_method, p_arg, p_argcount); } +Error MultiplayerAPI::spawn(Object *p_object, Variant p_config) { + return replicator->on_spawn(p_object, p_config); +} + +Error MultiplayerAPI::despawn(Object *p_object, Variant p_config) { + return replicator->on_despawn(p_object, p_config); +} + +Error MultiplayerAPI::replication_start(Object *p_object, Variant p_config) { + return replicator->on_replication_start(p_object, p_config); +} + +Error MultiplayerAPI::replication_stop(Object *p_object, Variant p_config) { + return replicator->on_replication_stop(p_object, p_config); +} + void MultiplayerAPI::_bind_methods() { ClassDB::bind_method(D_METHOD("set_root_node", "node"), &MultiplayerAPI::set_root_node); ClassDB::bind_method(D_METHOD("get_root_node"), &MultiplayerAPI::get_root_node); @@ -638,14 +718,12 @@ void MultiplayerAPI::_bind_methods() { ClassDB::bind_method(D_METHOD("is_refusing_new_connections"), &MultiplayerAPI::is_refusing_new_connections); ClassDB::bind_method(D_METHOD("set_allow_object_decoding", "enable"), &MultiplayerAPI::set_allow_object_decoding); ClassDB::bind_method(D_METHOD("is_object_decoding_allowed"), &MultiplayerAPI::is_object_decoding_allowed); - ClassDB::bind_method(D_METHOD("get_replicator"), &MultiplayerAPI::get_replicator); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_connections"), "set_refuse_new_connections", "is_refusing_new_connections"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "multiplayer_peer", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerPeer", PROPERTY_USAGE_NONE), "set_multiplayer_peer", "get_multiplayer_peer"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root_node", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_root_node", "get_root_node"); ADD_PROPERTY_DEFAULT("refuse_new_connections", false); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replicator", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerReplicator", PROPERTY_USAGE_NONE), "", "get_replicator"); ADD_SIGNAL(MethodInfo("peer_connected", PropertyInfo(Variant::INT, "id"))); ADD_SIGNAL(MethodInfo("peer_disconnected", PropertyInfo(Variant::INT, "id"))); @@ -656,13 +734,16 @@ void MultiplayerAPI::_bind_methods() { } MultiplayerAPI::MultiplayerAPI() { - replicator = memnew(MultiplayerReplicator(this)); + if (create_default_replication_interface) { + replicator = Ref<MultiplayerReplicationInterface>(create_default_replication_interface(this)); + } else { + replicator.instantiate(); + } rpc_manager = memnew(RPCManager(this)); clear(); } MultiplayerAPI::~MultiplayerAPI() { clear(); - memdelete(replicator); memdelete(rpc_manager); } diff --git a/core/multiplayer/multiplayer_api.h b/core/multiplayer/multiplayer_api.h index 713035428d..f4fdafc323 100644 --- a/core/multiplayer/multiplayer_api.h +++ b/core/multiplayer/multiplayer_api.h @@ -35,7 +35,26 @@ #include "core/multiplayer/multiplayer_peer.h" #include "core/object/ref_counted.h" -class MultiplayerReplicator; +class MultiplayerAPI; + +class MultiplayerReplicationInterface : public RefCounted { + GDCLASS(MultiplayerReplicationInterface, RefCounted); + +public: + virtual void on_peer_change(int p_id, bool p_connected) {} + virtual void on_reset() {} + virtual Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { return ERR_UNAVAILABLE; } + virtual Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { return ERR_UNAVAILABLE; } + virtual Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { return ERR_UNAVAILABLE; } + virtual Error on_spawn(Object *p_obj, Variant p_config) { return ERR_UNAVAILABLE; } + virtual Error on_despawn(Object *p_obj, Variant p_config) { return ERR_UNAVAILABLE; } + virtual Error on_replication_start(Object *p_obj, Variant p_config) { return ERR_UNAVAILABLE; } + virtual Error on_replication_stop(Object *p_obj, Variant p_config) { return ERR_UNAVAILABLE; } + virtual void on_network_process() {} + + MultiplayerReplicationInterface() {} +}; + class RPCManager; class MultiplayerAPI : public RefCounted { @@ -95,7 +114,7 @@ private: Node *root_node = nullptr; bool allow_object_decoding = false; - MultiplayerReplicator *replicator = nullptr; + Ref<MultiplayerReplicationInterface> replicator; RPCManager *rpc_manager = nullptr; protected: @@ -108,6 +127,13 @@ protected: void _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len); public: + static MultiplayerReplicationInterface *(*create_default_replication_interface)(MultiplayerAPI *p_multiplayer); + + static Error encode_and_compress_variant(const Variant &p_variant, uint8_t *p_buffer, int &r_len, bool p_allow_object_decoding); + static Error decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len, bool p_allow_object_decoding); + static Error encode_and_compress_variants(const Variant **p_variants, int p_count, uint8_t *p_buffer, int &r_len, bool *r_raw = nullptr, bool p_allow_object_decoding = false); + static Error decode_and_decompress_variants(Vector<Variant> &r_variants, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw = false, bool p_allow_object_decoding = false); + void poll(); void clear(); void set_root_node(Node *p_node); @@ -117,13 +143,13 @@ public: Error send_bytes(Vector<uint8_t> p_data, int p_to = MultiplayerPeer::TARGET_PEER_BROADCAST, Multiplayer::TransferMode p_mode = Multiplayer::TRANSFER_MODE_RELIABLE, int p_channel = 0); - Error encode_and_compress_variant(const Variant &p_variant, uint8_t *p_buffer, int &r_len); - Error decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len); - // Called by Node.rpc void rpcp(Node *p_node, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount); - // Called by Node._notification - void scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter); + // Replication API + Error spawn(Object *p_object, Variant p_config); + Error despawn(Object *p_object, Variant p_config); + Error replication_start(Object *p_object, Variant p_config); + Error replication_stop(Object *p_object, Variant p_config); // Called by replicator bool send_confirm_path(Node *p_node, NodePath p_path, int p_target, int &p_id); Node *get_cached_node(int p_from, uint32_t p_node_id); @@ -148,7 +174,6 @@ public: void set_allow_object_decoding(bool p_enable); bool is_object_decoding_allowed() const; - MultiplayerReplicator *get_replicator() const { return replicator; } RPCManager *get_rpc_manager() const { return rpc_manager; } #ifdef DEBUG_ENABLED diff --git a/core/multiplayer/multiplayer_replicator.cpp b/core/multiplayer/multiplayer_replicator.cpp deleted file mode 100644 index e7de8219c7..0000000000 --- a/core/multiplayer/multiplayer_replicator.cpp +++ /dev/null @@ -1,791 +0,0 @@ -/*************************************************************************/ -/* multiplayer_replicator.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 "core/multiplayer/multiplayer_replicator.h" - -#include "core/io/marshalls.h" -#include "scene/main/node.h" -#include "scene/resources/packed_scene.h" - -#define MAKE_ROOM(m_amount) \ - if (packet_cache.size() < m_amount) \ - packet_cache.resize(m_amount); - -Error MultiplayerReplicator::_sync_all_default(const ResourceUID::ID &p_scene_id, int p_peer) { - ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER); - SceneConfig &cfg = replications[p_scene_id]; - int full_size = 0; - bool same_size = true; - int last_size = 0; - bool all_raw = true; - struct EncodeInfo { - int size = 0; - bool raw = false; - List<Variant> state; - }; - Map<ObjectID, struct EncodeInfo> state; - if (tracked_objects.has(p_scene_id)) { - for (const ObjectID &obj_id : tracked_objects[p_scene_id]) { - Object *obj = ObjectDB::get_instance(obj_id); - if (obj) { - struct EncodeInfo info; - Error err = _get_state(cfg.sync_properties, obj, info.state); - ERR_CONTINUE(err); - err = _encode_state(info.state, nullptr, info.size, &info.raw); - ERR_CONTINUE(err); - state[obj_id] = info; - full_size += info.size; - if (last_size && info.size != last_size) { - same_size = false; - } - all_raw = all_raw && info.raw; - last_size = info.size; - } - } - } - // Default implementation do not send empty updates. - if (!full_size) { - return OK; - } -#ifdef DEBUG_ENABLED - if (full_size > 4096 && cfg.sync_interval) { - WARN_PRINT_ONCE(vformat("The timed state update for scene %d is big (%d bytes) consider optimizing it", p_scene_id)); - } -#endif - if (same_size) { - // This is fast and small. Should we allow more than 256 objects per type? - // This costs us 1 byte. - MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + 2 + full_size); - } else { - MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + state.size() * 2 + full_size); - } - int ofs = 0; - uint8_t *ptr = packet_cache.ptrw(); - ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC | (same_size ? BYTE_OR_ZERO_FLAG : 0); - ofs = 1; - ofs += encode_uint64(p_scene_id, &ptr[ofs]); - ptr[ofs] = cfg.sync_recv++; - ofs += 1; - ofs += encode_uint16(state.size(), &ptr[ofs]); - if (same_size) { - ofs += encode_uint16(last_size + (all_raw ? 1 << 15 : 0), &ptr[ofs]); - } - for (const ObjectID &obj_id : tracked_objects[p_scene_id]) { - if (!state.has(obj_id)) { - continue; - } - struct EncodeInfo &info = state[obj_id]; - Object *obj = ObjectDB::get_instance(obj_id); - ERR_CONTINUE(!obj); - int size = 0; - if (!same_size) { - // We need to encode the size of every object. - ofs += encode_uint16(info.size + (info.raw ? 1 << 15 : 0), &ptr[ofs]); - } - Error err = _encode_state(info.state, &ptr[ofs], size, &info.raw); - ERR_CONTINUE(err); - ofs += size; - } - Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); - peer->set_target_peer(p_peer); - peer->set_transfer_channel(0); - peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_UNRELIABLE); - return peer->put_packet(ptr, ofs); -} - -void MultiplayerReplicator::_process_default_sync(const ResourceUID::ID &p_id, const uint8_t *p_packet, int p_packet_len) { - ERR_FAIL_COND_MSG(p_packet_len < SYNC_CMD_OFFSET + 5, "Invalid spawn packet received"); - ERR_FAIL_COND_MSG(!replications.has(p_id), "Invalid spawn ID received " + itos(p_id)); - SceneConfig &cfg = replications[p_id]; - ERR_FAIL_COND_MSG(cfg.mode != REPLICATION_MODE_SERVER || multiplayer->is_server(), "The default implementation only allows sync packets from the server"); - const bool same_size = p_packet[0] & BYTE_OR_ZERO_FLAG; - int ofs = SYNC_CMD_OFFSET; - int time = p_packet[ofs]; - // Skip old update. - if (time < cfg.sync_recv && cfg.sync_recv - time < 127) { - return; - } - cfg.sync_recv = time; - ofs += 1; - int count = decode_uint16(&p_packet[ofs]); - ofs += 2; -#ifdef DEBUG_ENABLED - ERR_FAIL_COND(!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count); -#else - if (!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count) { - return; - } -#endif - int data_size = 0; - bool raw = false; - if (same_size) { - // This is fast and optimized. - data_size = decode_uint16(&p_packet[ofs]); - raw = (data_size & (1 << 15)) != 0; - data_size = data_size & ~(1 << 15); - ofs += 2; - ERR_FAIL_COND(p_packet_len - ofs < data_size * count); - } - for (const ObjectID &obj_id : tracked_objects[p_id]) { - Object *obj = ObjectDB::get_instance(obj_id); - ERR_CONTINUE(!obj); - if (!same_size) { - // This is slow and wasteful. - data_size = decode_uint16(&p_packet[ofs]); - raw = (data_size & (1 << 15)) != 0; - data_size = data_size & ~(1 << 15); - ofs += 2; - ERR_FAIL_COND(p_packet_len - ofs < data_size); - } - int size = 0; - Error err = _decode_state(cfg.sync_properties, obj, &p_packet[ofs], data_size, size, raw); - ofs += data_size; - ERR_CONTINUE(err); - ERR_CONTINUE(size != data_size); - } -} - -Error MultiplayerReplicator::_send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn) { - ERR_FAIL_COND_V(p_spawn && !p_obj, ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER); - Error err; - // Prepare state - List<Variant> state_variants; - int state_len = 0; - const SceneConfig &cfg = replications[p_scene_id]; - if (p_spawn) { - if ((err = _get_state(cfg.properties, p_obj, state_variants)) != OK) { - return err; - } - } - - bool is_raw = false; - if (state_variants.size() == 1 && state_variants[0].get_type() == Variant::PACKED_BYTE_ARRAY) { - is_raw = true; - const PackedByteArray pba = state_variants[0]; - state_len = pba.size(); - } else if (state_variants.size()) { - err = _encode_state(state_variants, nullptr, state_len); - ERR_FAIL_COND_V(err, err); - } else { - is_raw = true; - } - - int ofs = 0; - - // Prepare simplified path - const Node *root_node = multiplayer->get_root_node(); - ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED); - NodePath rel_path = (root_node->get_path()).rel_path_to(p_path); - const Vector<StringName> names = rel_path.get_names(); - ERR_FAIL_COND_V(names.size() < 2, ERR_INVALID_PARAMETER); - - NodePath parent = NodePath(names.slice(0, names.size() - 1), false); - ERR_FAIL_COND_V_MSG(!root_node->has_node(parent), ERR_INVALID_PARAMETER, "Path not found: " + parent); - - int path_id = 0; - multiplayer->send_confirm_path(root_node->get_node(parent), parent, p_peer_id, path_id); - - // Encode name and parent ID. - CharString cname = String(names[names.size() - 1]).utf8(); - int nlen = encode_cstring(cname.get_data(), nullptr); - MAKE_ROOM(SPAWN_CMD_OFFSET + 4 + 4 + nlen + state_len); - uint8_t *ptr = packet_cache.ptrw(); - ptr[0] = (p_spawn ? MultiplayerAPI::NETWORK_COMMAND_SPAWN : MultiplayerAPI::NETWORK_COMMAND_DESPAWN) | (is_raw ? BYTE_OR_ZERO_FLAG : 0); - ofs = 1; - ofs += encode_uint64(p_scene_id, &ptr[ofs]); - ofs += encode_uint32(path_id, &ptr[ofs]); - ofs += encode_uint32(nlen, &ptr[ofs]); - ofs += encode_cstring(cname.get_data(), &ptr[ofs]); - - // Encode state. - if (!is_raw) { - _encode_state(state_variants, &ptr[ofs], state_len); - } else if (state_len) { - PackedByteArray pba = state_variants[0]; - memcpy(&ptr[ofs], pba.ptr(), state_len); - } - - Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); - peer->set_target_peer(p_peer_id); - peer->set_transfer_channel(0); - peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_RELIABLE); - return peer->put_packet(ptr, ofs + state_len); -} - -void MultiplayerReplicator::_process_default_spawn_despawn(int p_from, const ResourceUID::ID &p_scene_id, const uint8_t *p_packet, int p_packet_len, bool p_spawn) { - ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET + 9, "Invalid spawn packet received"); - int ofs = SPAWN_CMD_OFFSET; - uint32_t node_target = decode_uint32(&p_packet[ofs]); - Node *parent = multiplayer->get_cached_node(p_from, node_target); - ofs += 4; - ERR_FAIL_COND_MSG(parent == nullptr, "Invalid packet received. Requested node was not found."); - - uint32_t name_len = decode_uint32(&p_packet[ofs]); - ofs += 4; - ERR_FAIL_COND_MSG(name_len > uint32_t(p_packet_len - ofs), vformat("Invalid spawn packet size: %d, wants: %d", p_packet_len, ofs + name_len)); - ERR_FAIL_COND_MSG(name_len < 1, "Zero spawn name size."); - - const String name = String::utf8((const char *)&p_packet[ofs], name_len); - // We need to make sure no trickery happens here (e.g. despawning a subpath), but we want to allow autogenerated ("@") node names. - ERR_FAIL_COND_MSG(name.validate_node_name() != name.replace("@", ""), vformat("Invalid node name received: '%s'", name)); - ofs += name_len; - - const SceneConfig &cfg = replications[p_scene_id]; - if (cfg.mode == REPLICATION_MODE_SERVER && p_from == 1) { - String scene_path = ResourceUID::get_singleton()->get_id_path(p_scene_id); - if (p_spawn) { - const bool is_raw = ((p_packet[0] & BYTE_OR_ZERO_FLAG) >> BYTE_OR_ZERO_SHIFT) == 1; - - ERR_FAIL_COND_MSG(parent->has_node(name), vformat("Unable to spawn node. Node already exists: %s/%s", parent->get_path(), name)); - RES res = ResourceLoader::load(scene_path); - ERR_FAIL_COND_MSG(!res.is_valid(), "Unable to load scene to spawn at path: " + scene_path); - PackedScene *scene = Object::cast_to<PackedScene>(res.ptr()); - ERR_FAIL_COND(!scene); - Node *node = scene->instantiate(); - ERR_FAIL_COND(!node); - replicated_nodes[node->get_instance_id()] = p_scene_id; - _track(p_scene_id, node); - int size; - _decode_state(cfg.properties, node, &p_packet[ofs], p_packet_len - ofs, size, is_raw); - parent->_add_child_nocheck(node, name); - emit_signal(SNAME("spawned"), p_scene_id, node); - } else { - ERR_FAIL_COND_MSG(!parent->has_node(name), vformat("Path not found: %s/%s", parent->get_path(), name)); - Node *node = parent->get_node(name); - ERR_FAIL_COND_MSG(!replicated_nodes.has(node->get_instance_id()), vformat("Trying to despawn a Node that was not replicated: %s/%s", parent->get_path(), name)); - emit_signal(SNAME("despawned"), p_scene_id, node); - _untrack(p_scene_id, node); - replicated_nodes.erase(node->get_instance_id()); - node->queue_delete(); - } - } else { - PackedByteArray data; - if (p_packet_len > ofs) { - data.resize(p_packet_len - ofs); - memcpy(data.ptrw(), &p_packet[ofs], data.size()); - } - if (p_spawn) { - emit_signal(SNAME("spawn_requested"), p_from, p_scene_id, parent, name, data); - } else { - emit_signal(SNAME("despawn_requested"), p_from, p_scene_id, parent, name, data); - } - } -} - -void MultiplayerReplicator::process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn) { - ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET, "Invalid spawn packet received"); - ResourceUID::ID id = decode_uint64(&p_packet[1]); - ERR_FAIL_COND_MSG(!replications.has(id), "Invalid spawn ID received " + itos(id)); - - const SceneConfig &cfg = replications[id]; - if (cfg.on_spawn_despawn_receive.is_valid()) { - int ofs = SPAWN_CMD_OFFSET; - bool is_raw = ((p_packet[0] & BYTE_OR_ZERO_FLAG) >> BYTE_OR_ZERO_SHIFT) == 1; - Variant data; - int left = p_packet_len - ofs; - if (is_raw && left) { - PackedByteArray pba; - pba.resize(left); - memcpy(pba.ptrw(), &p_packet[ofs], pba.size()); - data = pba; - } else if (left) { - ERR_FAIL_COND(decode_variant(data, &p_packet[ofs], left) != OK); - } - - Variant args[4]; - args[0] = p_from; - args[1] = id; - args[2] = data; - args[3] = p_spawn; - const Variant *argp[] = { &args[0], &args[1], &args[2], &args[3] }; - Callable::CallError ce; - Variant ret; - cfg.on_spawn_despawn_receive.call(argp, 4, ret, ce); - ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, "Custom receive function failed"); - } else { - _process_default_spawn_despawn(p_from, id, p_packet, p_packet_len, p_spawn); - } -} - -void MultiplayerReplicator::process_sync(int p_from, const uint8_t *p_packet, int p_packet_len) { - ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET, "Invalid spawn packet received"); - ResourceUID::ID id = decode_uint64(&p_packet[1]); - ERR_FAIL_COND_MSG(!replications.has(id), "Invalid spawn ID received " + itos(id)); - const SceneConfig &cfg = replications[id]; - if (cfg.on_sync_receive.is_valid()) { - Array objs; - if (tracked_objects.has(id)) { - objs.resize(tracked_objects[id].size()); - int idx = 0; - for (const ObjectID &obj_id : tracked_objects[id]) { - objs[idx++] = ObjectDB::get_instance(obj_id); - } - } - PackedByteArray pba; - pba.resize(p_packet_len - SYNC_CMD_OFFSET); - if (pba.size()) { - memcpy(pba.ptrw(), p_packet + SYNC_CMD_OFFSET, p_packet_len - SYNC_CMD_OFFSET); - } - Variant args[4] = { p_from, id, objs, pba }; - Variant *argp[4] = { args, &args[1], &args[2], &args[3] }; - Callable::CallError ce; - Variant ret; - cfg.on_sync_receive.call((const Variant **)argp, 4, ret, ce); - ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, "Custom sync function failed"); - } else { - ERR_FAIL_COND_MSG(p_from != 1, "Default sync implementation only allow syncing from server to client"); - _process_default_sync(id, p_packet, p_packet_len); - } -} - -Error MultiplayerReplicator::_get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant) { - ERR_FAIL_COND_V_MSG(!p_obj, ERR_INVALID_PARAMETER, "Cannot encode null object"); - for (const StringName &prop : p_properties) { - bool valid = false; - const Variant v = p_obj->get(prop, &valid); - ERR_FAIL_COND_V_MSG(!valid, ERR_INVALID_DATA, vformat("Property '%s' not found.", prop)); - r_variant.push_back(v); - } - return OK; -} - -Error MultiplayerReplicator::_encode_state(const List<Variant> &p_variants, uint8_t *p_buffer, int &r_len, bool *r_raw) { - r_len = 0; - int size = 0; - - // Try raw encoding optimization. - if (r_raw && p_variants.size() == 1) { - *r_raw = false; - const Variant v = p_variants[0]; - if (v.get_type() == Variant::PACKED_BYTE_ARRAY) { - *r_raw = true; - const PackedByteArray pba = v; - if (p_buffer) { - memcpy(p_buffer, pba.ptr(), pba.size()); - } - r_len += pba.size(); - } else { - multiplayer->encode_and_compress_variant(v, p_buffer, size); - r_len += size; - } - return OK; - } - - // Regular encoding. - for (const Variant &v : p_variants) { - multiplayer->encode_and_compress_variant(v, p_buffer ? p_buffer + r_len : nullptr, size); - r_len += size; - } - return OK; -} - -Error MultiplayerReplicator::_decode_state(const List<StringName> &p_properties, Object *p_obj, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw) { - r_len = 0; - int argc = p_properties.size(); - if (argc == 0 && p_raw) { - ERR_FAIL_COND_V_MSG(p_len != 0, ERR_INVALID_DATA, "Buffer has trailing bytes."); - return OK; - } - ERR_FAIL_COND_V(p_raw && argc != 1, ERR_INVALID_DATA); - if (p_raw) { - r_len = p_len; - PackedByteArray pba; - pba.resize(p_len); - memcpy(pba.ptrw(), p_buffer, p_len); - p_obj->set(p_properties[0], pba); - return OK; - } - - Vector<Variant> args; - Vector<const Variant *> argp; - args.resize(argc); - - for (int i = 0; i < argc; i++) { - ERR_FAIL_COND_V_MSG(r_len >= p_len, ERR_INVALID_DATA, "Invalid packet received. Size too small."); - - int vlen; - Error err = multiplayer->decode_and_decompress_variant(args.write[i], &p_buffer[r_len], p_len - r_len, &vlen); - ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid packet received. Unable to decode state variable."); - r_len += vlen; - } - ERR_FAIL_COND_V_MSG(p_len - r_len != 0, ERR_INVALID_DATA, "Buffer has trailing bytes."); - - int i = 0; - for (const StringName &prop : p_properties) { - p_obj->set(prop, args[i]); - i += 1; - } - return OK; -} - -Error MultiplayerReplicator::spawn_config(const ResourceUID::ID &p_id, ReplicationMode p_mode, const TypedArray<StringName> &p_props, const Callable &p_on_send, const Callable &p_on_recv) { - ERR_FAIL_COND_V(p_mode < REPLICATION_MODE_NONE || p_mode > REPLICATION_MODE_CUSTOM, ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V_MSG(p_on_send.is_valid() != p_on_recv.is_valid(), ERR_INVALID_PARAMETER, "Send and receive custom callables must be both valid or both empty"); -#ifdef TOOLS_ENABLED - if (!p_on_send.is_valid()) { - // We allow non scene spawning with custom callables. - String path = ResourceUID::get_singleton()->get_id_path(p_id); - RES res = ResourceLoader::load(path); - ERR_FAIL_COND_V(!res->is_class("PackedScene"), ERR_INVALID_PARAMETER); - } -#endif - if (p_mode == REPLICATION_MODE_NONE) { - if (replications.has(p_id)) { - replications.erase(p_id); - } - } else { - SceneConfig cfg; - cfg.mode = p_mode; - for (int i = 0; i < p_props.size(); i++) { - cfg.properties.push_back(p_props[i]); - } - cfg.on_spawn_despawn_send = p_on_send; - cfg.on_spawn_despawn_receive = p_on_recv; - replications[p_id] = cfg; - } - return OK; -} - -Error MultiplayerReplicator::sync_config(const ResourceUID::ID &p_id, uint64_t p_interval, const TypedArray<StringName> &p_props, const Callable &p_on_send, const Callable &p_on_recv) { - ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V_MSG(p_on_send.is_valid() != p_on_recv.is_valid(), ERR_INVALID_PARAMETER, "Send and receive custom callables must be both valid or both empty"); - ERR_FAIL_COND_V(!replications.has(p_id), ERR_UNCONFIGURED); - SceneConfig &cfg = replications[p_id]; - ERR_FAIL_COND_V_MSG(p_interval && cfg.mode != REPLICATION_MODE_SERVER && !p_on_send.is_valid(), ERR_INVALID_PARAMETER, "Timed updates in custom mode are only allowed if custom callbacks are also specified"); - for (int i = 0; i < p_props.size(); i++) { - cfg.sync_properties.push_back(p_props[i]); - } - cfg.on_sync_send = p_on_send; - cfg.on_sync_receive = p_on_recv; - cfg.sync_interval = p_interval * 1000; - return OK; -} - -Error MultiplayerReplicator::_send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn) { - int data_size = 0; - int is_raw = false; - if (p_data.get_type() == Variant::PACKED_BYTE_ARRAY) { - const PackedByteArray pba = p_data; - is_raw = true; - data_size = p_data.operator PackedByteArray().size(); - } else if (p_data.get_type() == Variant::NIL) { - is_raw = true; - } else { - Error err = encode_variant(p_data, nullptr, data_size); - ERR_FAIL_COND_V(err, err); - } - MAKE_ROOM(SPAWN_CMD_OFFSET + data_size); - uint8_t *ptr = packet_cache.ptrw(); - ptr[0] = (p_spawn ? MultiplayerAPI::NETWORK_COMMAND_SPAWN : MultiplayerAPI::NETWORK_COMMAND_DESPAWN) + ((is_raw ? 1 : 0) << BYTE_OR_ZERO_SHIFT); - encode_uint64(p_scene_id, &ptr[1]); - if (p_data.get_type() == Variant::PACKED_BYTE_ARRAY) { - const PackedByteArray pba = p_data; - memcpy(&ptr[SPAWN_CMD_OFFSET], pba.ptr(), pba.size()); - } else if (data_size) { - encode_variant(p_data, &ptr[SPAWN_CMD_OFFSET], data_size); - } - Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); - peer->set_target_peer(p_peer_id); - peer->set_transfer_channel(0); - peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_RELIABLE); - return peer->put_packet(ptr, SPAWN_CMD_OFFSET + data_size); -} - -Error MultiplayerReplicator::send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) { - ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); - ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - if (cfg.on_spawn_despawn_send.is_valid()) { - return _send_spawn_despawn(p_peer_id, p_scene_id, p_data, true); - } else { - ERR_FAIL_COND_V_MSG(cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_server(), ERR_UNAVAILABLE, "Manual despawn is restricted in default server mode implementation. Use custom mode if you desire control over server spawn requests."); - NodePath path = p_path; - Object *obj = p_data.get_type() == Variant::OBJECT ? p_data.get_validated_object() : nullptr; - if (path.is_empty() && obj) { - Node *node = Object::cast_to<Node>(obj); - if (node && node->is_inside_tree()) { - path = node->get_path(); - } - } - ERR_FAIL_COND_V_MSG(path.is_empty(), ERR_INVALID_PARAMETER, "Despawn default implementation requires a despawn path, or the data to be a node inside the SceneTree"); - return _send_default_spawn_despawn(p_peer_id, p_scene_id, obj, path, false); - } -} - -Error MultiplayerReplicator::send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) { - ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); - ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - if (cfg.on_spawn_despawn_send.is_valid()) { - return _send_spawn_despawn(p_peer_id, p_scene_id, p_data, false); - } else { - ERR_FAIL_COND_V_MSG(cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_server(), ERR_UNAVAILABLE, "Manual spawn is restricted in default server mode implementation. Use custom mode if you desire control over server spawn requests."); - NodePath path = p_path; - Object *obj = p_data.get_type() == Variant::OBJECT ? p_data.get_validated_object() : nullptr; - ERR_FAIL_COND_V_MSG(!obj, ERR_INVALID_PARAMETER, "Spawn default implementation requires the data to be an object."); - if (path.is_empty()) { - Node *node = Object::cast_to<Node>(obj); - if (node && node->is_inside_tree()) { - path = node->get_path(); - } - } - ERR_FAIL_COND_V_MSG(path.is_empty(), ERR_INVALID_PARAMETER, "Spawn default implementation requires a spawn path, or the data to be a node inside the SceneTree"); - return _send_default_spawn_despawn(p_peer_id, p_scene_id, obj, path, true); - } -} - -Error MultiplayerReplicator::_spawn_despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer, bool p_spawn) { - ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id)); - - const SceneConfig &cfg = replications[p_scene_id]; - if (cfg.on_spawn_despawn_send.is_valid()) { - Variant args[4]; - args[0] = p_peer; - args[1] = p_scene_id; - args[2] = p_obj; - args[3] = p_spawn; - const Variant *argp[] = { &args[0], &args[1], &args[2], &args[3] }; - Callable::CallError ce; - Variant ret; - cfg.on_spawn_despawn_send.call(argp, 4, ret, ce); - ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, "Custom send function failed"); - return OK; - } else { - Node *node = Object::cast_to<Node>(p_obj); - ERR_FAIL_COND_V_MSG(!p_obj, ERR_INVALID_PARAMETER, "Only nodes can be replicated by the default implementation"); - return _send_default_spawn_despawn(p_peer, p_scene_id, node, node->get_path(), p_spawn); - } -} - -Error MultiplayerReplicator::spawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer) { - return _spawn_despawn(p_scene_id, p_obj, p_peer, true); -} - -Error MultiplayerReplicator::despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer) { - return _spawn_despawn(p_scene_id, p_obj, p_peer, false); -} - -PackedByteArray MultiplayerReplicator::encode_state(const ResourceUID::ID &p_scene_id, const Object *p_obj, bool p_initial) { - PackedByteArray state; - ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), state, vformat("Spawnable not found: %d", p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - int len = 0; - List<Variant> state_vars; - const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties; - Error err = _get_state(props, p_obj, state_vars); - ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to retrieve object state."); - err = _encode_state(state_vars, nullptr, len); - ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to encode object state."); - state.resize(len); - _encode_state(state_vars, state.ptrw(), len); - return state; -} - -Error MultiplayerReplicator::decode_state(const ResourceUID::ID &p_scene_id, Object *p_obj, const PackedByteArray p_data, bool p_initial) { - ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties; - int size; - return _decode_state(props, p_obj, p_data.ptr(), p_data.size(), size); -} - -void MultiplayerReplicator::scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter) { - if (!multiplayer->has_multiplayer_peer()) { - return; - } - Node *root_node = multiplayer->get_root_node(); - ERR_FAIL_COND(!p_node || !p_node->get_parent() || !root_node); - NodePath path = (root_node->get_path()).rel_path_to(p_node->get_parent()->get_path()); - if (path.is_empty()) { - return; - } - ResourceUID::ID id = ResourceLoader::get_resource_uid(p_scene); - if (!replications.has(id)) { - return; - } - const SceneConfig &cfg = replications[id]; - if (p_enter) { - if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_server()) { - replicated_nodes[p_node->get_instance_id()] = id; - _track(id, p_node); - spawn(id, p_node, 0); - } - emit_signal(SNAME("replicated_instance_added"), id, p_node); - } else { - if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_server() && replicated_nodes.has(p_node->get_instance_id())) { - replicated_nodes.erase(p_node->get_instance_id()); - _untrack(id, p_node); - despawn(id, p_node, 0); - } - emit_signal(SNAME("replicated_instance_removed"), id, p_node); - } -} - -void MultiplayerReplicator::spawn_all(int p_peer) { - for (const KeyValue<ObjectID, ResourceUID::ID> &E : replicated_nodes) { - // Only server mode adds to replicated_nodes, no need to check it. - Object *obj = ObjectDB::get_instance(E.key); - ERR_CONTINUE(!obj); - Node *node = Object::cast_to<Node>(obj); - ERR_CONTINUE(!node); - spawn(E.value, node, p_peer); - } -} - -void MultiplayerReplicator::poll() { - for (KeyValue<ResourceUID::ID, SceneConfig> &E : replications) { - if (!E.value.sync_interval) { - continue; - } - if (E.value.mode == REPLICATION_MODE_SERVER && !multiplayer->is_server()) { - continue; - } - uint64_t time = OS::get_singleton()->get_ticks_usec(); - if (E.value.sync_last + E.value.sync_interval <= time) { - sync_all(E.key, 0); - E.value.sync_last = time; - } - // Handle wrapping. - if (E.value.sync_last > time) { - E.value.sync_last = time; - } - } -} - -void MultiplayerReplicator::track(const ResourceUID::ID &p_scene_id, Object *p_obj) { - ERR_FAIL_COND(!replications.has(p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode."); - _track(p_scene_id, p_obj); -} - -void MultiplayerReplicator::_track(const ResourceUID::ID &p_scene_id, Object *p_obj) { - ERR_FAIL_COND(!p_obj); - ERR_FAIL_COND(!replications.has(p_scene_id)); - if (!tracked_objects.has(p_scene_id)) { - tracked_objects[p_scene_id] = List<ObjectID>(); - } - tracked_objects[p_scene_id].push_back(p_obj->get_instance_id()); -} - -void MultiplayerReplicator::untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) { - ERR_FAIL_COND(!replications.has(p_scene_id)); - const SceneConfig &cfg = replications[p_scene_id]; - ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode."); - _untrack(p_scene_id, p_obj); -} - -void MultiplayerReplicator::_untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) { - ERR_FAIL_COND(!p_obj); - ERR_FAIL_COND(!replications.has(p_scene_id)); - if (tracked_objects.has(p_scene_id)) { - tracked_objects[p_scene_id].erase(p_obj->get_instance_id()); - } -} - -Error MultiplayerReplicator::sync_all(const ResourceUID::ID &p_scene_id, int p_peer) { - ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER); - if (!tracked_objects.has(p_scene_id)) { - return OK; - } - const SceneConfig &cfg = replications[p_scene_id]; - if (cfg.on_sync_send.is_valid()) { - Array objs; - if (tracked_objects.has(p_scene_id)) { - objs.resize(tracked_objects[p_scene_id].size()); - int idx = 0; - for (const ObjectID &obj_id : tracked_objects[p_scene_id]) { - objs[idx++] = ObjectDB::get_instance(obj_id); - } - } - Variant args[3] = { p_scene_id, objs, p_peer }; - Variant *argp[3] = { args, &args[1], &args[2] }; - Callable::CallError ce; - Variant ret; - cfg.on_sync_send.call((const Variant **)argp, 3, ret, ce); - ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, "Custom sync function failed"); - return OK; - } else if (cfg.sync_properties.size()) { - return _sync_all_default(p_scene_id, p_peer); - } - return OK; -} - -Error MultiplayerReplicator::send_sync(int p_peer_id, const ResourceUID::ID &p_scene_id, PackedByteArray p_data, Multiplayer::TransferMode p_transfer_mode, int p_channel) { - ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); - ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER); - const SceneConfig &cfg = replications[p_scene_id]; - ERR_FAIL_COND_V_MSG(!cfg.on_sync_send.is_valid(), ERR_UNCONFIGURED, "Sending raw sync messages is only available with custom functions"); - MAKE_ROOM(SYNC_CMD_OFFSET + p_data.size()); - uint8_t *ptr = packet_cache.ptrw(); - ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC; - encode_uint64(p_scene_id, &ptr[1]); - if (p_data.size()) { - memcpy(&ptr[SYNC_CMD_OFFSET], p_data.ptr(), p_data.size()); - } - Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); - peer->set_target_peer(p_peer_id); - peer->set_transfer_channel(p_channel); - peer->set_transfer_mode(p_transfer_mode); - return peer->put_packet(ptr, SYNC_CMD_OFFSET + p_data.size()); -} - -void MultiplayerReplicator::clear() { - tracked_objects.clear(); - replicated_nodes.clear(); -} - -void MultiplayerReplicator::_bind_methods() { - ClassDB::bind_method(D_METHOD("spawn_config", "scene_id", "spawn_mode", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::spawn_config, DEFVAL(TypedArray<StringName>()), DEFVAL(Callable()), DEFVAL(Callable())); - ClassDB::bind_method(D_METHOD("sync_config", "scene_id", "interval", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::sync_config, DEFVAL(TypedArray<StringName>()), DEFVAL(Callable()), DEFVAL(Callable())); - ClassDB::bind_method(D_METHOD("despawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::despawn, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("spawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::spawn, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("send_despawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_despawn, DEFVAL(Variant()), DEFVAL(NodePath())); - ClassDB::bind_method(D_METHOD("send_spawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_spawn, DEFVAL(Variant()), DEFVAL(NodePath())); - ClassDB::bind_method(D_METHOD("send_sync", "peer_id", "scene_id", "data", "transfer_mode", "channel"), &MultiplayerReplicator::send_sync, DEFVAL(Multiplayer::TRANSFER_MODE_RELIABLE), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("sync_all", "scene_id", "peer_id"), &MultiplayerReplicator::sync_all, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("track", "scene_id", "object"), &MultiplayerReplicator::track); - ClassDB::bind_method(D_METHOD("untrack", "scene_id", "object"), &MultiplayerReplicator::untrack); - ClassDB::bind_method(D_METHOD("encode_state", "scene_id", "object", "initial"), &MultiplayerReplicator::encode_state, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("decode_state", "scene_id", "object", "data", "initial"), &MultiplayerReplicator::decode_state, DEFVAL(true)); - - ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - ADD_SIGNAL(MethodInfo("spawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - ADD_SIGNAL(MethodInfo("despawn_requested", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); - ADD_SIGNAL(MethodInfo("spawn_requested", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); - ADD_SIGNAL(MethodInfo("replicated_instance_added", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - ADD_SIGNAL(MethodInfo("replicated_instance_removed", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - - BIND_ENUM_CONSTANT(REPLICATION_MODE_NONE); - BIND_ENUM_CONSTANT(REPLICATION_MODE_SERVER); - BIND_ENUM_CONSTANT(REPLICATION_MODE_CUSTOM); -} diff --git a/core/multiplayer/multiplayer_replicator.h b/core/multiplayer/multiplayer_replicator.h deleted file mode 100644 index a9cd6e211e..0000000000 --- a/core/multiplayer/multiplayer_replicator.h +++ /dev/null @@ -1,138 +0,0 @@ -/*************************************************************************/ -/* multiplayer_replicator.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 MULTIPLAYER_REPLICATOR_H -#define MULTIPLAYER_REPLICATOR_H - -#include "core/multiplayer/multiplayer_api.h" - -#include "core/io/resource_uid.h" -#include "core/templates/hash_map.h" -#include "core/variant/typed_array.h" - -class MultiplayerReplicator : public Object { - GDCLASS(MultiplayerReplicator, Object); - -public: - enum { - SPAWN_CMD_OFFSET = 9, - SYNC_CMD_OFFSET = 9, - }; - - enum ReplicationMode { - REPLICATION_MODE_NONE, - REPLICATION_MODE_SERVER, - REPLICATION_MODE_CUSTOM, - }; - - struct SceneConfig { - ReplicationMode mode; - uint64_t sync_interval = 0; - uint64_t sync_last = 0; - uint8_t sync_recv = 0; - List<StringName> properties; - List<StringName> sync_properties; - Callable on_spawn_despawn_send; - Callable on_spawn_despawn_receive; - Callable on_sync_send; - Callable on_sync_receive; - }; - -protected: - static void _bind_methods(); - -private: - enum { - BYTE_OR_ZERO_SHIFT = MultiplayerAPI::CMD_FLAG_0_SHIFT, - }; - - enum { - BYTE_OR_ZERO_FLAG = 1 << BYTE_OR_ZERO_SHIFT, - }; - - MultiplayerAPI *multiplayer = nullptr; - Vector<uint8_t> packet_cache; - Map<ResourceUID::ID, SceneConfig> replications; - Map<ObjectID, ResourceUID::ID> replicated_nodes; - HashMap<ResourceUID::ID, List<ObjectID>> tracked_objects; - - // Encoding - Error _get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant); - Error _encode_state(const List<Variant> &p_variants, uint8_t *p_buffer, int &r_len, bool *r_raw = nullptr); - Error _decode_state(const List<StringName> &p_cfg, Object *p_obj, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw = false); - - // Spawn - Error _spawn_despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer, bool p_spawn); - Error _send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn); - void _process_default_spawn_despawn(int p_from, const ResourceUID::ID &p_scene_id, const uint8_t *p_packet, int p_packet_len, bool p_spawn); - Error _send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn); - - // Sync - void _process_default_sync(const ResourceUID::ID &p_id, const uint8_t *p_packet, int p_packet_len); - Error _sync_all_default(const ResourceUID::ID &p_scene_id, int p_peer); - void _track(const ResourceUID::ID &p_scene_id, Object *p_object); - void _untrack(const ResourceUID::ID &p_scene_id, Object *p_object); - -public: - void clear(); - - // Encoding - PackedByteArray encode_state(const ResourceUID::ID &p_scene_id, const Object *p_node, bool p_initial); - Error decode_state(const ResourceUID::ID &p_scene_id, Object *p_node, PackedByteArray p_data, bool p_initial); - - // Spawn - Error spawn_config(const ResourceUID::ID &p_id, ReplicationMode p_mode, const TypedArray<StringName> &p_props = TypedArray<StringName>(), const Callable &p_on_send = Callable(), const Callable &p_on_recv = Callable()); - Error spawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0); - Error despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0); - Error send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath()); - Error send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath()); - - // Sync - Error sync_config(const ResourceUID::ID &p_id, uint64_t p_interval, const TypedArray<StringName> &p_props = TypedArray<StringName>(), const Callable &p_on_send = Callable(), const Callable &p_on_recv = Callable()); - Error sync_all(const ResourceUID::ID &p_scene_id, int p_peer); - Error send_sync(int p_peer_id, const ResourceUID::ID &p_scene_id, PackedByteArray p_data, Multiplayer::TransferMode p_mode, int p_channel); - void track(const ResourceUID::ID &p_scene_id, Object *p_object); - void untrack(const ResourceUID::ID &p_scene_id, Object *p_object); - - // Used by MultiplayerAPI - void spawn_all(int p_peer); - void process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn); - void process_sync(int p_from, const uint8_t *p_packet, int p_packet_len); - void scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter); - void poll(); - - MultiplayerReplicator(MultiplayerAPI *p_multiplayer) { - multiplayer = p_multiplayer; - } -}; - -VARIANT_ENUM_CAST(MultiplayerReplicator::ReplicationMode); - -#endif // MULTIPLAYER_REPLICATOR_H diff --git a/core/multiplayer/rpc_manager.cpp b/core/multiplayer/rpc_manager.cpp index 7736637349..1e6d2108be 100644 --- a/core/multiplayer/rpc_manager.cpp +++ b/core/multiplayer/rpc_manager.cpp @@ -235,16 +235,12 @@ void RPCManager::_process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int ERR_FAIL_COND_MSG(!can_call, "RPC '" + String(config.name) + "' is not allowed on node " + p_node->get_path() + " from: " + itos(p_from) + ". Mode is " + itos((int)config.rpc_mode) + ", authority is " + itos(p_node->get_multiplayer_authority()) + "."); int argc = 0; - bool byte_only = false; const bool byte_only_or_no_args = p_packet[0] & BYTE_ONLY_OR_NO_ARGS_FLAG; if (byte_only_or_no_args) { if (p_offset < p_packet_len) { // This packet contains only bytes. argc = 1; - byte_only = true; - } else { - // This rpc calls a method without parameters. } } else { // Normal variant, takes the argument count from the packet. @@ -262,25 +258,10 @@ void RPCManager::_process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int _profile_node_data("in_rpc", p_node->get_instance_id()); #endif - if (byte_only) { - Vector<uint8_t> pure_data; - const int len = p_packet_len - p_offset; - pure_data.resize(len); - memcpy(pure_data.ptrw(), &p_packet[p_offset], len); - args.write[0] = pure_data; - argp.write[0] = &args[0]; - p_offset += len; - } else { - for (int i = 0; i < argc; i++) { - ERR_FAIL_COND_MSG(p_offset >= p_packet_len, "Invalid packet received. Size too small."); - - int vlen; - Error err = multiplayer->decode_and_decompress_variant(args.write[i], &p_packet[p_offset], p_packet_len - p_offset, &vlen); - ERR_FAIL_COND_MSG(err != OK, "Invalid packet received. Unable to decode RPC argument."); - - argp.write[i] = &args[i]; - p_offset += vlen; - } + int out; + MultiplayerAPI::decode_and_decompress_variants(args, &p_packet[p_offset], p_packet_len - p_offset, out, byte_only_or_no_args, multiplayer->is_object_decoding_allowed()); + for (int i = 0; i < argc; i++) { + argp.write[i] = &args[i]; } Callable::CallError ce; @@ -380,28 +361,19 @@ void RPCManager::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const Mult ofs += 2; } - if (p_argcount == 0) { - byte_only_or_no_args = true; - } else if (p_argcount == 1 && p_arg[0]->get_type() == Variant::PACKED_BYTE_ARRAY) { - byte_only_or_no_args = true; - // Special optimization when only the byte vector is sent. - const Vector<uint8_t> data = *p_arg[0]; - MAKE_ROOM(ofs + data.size()); - memcpy(&(packet_cache.write[ofs]), data.ptr(), sizeof(uint8_t) * data.size()); - ofs += data.size(); + int len; + Error err = MultiplayerAPI::encode_and_compress_variants(p_arg, p_argcount, nullptr, len, &byte_only_or_no_args, multiplayer->is_object_decoding_allowed()); + ERR_FAIL_COND_MSG(err != OK, "Unable to encode RPC arguments. THIS IS LIKELY A BUG IN THE ENGINE!"); + if (byte_only_or_no_args) { + MAKE_ROOM(ofs + len); } else { - // Arguments - MAKE_ROOM(ofs + 1); + MAKE_ROOM(ofs + 1 + len); packet_cache.write[ofs] = p_argcount; ofs += 1; - for (int i = 0; i < p_argcount; i++) { - int len(0); - Error err = multiplayer->encode_and_compress_variant(*p_arg[i], nullptr, len); - ERR_FAIL_COND_MSG(err != OK, "Unable to encode RPC argument. THIS IS LIKELY A BUG IN THE ENGINE!"); - MAKE_ROOM(ofs + len); - multiplayer->encode_and_compress_variant(*p_arg[i], &(packet_cache.write[ofs]), len); - ofs += len; - } + } + if (len) { + MultiplayerAPI::encode_and_compress_variants(p_arg, p_argcount, &packet_cache.write[ofs], len, &byte_only_or_no_args, multiplayer->is_object_decoding_allowed()); + ofs += len; } ERR_FAIL_COND(command_type > 7); diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index a18ec4d6ad..388368d181 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -69,7 +69,6 @@ #include "core/math/triangle_mesh.h" #include "core/multiplayer/multiplayer_api.h" #include "core/multiplayer/multiplayer_peer.h" -#include "core/multiplayer/multiplayer_replicator.h" #include "core/object/class_db.h" #include "core/object/undo_redo.h" #include "core/os/main_loop.h" @@ -200,7 +199,6 @@ void register_core_types() { GDREGISTER_VIRTUAL_CLASS(MultiplayerPeer); GDREGISTER_CLASS(MultiplayerPeerExtension); - GDREGISTER_VIRTUAL_CLASS(MultiplayerReplicator); GDREGISTER_CLASS(MultiplayerAPI); GDREGISTER_CLASS(MainLoop); GDREGISTER_CLASS(Translation); diff --git a/core/string/char_utils.h b/core/string/char_utils.h new file mode 100644 index 0000000000..0afd058f01 --- /dev/null +++ b/core/string/char_utils.h @@ -0,0 +1,92 @@ +/*************************************************************************/ +/* char_utils.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 CHAR_UTILS_H +#define CHAR_UTILS_H + +#include "core/typedefs.h" + +static _FORCE_INLINE_ bool is_ascii_upper_case(char32_t c) { + return (c >= 'A' && c <= 'Z'); +} + +static _FORCE_INLINE_ bool is_ascii_lower_case(char32_t c) { + return (c >= 'a' && c <= 'z'); +} + +static _FORCE_INLINE_ bool is_digit(char32_t c) { + return (c >= '0' && c <= '9'); +} + +static _FORCE_INLINE_ bool is_hex_digit(char32_t c) { + return (is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); +} + +static _FORCE_INLINE_ bool is_binary_digit(char32_t c) { + return (c == '0' || c == '1'); +} + +static _FORCE_INLINE_ bool is_ascii_char(char32_t c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +static _FORCE_INLINE_ bool is_ascii_alphanumeric_char(char32_t c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); +} + +static _FORCE_INLINE_ bool is_ascii_identifier_char(char32_t c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +static _FORCE_INLINE_ bool is_symbol(char32_t c) { + return c != '_' && ((c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') || (c >= '{' && c <= '~') || c == '\t' || c == ' '); +} + +static _FORCE_INLINE_ bool is_control(char32_t p_char) { + return (p_char <= 0x001f) || (p_char >= 0x007f && p_char <= 0x009f); +} + +static _FORCE_INLINE_ bool is_whitespace(char32_t p_char) { + return (p_char == ' ') || (p_char == 0x00a0) || (p_char == 0x1680) || (p_char >= 0x2000 && p_char <= 0x200a) || (p_char == 0x202f) || (p_char == 0x205f) || (p_char == 0x3000) || (p_char == 0x2028) || (p_char == 0x2029) || (p_char >= 0x0009 && p_char <= 0x000d) || (p_char == 0x0085); +} + +static _FORCE_INLINE_ bool is_linebreak(char32_t p_char) { + return (p_char >= 0x000a && p_char <= 0x000d) || (p_char == 0x0085) || (p_char == 0x2028) || (p_char == 0x2029); +} + +static _FORCE_INLINE_ bool is_punct(char32_t p_char) { + return (p_char >= ' ' && p_char <= '/') || (p_char >= ':' && p_char <= '@') || (p_char >= '[' && p_char <= '^') || (p_char == '`') || (p_char >= '{' && p_char <= '~') || (p_char >= 0x2000 && p_char <= 0x206f) || (p_char >= 0x3000 && p_char <= 0x303f); +} + +static _FORCE_INLINE_ bool is_underscore(char32_t p_char) { + return (p_char == '_'); +} + +#endif // CHAR_UTILS_H diff --git a/core/string/translation.cpp b/core/string/translation.cpp index 355ee238e8..7cc41df9ef 100644 --- a/core/string/translation.cpp +++ b/core/string/translation.cpp @@ -213,14 +213,6 @@ static _character_accent_pair _character_to_accented[] = { { 'z', U"ź" }, }; -static _FORCE_INLINE_ bool is_upper_case(char32_t c) { - return (c >= 'A' && c <= 'Z'); -} - -static _FORCE_INLINE_ bool is_lower_case(char32_t c) { - return (c >= 'a' && c <= 'z'); -} - Vector<TranslationServer::LocaleScriptInfo> TranslationServer::locale_script_info; Map<String, String> TranslationServer::language_map; @@ -309,15 +301,15 @@ String TranslationServer::standardize_locale(const String &p_locale) const { Vector<String> locale_elements = univ_locale.get_slice("@", 0).split("_"); lang = locale_elements[0]; if (locale_elements.size() >= 2) { - if (locale_elements[1].length() == 4 && is_upper_case(locale_elements[1][0]) && is_lower_case(locale_elements[1][1]) && is_lower_case(locale_elements[1][2]) && is_lower_case(locale_elements[1][3])) { + if (locale_elements[1].length() == 4 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_lower_case(locale_elements[1][1]) && is_ascii_lower_case(locale_elements[1][2]) && is_ascii_lower_case(locale_elements[1][3])) { script = locale_elements[1]; } - if (locale_elements[1].length() == 2 && is_upper_case(locale_elements[1][0]) && is_upper_case(locale_elements[1][1])) { + if (locale_elements[1].length() == 2 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_upper_case(locale_elements[1][1])) { country = locale_elements[1]; } } if (locale_elements.size() >= 3) { - if (locale_elements[2].length() == 2 && is_upper_case(locale_elements[2][0]) && is_upper_case(locale_elements[2][1])) { + if (locale_elements[2].length() == 2 && is_ascii_upper_case(locale_elements[2][0]) && is_ascii_upper_case(locale_elements[2][1])) { country = locale_elements[2]; } else if (variant_map.has(locale_elements[2].to_lower()) && variant_map[locale_elements[2].to_lower()] == lang) { variant = locale_elements[2].to_lower(); @@ -434,15 +426,15 @@ String TranslationServer::get_locale_name(const String &p_locale) const { Vector<String> locale_elements = locale.split("_"); lang = locale_elements[0]; if (locale_elements.size() >= 2) { - if (locale_elements[1].length() == 4 && is_upper_case(locale_elements[1][0]) && is_lower_case(locale_elements[1][1]) && is_lower_case(locale_elements[1][2]) && is_lower_case(locale_elements[1][3])) { + if (locale_elements[1].length() == 4 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_lower_case(locale_elements[1][1]) && is_ascii_lower_case(locale_elements[1][2]) && is_ascii_lower_case(locale_elements[1][3])) { script = locale_elements[1]; } - if (locale_elements[1].length() == 2 && is_upper_case(locale_elements[1][0]) && is_upper_case(locale_elements[1][1])) { + if (locale_elements[1].length() == 2 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_upper_case(locale_elements[1][1])) { country = locale_elements[1]; } } if (locale_elements.size() >= 3) { - if (locale_elements[2].length() == 2 && is_upper_case(locale_elements[2][0]) && is_upper_case(locale_elements[2][1])) { + if (locale_elements[2].length() == 2 && is_ascii_upper_case(locale_elements[2][0]) && is_ascii_upper_case(locale_elements[2][1])) { country = locale_elements[2]; } } @@ -911,7 +903,7 @@ String TranslationServer::add_padding(String &p_message, int p_length) const { } const char32_t *TranslationServer::get_accented_version(char32_t p_character) const { - if (!((p_character >= 'a' && p_character <= 'z') || (p_character >= 'A' && p_character <= 'Z'))) { + if (!is_ascii_char(p_character)) { return nullptr; } diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 6e0a7c7022..c4edc8c086 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -54,34 +54,14 @@ static const int MAX_DECIMALS = 32; -static _FORCE_INLINE_ bool is_digit(char32_t c) { - return (c >= '0' && c <= '9'); -} - -static _FORCE_INLINE_ bool is_hex_digit(char32_t c) { - return (is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); -} - -static _FORCE_INLINE_ bool is_upper_case(char32_t c) { - return (c >= 'A' && c <= 'Z'); -} - -static _FORCE_INLINE_ bool is_lower_case(char32_t c) { - return (c >= 'a' && c <= 'z'); -} - static _FORCE_INLINE_ char32_t lower_case(char32_t c) { - return (is_upper_case(c) ? (c + ('a' - 'A')) : c); + return (is_ascii_upper_case(c) ? (c + ('a' - 'A')) : c); } const char CharString::_null = 0; const char16_t Char16String::_null = 0; const char32_t String::_null = 0; -bool is_symbol(char32_t c) { - return c != '_' && ((c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') || (c >= '{' && c <= '~') || c == '\t' || c == ' '); -} - bool select_word(const String &p_s, int p_col, int &r_beg, int &r_end) { const String &s = p_s; int beg = CLAMP(p_col, 0, s.length()); @@ -974,21 +954,21 @@ String String::camelcase_to_underscore(bool lowercase) const { int start_index = 0; for (int i = 1; i < this->size(); i++) { - bool is_upper = is_upper_case(cstr[i]); + bool is_upper = is_ascii_upper_case(cstr[i]); bool is_number = is_digit(cstr[i]); bool are_next_2_lower = false; bool is_next_lower = false; bool is_next_number = false; - bool was_precedent_upper = is_upper_case(cstr[i - 1]); + bool was_precedent_upper = is_ascii_upper_case(cstr[i - 1]); bool was_precedent_number = is_digit(cstr[i - 1]); if (i + 2 < this->size()) { - are_next_2_lower = is_lower_case(cstr[i + 1]) && is_lower_case(cstr[i + 2]); + are_next_2_lower = is_ascii_lower_case(cstr[i + 1]) && is_ascii_lower_case(cstr[i + 2]); } if (i + 1 < this->size()) { - is_next_lower = is_lower_case(cstr[i + 1]); + is_next_lower = is_ascii_lower_case(cstr[i + 1]); is_next_number = is_digit(cstr[i + 1]); } @@ -2212,7 +2192,7 @@ bool String::is_numeric() const { return false; } dot = true; - } else if (c < '0' || c > '9') { + } else if (!is_digit(c)) { return false; } } @@ -3691,7 +3671,7 @@ bool String::is_valid_identifier() const { } } - bool valid_char = is_digit(str[i]) || is_lower_case(str[i]) || is_upper_case(str[i]) || str[i] == '_'; + bool valid_char = is_ascii_identifier_char(str[i]); if (!valid_char) { return false; @@ -3716,7 +3696,7 @@ String String::uri_encode() const { String res; for (int i = 0; i < temp.length(); ++i) { char ord = temp[i]; - if (ord == '.' || ord == '-' || ord == '_' || ord == '~' || is_lower_case(ord) || is_upper_case(ord) || is_digit(ord)) { + if (ord == '.' || ord == '-' || ord == '~' || is_ascii_identifier_char(ord)) { res += ord; } else { char h_Val[3]; @@ -3738,9 +3718,9 @@ String String::uri_decode() const { for (int i = 0; i < src.length(); ++i) { if (src[i] == '%' && i + 2 < src.length()) { char ord1 = src[i + 1]; - if (is_digit(ord1) || is_upper_case(ord1)) { + if (is_digit(ord1) || is_ascii_upper_case(ord1)) { char ord2 = src[i + 2]; - if (is_digit(ord2) || is_upper_case(ord2)) { + if (is_digit(ord2) || is_ascii_upper_case(ord2)) { char bytes[3] = { (char)ord1, (char)ord2, 0 }; res += (char)strtol(bytes, nullptr, 16); i += 2; @@ -3867,7 +3847,7 @@ static _FORCE_INLINE_ int _xml_unescape(const char32_t *p_src, int p_src_len, ch for (int i = 2; i < p_src_len; i++) { eat = i + 1; char32_t ct = p_src[i]; - if (ct == ';' || ct < '0' || ct > '9') { + if (ct == ';' || !is_digit(ct)) { break; } } @@ -3997,7 +3977,7 @@ String String::pad_zeros(int p_digits) const { int begin = 0; - while (begin < end && (s[begin] < '0' || s[begin] > '9')) { + while (begin < end && !is_digit(s[begin])) { begin++; } @@ -4042,7 +4022,7 @@ bool String::is_valid_int() const { } for (int i = from; i < len; i++) { - if (operator[](i) < '0' || operator[](i) > '9') { + if (!is_digit(operator[](i))) { return false; // no start with number plz } } diff --git a/core/string/ustring.h b/core/string/ustring.h index b685e3929f..1d302b65a7 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -32,6 +32,7 @@ #define USTRING_GODOT_H // Note: Renamed to avoid conflict with ICU header with the same name. +#include "core/string/char_utils.h" #include "core/templates/cowdata.h" #include "core/templates/vector.h" #include "core/typedefs.h" @@ -533,7 +534,6 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St String RTR(const String &p_text, const String &p_context = ""); String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = ""); -bool is_symbol(char32_t c); bool select_word(const String &p_s, int p_col, int &r_beg, int &r_end); _FORCE_INLINE_ void sarray_add_str(Vector<String> &arr) { diff --git a/core/variant/method_ptrcall.h b/core/variant/method_ptrcall.h index 75a93ac4c8..d0acf60c22 100644 --- a/core/variant/method_ptrcall.h +++ b/core/variant/method_ptrcall.h @@ -31,7 +31,6 @@ #ifndef METHOD_PTRCALL_H #define METHOD_PTRCALL_H -#include "core/math/transform_2d.h" #include "core/object/object_id.h" #include "core/typedefs.h" #include "core/variant/variant.h" diff --git a/core/variant/variant.h b/core/variant/variant.h index 36fa755647..b75882a87c 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -39,8 +39,12 @@ #include "core/math/face3.h" #include "core/math/plane.h" #include "core/math/quaternion.h" +#include "core/math/rect2.h" +#include "core/math/rect2i.h" #include "core/math/transform_2d.h" #include "core/math/transform_3d.h" +#include "core/math/vector2.h" +#include "core/math/vector2i.h" #include "core/math/vector3.h" #include "core/math/vector3i.h" #include "core/object/object_id.h" diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp index 55fc9212b7..e889a1bb40 100644 --- a/core/variant/variant_parser.cpp +++ b/core/variant/variant_parser.cpp @@ -188,7 +188,7 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri if (p_stream->is_eof()) { r_token.type = TK_EOF; return OK; - } else if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) { + } else if (is_hex_digit(ch)) { color_str += ch; } else { @@ -265,13 +265,13 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + if (!is_hex_digit(c)) { r_err_str = "Malformed hex constant in string"; r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } char32_t v; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a'; @@ -381,7 +381,7 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri while (true) { switch (reading) { case READING_INT: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { //pass } else if (c == '.') { reading = READING_DEC; @@ -395,7 +395,7 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri } break; case READING_DEC: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { } else if (c == 'e') { reading = READING_EXP; } else { @@ -404,7 +404,7 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri } break; case READING_EXP: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { exp_beg = true; } else if ((c == '-' || c == '+') && !exp_sign && !exp_beg) { @@ -433,11 +433,11 @@ Error VariantParser::get_token(Stream *p_stream, Token &r_token, int &line, Stri r_token.value = num.as_int(); } return OK; - } else if ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_') { + } else if (is_ascii_char(cchar) || is_underscore(cchar)) { StringBuffer<> id; bool first = true; - while ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_' || (!first && cchar >= '0' && cchar <= '9')) { + while (is_ascii_char(cchar) || is_underscore(cchar) || (!first && is_digit(cchar))) { id += cchar; cchar = p_stream->get_char(); first = false; diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 8be944b105..281c218d0d 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -399,6 +399,14 @@ <description> </description> </method> + <method name="screen_get_refresh_rate" qualifiers="const"> + <return type="float" /> + <argument index="0" name="screen" type="int" default="-1" /> + <description> + Returns the current refresh rate of the specified screen. If [code]screen[/code] is [code]SCREEN_OF_MAIN_WINDOW[/code] (the default value), a screen with the main window will be used. + [b]Note:[/b] Returns [code]60.0[/code] if the DisplayServer fails to find the refresh rate for the specified screen. On HTML5, [method screen_get_refresh_rate] will always return [code]60.0[/code] as there is no way to retrieve the refresh rate on that platform. + </description> + </method> <method name="screen_get_scale" qualifiers="const"> <return type="float" /> <argument index="0" name="screen" type="int" default="-1" /> @@ -900,6 +908,11 @@ Fullscreen window mode. Note that this is not [i]exclusive[/i] fullscreen. On Windows and Linux, a borderless window is used to emulate fullscreen. On macOS, a new desktop is used to display the running project. Regardless of the platform, enabling fullscreen will change the window size to match the monitor's size. Therefore, make sure your project supports [url=$DOCS_URL/tutorials/rendering/multiple_resolutions.html]multiple resolutions[/url] when enabling fullscreen mode. </constant> + <constant name="WINDOW_MODE_EXCLUSIVE_FULLSCREEN" value="4" enum="WindowMode"> + Exclusive fullscreen window mode. This mode is implemented on Windows only. On other platforms, it is equivalent to [constant WINDOW_MODE_FULLSCREEN]. + Only one window in exclusive fullscreen mode can be visible on a given screen at a time. If multiple windows are in exclusive fullscreen mode for the same screen, the last one being set to this mode takes precedence. + Regardless of the platform, enabling fullscreen will change the window size to match the monitor's size. Therefore, make sure your project supports [url=$DOCS_URL/tutorials/rendering/multiple_resolutions.html]multiple resolutions[/url] when enabling fullscreen mode. + </constant> <constant name="WINDOW_FLAG_RESIZE_DISABLED" value="0" enum="WindowFlags"> </constant> <constant name="WINDOW_FLAG_BORDERLESS" value="1" enum="WindowFlags"> diff --git a/doc/classes/MultiplayerAPI.xml b/doc/classes/MultiplayerAPI.xml index e0da08f5bd..426d902983 100644 --- a/doc/classes/MultiplayerAPI.xml +++ b/doc/classes/MultiplayerAPI.xml @@ -79,8 +79,6 @@ <member name="refuse_new_connections" type="bool" setter="set_refuse_new_connections" getter="is_refusing_new_connections" default="false"> If [code]true[/code], the MultiplayerAPI's [member multiplayer_peer] refuses new incoming connections. </member> - <member name="replicator" type="MultiplayerReplicator" setter="" getter="get_replicator"> - </member> <member name="root_node" type="Node" setter="set_root_node" getter="get_root_node"> The root node to use for RPCs. Instead of an absolute path, a relative path will be used to find the node upon which the RPC should be executed. This effectively allows to have different branches of the scene tree to be managed by different MultiplayerAPI, allowing for example to run both client and server in the same scene. diff --git a/doc/classes/MultiplayerReplicator.xml b/doc/classes/MultiplayerReplicator.xml deleted file mode 100644 index c2e93ddeab..0000000000 --- a/doc/classes/MultiplayerReplicator.xml +++ /dev/null @@ -1,191 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="MultiplayerReplicator" inherits="Object" version="4.0"> - <brief_description> - </brief_description> - <description> - </description> - <tutorials> - </tutorials> - <methods> - <method name="decode_state"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <argument index="2" name="data" type="PackedByteArray" /> - <argument index="3" name="initial" type="bool" default="true" /> - <description> - Decode the given [code]data[/code] representing a spawnable state into [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when a client receives a server spawn for a scene with [constant REPLICATION_MODE_SERVER]. See [method spawn_config]. - Tip: You may find this function useful in servers when parsing spawn requests from clients, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM]. - </description> - </method> - <method name="despawn"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <argument index="2" name="peer_id" type="int" default="0" /> - <description> - Request a despawn for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code]. This will either trigger the default behavior, or invoke the custom spawn/despawn callables specified in [method spawn_config]. See [method send_despawn] for the default behavior. - </description> - </method> - <method name="encode_state"> - <return type="PackedByteArray" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <argument index="2" name="initial" type="bool" default="true" /> - <description> - Encode the given [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when the server spawns scenes with [constant REPLICATION_MODE_SERVER]. See [method spawn_config]. - Tip: You may find this function useful when requesting spawns from clients to server, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM]. - </description> - </method> - <method name="send_despawn"> - <return type="int" enum="Error" /> - <argument index="0" name="peer_id" type="int" /> - <argument index="1" name="scene_id" type="int" /> - <argument index="2" name="data" type="Variant" default="null" /> - <argument index="3" name="path" type="NodePath" default="NodePath("")" /> - <description> - Sends a despawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant REPLICATION_MODE_SERVER] (see [method spawn_config]) and the request is sent by the server (see [method MultiplayerAPI.is_server]), the receiving peer(s) will automatically queue for deletion the node at [code]path[/code] and emit the signal [signal despawned]. In all other cases no deletion happens, and the signal [signal despawn_requested] is emitted instead. - </description> - </method> - <method name="send_spawn"> - <return type="int" enum="Error" /> - <argument index="0" name="peer_id" type="int" /> - <argument index="1" name="scene_id" type="int" /> - <argument index="2" name="data" type="Variant" default="null" /> - <argument index="3" name="path" type="NodePath" default="NodePath("")" /> - <description> - Sends a spawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant REPLICATION_MODE_SERVER] (see [method spawn_config]) and the request is sent by the server (see [method MultiplayerAPI.is_server]), the receiving peer(s) will automatically instantiate that scene, add it to the [SceneTree] at the given [code]path[/code] and emit the signal [signal spawned]. In all other cases no instantiation happens, and the signal [signal spawn_requested] is emitted instead. - </description> - </method> - <method name="send_sync"> - <return type="int" enum="Error" /> - <argument index="0" name="peer_id" type="int" /> - <argument index="1" name="scene_id" type="int" /> - <argument index="2" name="data" type="PackedByteArray" /> - <argument index="3" name="transfer_mode" type="int" enum="TransferMode" default="2" /> - <argument index="4" name="channel" type="int" default="0" /> - <description> - Sends a sync request for the instances of the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). This function can only be called manually when overriding the send and receive sync functions (see [method sync_config]). - </description> - </method> - <method name="spawn"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <argument index="2" name="peer_id" type="int" default="0" /> - <description> - Request a spawn for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code]. This will either trigger the default behavior, or invoke the custom spawn/despawn callables specified in [method spawn_config]. See [method send_spawn] for the default behavior. - </description> - </method> - <method name="spawn_config"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="spawn_mode" type="int" enum="MultiplayerReplicator.ReplicationMode" /> - <argument index="2" name="properties" type="StringName[]" default="[]" /> - <argument index="3" name="custom_send" type="Callable" /> - <argument index="4" name="custom_receive" type="Callable" /> - <description> - Configures the MultiplayerReplicator to track instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. When [code]mode[/code] is [constant REPLICATION_MODE_SERVER], the specified [code]properties[/code] will also be replicated to clients during the initial spawn. You can optionally specify a [code]custom_send[/code] and a [code]custom_receive[/code] to override the default behavior and customize the spawn/despawn proecess. - Tip: You can use a custom property in the scene main script to return a customly optimized state representation. - </description> - </method> - <method name="sync_all"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="peer_id" type="int" default="0" /> - <description> - Manually request a sync for all the instances of the scene identified by [code]scene_id[/code]. This function will trigger the default sync behavior, or call your send custom send callable if specified in [method sync_config]. - [b]Note:[/b] The default implementation only allow syncing from server to clients. - </description> - </method> - <method name="sync_config"> - <return type="int" enum="Error" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="interval" type="int" /> - <argument index="2" name="properties" type="StringName[]" default="[]" /> - <argument index="3" name="custom_send" type="Callable" /> - <argument index="4" name="custom_receive" type="Callable" /> - <description> - Configures the MultiplayerReplicator to sync instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication at the desired [code]interval[/code] (in milliseconds). The specified [code]properties[/code] will be part of the state sync. You can optionally specify a [code]custom_send[/code] and a [code]custom_receive[/code] to override the default behavior and customize the synchronization proecess. - Tip: You can use a custom property in the scene main script to return a customly optimized state representation (having a single property that returns a PackedByteArray is highly recommended when dealing with many instances). - </description> - </method> - <method name="track"> - <return type="void" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <description> - Track the given [code]object[/code] as an instance of the scene identified by [code]scene_id[/code]. This object will be passed to your custom sync callables (see [method sync_config]). Tracking and untracking is automatic in [constant REPLICATION_MODE_SERVER]. - </description> - </method> - <method name="untrack"> - <return type="void" /> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="object" type="Object" /> - <description> - Untrack the given [code]object[/code]. This object will no longer be passed to your custom sync callables (see [method sync_config]). Tracking and untracking is automatic in [constant REPLICATION_MODE_SERVER]. - </description> - </method> - </methods> - <signals> - <signal name="despawn_requested"> - <argument index="0" name="id" type="int" /> - <argument index="1" name="scene_id" type="int" /> - <argument index="2" name="parent" type="Node" /> - <argument index="3" name="name" type="String" /> - <argument index="4" name="data" type="PackedByteArray" /> - <description> - Emitted when a network despawn request has been received from a client, or for a [PackedScene] that has been configured as [constant REPLICATION_MODE_CUSTOM]. - </description> - </signal> - <signal name="despawned"> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="node" type="Node" /> - <description> - Emitted on a client before deleting a local Node upon receiving a despawn request from the server. - </description> - </signal> - <signal name="replicated_instance_added"> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="node" type="Node" /> - <description> - Emitted when an instance of a [PackedScene] that has been configured for networking enters the [SceneTree]. See [method spawn_config]. - </description> - </signal> - <signal name="replicated_instance_removed"> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="node" type="Node" /> - <description> - Emitted when an instance of a [PackedScene] that has been configured for networking leaves the [SceneTree]. See [method spawn_config]. - </description> - </signal> - <signal name="spawn_requested"> - <argument index="0" name="id" type="int" /> - <argument index="1" name="scene_id" type="int" /> - <argument index="2" name="parent" type="Node" /> - <argument index="3" name="name" type="String" /> - <argument index="4" name="data" type="PackedByteArray" /> - <description> - Emitted when a network spawn request has been received from a client, or for a [PackedScene] that has been configured as [constant REPLICATION_MODE_CUSTOM]. - </description> - </signal> - <signal name="spawned"> - <argument index="0" name="scene_id" type="int" /> - <argument index="1" name="node" type="Node" /> - <description> - Emitted on a client after a new Node is instantiated locally and added to the SceneTree upon receiving a spawn request from the server. - </description> - </signal> - </signals> - <constants> - <constant name="REPLICATION_MODE_NONE" value="0" enum="ReplicationMode"> - Used with [method spawn_config] to identify a [PackedScene] that should not be replicated. - </constant> - <constant name="REPLICATION_MODE_SERVER" value="1" enum="ReplicationMode"> - Used with [method spawn_config] to identify a [PackedScene] that should be automatically replicated from server to clients. - </constant> - <constant name="REPLICATION_MODE_CUSTOM" value="2" enum="ReplicationMode"> - Used with [method spawn_config] to identify a [PackedScene] that can be manually replicated among peers. - </constant> - </constants> -</class> diff --git a/doc/classes/MultiplayerSpawner.xml b/doc/classes/MultiplayerSpawner.xml new file mode 100644 index 0000000000..8bfecfce41 --- /dev/null +++ b/doc/classes/MultiplayerSpawner.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="MultiplayerSpawner" inherits="Node" version="4.0"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <methods> + <method name="_spawn_custom" qualifiers="virtual"> + <return type="Object" /> + <argument index="0" name="data" type="Variant" /> + <description> + </description> + </method> + <method name="spawn"> + <return type="Node" /> + <argument index="0" name="data" type="Variant" default="null" /> + <description> + </description> + </method> + </methods> + <members> + <member name="auto_spawn" type="bool" setter="set_auto_spawning" getter="is_auto_spawning" default="false"> + </member> + <member name="replication" type="PackedScene[]" setter="set_spawnable_scenes" getter="get_spawnable_scenes" default="[]"> + </member> + <member name="spawn_limit" type="int" setter="set_spawn_limit" getter="get_spawn_limit" default="0"> + </member> + <member name="spawn_path" type="NodePath" setter="set_spawn_path" getter="get_spawn_path" default="NodePath("")"> + </member> + </members> + <signals> + <signal name="despawned"> + <argument index="0" name="scene_id" type="int" /> + <argument index="1" name="node" type="Node" /> + <description> + </description> + </signal> + <signal name="spawned"> + <argument index="0" name="scene_id" type="int" /> + <argument index="1" name="node" type="Node" /> + <description> + </description> + </signal> + </signals> +</class> diff --git a/doc/classes/MultiplayerSynchronizer.xml b/doc/classes/MultiplayerSynchronizer.xml new file mode 100644 index 0000000000..242d4589a4 --- /dev/null +++ b/doc/classes/MultiplayerSynchronizer.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="MultiplayerSynchronizer" inherits="Node" version="4.0"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <members> + <member name="replication_interval" type="float" setter="set_replication_interval" getter="get_replication_interval" default="0.0"> + </member> + <member name="resource" type="SceneReplicationConfig" setter="set_replication_config" getter="get_replication_config"> + </member> + <member name="root_path" type="NodePath" setter="set_root_path" getter="get_root_path" default="NodePath("")"> + </member> + </members> +</class> diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index 47be5695a0..d041d20656 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -79,7 +79,7 @@ <return type="void" /> <description> Called when the node is "ready", i.e. when both the node and its children have entered the scene tree. If the node has children, their [method _ready] callbacks get triggered first, and the parent node will receive the ready notification afterwards. - Corresponds to the [constant NOTIFICATION_READY] notification in [method Object._notification]. See also the [code]onready[/code] keyword for variables. + Corresponds to the [constant NOTIFICATION_READY] notification in [method Object._notification]. See also the [code]@onready[/code] annotation for variables. Usually used for initialization. For even earlier initialization, [method Object._init] may be used. See also [method _enter_tree]. [b]Note:[/b] [method _ready] may be called only once for each node. After removing a node from the scene tree and adding again, [code]_ready[/code] will not be called for the second time. This can be bypassed with requesting another call with [method request_ready], which may be called anywhere before adding the node again. </description> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index dc40d2fd1b..ed124d1d15 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -356,6 +356,8 @@ <member name="debug/gdscript/warnings/incompatible_ternary" type="bool" setter="" getter="" default="true"> If [code]true[/code], enables warnings when a ternary operator may emit values with incompatible types. </member> + <member name="debug/gdscript/warnings/int_assigned_to_enum" type="bool" setter="" getter="" default="true"> + </member> <member name="debug/gdscript/warnings/integer_division" type="bool" setter="" getter="" default="true"> If [code]true[/code], enables warnings when dividing an integer by another integer (the decimal part will be discarded). </member> diff --git a/doc/classes/SceneReplicationConfig.xml b/doc/classes/SceneReplicationConfig.xml new file mode 100644 index 0000000000..e846740dd3 --- /dev/null +++ b/doc/classes/SceneReplicationConfig.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="SceneReplicationConfig" inherits="Resource" version="4.0"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <methods> + <method name="add_property"> + <return type="void" /> + <argument index="0" name="path" type="NodePath" /> + <argument index="1" name="index" type="int" default="-1" /> + <description> + </description> + </method> + <method name="get_properties" qualifiers="const"> + <return type="NodePath[]" /> + <description> + </description> + </method> + <method name="property_get_index" qualifiers="const"> + <return type="int" /> + <argument index="0" name="path" type="NodePath" /> + <description> + </description> + </method> + <method name="property_get_spawn"> + <return type="bool" /> + <argument index="0" name="path" type="NodePath" /> + <description> + </description> + </method> + <method name="property_get_sync"> + <return type="bool" /> + <argument index="0" name="path" type="NodePath" /> + <description> + </description> + </method> + <method name="property_set_spawn"> + <return type="void" /> + <argument index="0" name="path" type="NodePath" /> + <argument index="1" name="enabled" type="bool" /> + <description> + </description> + </method> + <method name="property_set_sync"> + <return type="void" /> + <argument index="0" name="path" type="NodePath" /> + <argument index="1" name="enabled" type="bool" /> + <description> + </description> + </method> + <method name="remove_property"> + <return type="void" /> + <argument index="0" name="path" type="NodePath" /> + <description> + </description> + </method> + </methods> +</class> diff --git a/doc/classes/TextureButton.xml b/doc/classes/TextureButton.xml index 476ab2d1bf..5f081b95f5 100644 --- a/doc/classes/TextureButton.xml +++ b/doc/classes/TextureButton.xml @@ -12,17 +12,17 @@ <link title="3D Voxel Demo">https://godotengine.org/asset-library/asset/676</link> </tutorials> <members> - <member name="expand" type="bool" setter="set_expand" getter="get_expand" default="false"> - If [code]true[/code], the texture stretches to the edges of the node's bounding rectangle using the [member stretch_mode]. If [code]false[/code], the texture will not scale with the node. - </member> <member name="flip_h" type="bool" setter="set_flip_h" getter="is_flipped_h" default="false"> If [code]true[/code], texture is flipped horizontally. </member> <member name="flip_v" type="bool" setter="set_flip_v" getter="is_flipped_v" default="false"> If [code]true[/code], texture is flipped vertically. </member> - <member name="stretch_mode" type="int" setter="set_stretch_mode" getter="get_stretch_mode" enum="TextureButton.StretchMode" default="0"> - Controls the texture's behavior when you resize the node's bounding rectangle, [b]only if[/b] [member expand] is [code]true[/code]. Set it to one of the [enum StretchMode] constants. See the constants to learn more. + <member name="ignore_texture_size" type="bool" setter="set_ignore_texture_size" getter="get_ignore_texture_size" default="false"> + If [code]true[/code], the size of the texture won't be considered for minimum size calculation, so the [TextureButton] can be shrunk down past the texture size. + </member> + <member name="stretch_mode" type="int" setter="set_stretch_mode" getter="get_stretch_mode" enum="TextureButton.StretchMode" default="2"> + Controls the texture's behavior when you resize the node's bounding rectangle. See the [enum StretchMode] constants for available options. </member> <member name="texture_click_mask" type="BitMap" setter="set_click_mask" getter="get_click_mask"> Pure black and white [BitMap] image to use for click detection. On the mask, white pixels represent the button's clickable area. Use it to create buttons with curved shapes. diff --git a/doc/classes/Theme.xml b/doc/classes/Theme.xml index d1a48fda55..b1367be263 100644 --- a/doc/classes/Theme.xml +++ b/doc/classes/Theme.xml @@ -1,20 +1,22 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="Theme" inherits="Resource" version="4.0"> <brief_description> - Theme for controls. + Theme resource for styling/skinning [Control]s and [Window]s. </brief_description> <description> - A theme for skinning controls. Controls can be skinned individually, but for complex applications, it's more practical to just create a global theme that defines everything. This theme can be applied to any [Control]; the Control and its children will automatically use it. - Theme resources can alternatively be loaded by writing them in a [code].theme[/code] file, see the documentation for more information. + A theme resource is used for styling/skinning [Control] and [Window] nodes. While individual controls can be styled using their local theme overrides (see [method Control.add_theme_color_override]), theme resources allow you to store and apply the same settings between all controls sharing the same type (e.g. style all [Button]s the same). One theme resource can be used for the entire project, but you can also set a separate theme resource to a branch of control nodes. A theme resources assigned to a control node applies to the control itself, as well as all of its direct and indirect children (as long as a chain of controls is uninterrupted). + Use [member ProjectSettings.gui/theme/custom] to set up a project-scope theme that will be available to every control in your project. + Use [member Control.theme] of any control node to set up a theme that will be available to that control and all of its direct and indirect children. </description> <tutorials> <link title="GUI skinning">$DOCS_URL/tutorials/ui/gui_skinning.html</link> + <link title="Using the theme editor">$DOCS_URL/tutorials/ui/gui_using_theme_editor.html</link> </tutorials> <methods> <method name="clear"> <return type="void" /> <description> - Clears all values on the theme. + Removes all the theme properties defined on the theme resource. </description> </method> <method name="clear_color"> @@ -22,7 +24,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears the [Color] at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the [Color] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_color] to check for existence. </description> </method> <method name="clear_constant"> @@ -30,7 +33,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears the constant at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the constant property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_constant] to check for existence. </description> </method> <method name="clear_font"> @@ -38,7 +42,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears the [Font] at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the [Font] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_font] to check for existence. </description> </method> <method name="clear_font_size"> @@ -46,7 +51,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears the font size [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the font size property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_font_size] to check for existence. </description> </method> <method name="clear_icon"> @@ -54,7 +60,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears the icon at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the icon property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_icon] to check for existence. </description> </method> <method name="clear_stylebox"> @@ -62,7 +69,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Clears [StyleBox] at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the [StyleBox] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_stylebox] to check for existence. </description> </method> <method name="clear_theme_item"> @@ -71,14 +79,16 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Clears the theme item of [code]data_type[/code] at [code]name[/code] if the theme has [code]theme_type[/code]. + Removes the theme property of [code]data_type[/code] defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Fails if it doesn't exist. Use [method has_theme_item] to check for existence. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="clear_type_variation"> <return type="void" /> <argument index="0" name="theme_type" type="StringName" /> <description> - Unmarks [code]theme_type[/code] as being a variation of any other type. + Unmarks [code]theme_type[/code] as being a variation of another theme type. See [method set_type_variation]. </description> </method> <method name="get_color" qualifiers="const"> @@ -86,20 +96,21 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the [Color] at [code]name[/code] if the theme has [code]theme_type[/code]. + Returns the [Color] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the default color value if the property doesn't exist. Use [method has_color] to check for existence. </description> </method> <method name="get_color_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the [Color]s as a [PackedStringArray] filled with each [Color]'s name, for use in [method get_color], if the theme has [code]theme_type[/code]. + Returns a list of names for [Color] properties defined with [code]theme_type[/code]. Use [method get_color_type_list] to get a list of possible theme type names. </description> </method> <method name="get_color_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the [Color] types as a [PackedStringArray] filled with unique type names, for use in [method get_color] and/or [method get_color_list]. + Returns a list of all unique theme type names for [Color] properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_constant" qualifiers="const"> @@ -107,20 +118,21 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the constant at [code]name[/code] if the theme has [code]theme_type[/code]. + Returns the constant property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns [code]0[/code] if the property doesn't exist. Use [method has_constant] to check for existence. </description> </method> <method name="get_constant_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the constants as a [PackedStringArray] filled with each constant's name, for use in [method get_constant], if the theme has [code]theme_type[/code]. + Returns a list of names for constant properties defined with [code]theme_type[/code]. Use [method get_constant_type_list] to get a list of possible theme type names. </description> </method> <method name="get_constant_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the constant types as a [PackedStringArray] filled with unique type names, for use in [method get_constant] and/or [method get_constant_list]. + Returns a list of all unique theme type names for constant properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_font" qualifiers="const"> @@ -128,14 +140,16 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the [Font] at [code]name[/code] if the theme has [code]theme_type[/code]. + Returns the [Font] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the default theme font if the property doesn't exist and the default theme font is set up (see [member default_font]). Use [method has_font] to check for existence of the property and [method has_default_font] to check for existence of the default theme font. + Returns the engine fallback font value, if neither exist. </description> </method> <method name="get_font_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the [Font]s as a [PackedStringArray] filled with each [Font]'s name, for use in [method get_font], if the theme has [code]theme_type[/code]. + Returns a list of names for [Font] properties defined with [code]theme_type[/code]. Use [method get_font_type_list] to get a list of possible theme type names. </description> </method> <method name="get_font_size" qualifiers="const"> @@ -143,26 +157,28 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the font size at [code]name[/code] if the theme has [code]theme_type[/code]. + Returns the font size property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the default theme font size if the property doesn't exist and the default theme font size is set up (see [member default_font_size]). Use [method has_font_size] to check for existence of the property and [method has_default_font_size] to check for existence of the default theme font. + Returns the engine fallback font size value, if neither exist. </description> </method> <method name="get_font_size_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the font sizes as a [PackedStringArray] filled with each font size name, for use in [method get_font_size], if the theme has [code]theme_type[/code]. + Returns a list of names for font size properties defined with [code]theme_type[/code]. Use [method get_font_size_type_list] to get a list of possible theme type names. </description> </method> <method name="get_font_size_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the font size types as a [PackedStringArray] filled with unique type names, for use in [method get_font_size] and/or [method get_font_size_list]. + Returns a list of all unique theme type names for font size properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_font_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the [Font] types as a [PackedStringArray] filled with unique type names, for use in [method get_font] and/or [method get_font_list]. + Returns a list of all unique theme type names for [Font] properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_icon" qualifiers="const"> @@ -170,20 +186,21 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the icon [Texture2D] at [code]name[/code] if the theme has [code]theme_type[/code]. + Returns the icon property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the engine fallback icon value if the property doesn't exist. Use [method has_icon] to check for existence. </description> </method> <method name="get_icon_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the icons as a [PackedStringArray] filled with each [Texture2D]'s name, for use in [method get_icon], if the theme has [code]theme_type[/code]. + Returns a list of names for icon properties defined with [code]theme_type[/code]. Use [method get_icon_type_list] to get a list of possible theme type names. </description> </method> <method name="get_icon_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the icon types as a [PackedStringArray] filled with unique type names, for use in [method get_icon] and/or [method get_icon_list]. + Returns a list of all unique theme type names for icon properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_stylebox" qualifiers="const"> @@ -191,22 +208,21 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns the [StyleBox] at [code]name[/code] if the theme has [code]theme_type[/code]. - Valid [code]name[/code]s may be found using [method get_stylebox_list]. Valid [code]theme_type[/code]s may be found using [method get_stylebox_type_list]. + Returns the [StyleBox] property defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the engine fallback stylebox value if the property doesn't exist. Use [method has_stylebox] to check for existence. </description> </method> <method name="get_stylebox_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="theme_type" type="String" /> <description> - Returns all the [StyleBox]s as a [PackedStringArray] filled with each [StyleBox]'s name, for use in [method get_stylebox], if the theme has [code]theme_type[/code]. - Valid [code]theme_type[/code]s may be found using [method get_stylebox_type_list]. + Returns a list of names for [StyleBox] properties defined with [code]theme_type[/code]. Use [method get_stylebox_type_list] to get a list of possible theme type names. </description> </method> <method name="get_stylebox_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the [StyleBox] types as a [PackedStringArray] filled with unique type names, for use in [method get_stylebox] and/or [method get_stylebox_list]. + Returns a list of all unique theme type names for [StyleBox] properties. Use [method get_type_list] to get a list of all unique theme types. </description> </method> <method name="get_theme_item" qualifiers="const"> @@ -215,8 +231,9 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Returns the theme item of [code]data_type[/code] at [code]name[/code] if the theme has [code]theme_type[/code]. - Valid [code]name[/code]s may be found using [method get_theme_item_list] or a data type specific method. Valid [code]theme_type[/code]s may be found using [method get_theme_item_type_list] or a data type specific method. + Returns the theme property of [code]data_type[/code] defined by [code]name[/code] and [code]theme_type[/code], if it exists. + Returns the engine fallback icon value if the property doesn't exist. Use [method has_theme_item] to check for existence. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="get_theme_item_list" qualifiers="const"> @@ -224,35 +241,36 @@ <argument index="0" name="data_type" type="int" enum="Theme.DataType" /> <argument index="1" name="theme_type" type="String" /> <description> - Returns all the theme items of [code]data_type[/code] as a [PackedStringArray] filled with each theme items's name, for use in [method get_theme_item] or a data type specific method, if the theme has [code]theme_type[/code]. - Valid [code]theme_type[/code]s may be found using [method get_theme_item_type_list] or a data type specific method. + Returns a list of names for properties of [code]data_type[/code] defined with [code]theme_type[/code]. Use [method get_theme_item_type_list] to get a list of possible theme type names. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="get_theme_item_type_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="data_type" type="int" enum="Theme.DataType" /> <description> - Returns all the theme items of [code]data_type[/code] types as a [PackedStringArray] filled with unique type names, for use in [method get_theme_item], [method get_theme_item_list] or data type specific methods. + Returns a list of all unique theme type names for [code]data_type[/code] properties. Use [method get_type_list] to get a list of all unique theme types. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="get_type_list" qualifiers="const"> <return type="PackedStringArray" /> <description> - Returns all the theme types as a [PackedStringArray] filled with unique type names, for use in other [code]get_*[/code] functions of this theme. + Returns a list of all unique theme type names. Use the appropriate [code]get_*_type_list[/code] method to get a list of unique theme types for a single data type. </description> </method> <method name="get_type_variation_base" qualifiers="const"> <return type="StringName" /> <argument index="0" name="theme_type" type="StringName" /> <description> - Returns the base theme type if [code]theme_type[/code] is a valid variation type. Returns an empty string otherwise. + Returns the name of the base theme type if [code]theme_type[/code] is a valid variation type. Returns an empty string otherwise. </description> </method> <method name="get_type_variation_list" qualifiers="const"> <return type="PackedStringArray" /> <argument index="0" name="base_type" type="StringName" /> <description> - Returns a list of all variation for the given [code]base_type[/code]. + Returns a list of all type variations for the given [code]base_type[/code]. </description> </method> <method name="has_color" qualifiers="const"> @@ -260,8 +278,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if [Color] with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the [Color] property defined by [code]name[/code] and [code]theme_type[/code] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_color] to define it. </description> </method> <method name="has_constant" qualifiers="const"> @@ -269,26 +287,29 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if constant with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the constant property defined by [code]name[/code] and [code]theme_type[/code] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_constant] to define it. </description> </method> <method name="has_default_base_scale" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if this theme has a valid [member default_base_scale] value. + Returns [code]true[/code] if [member default_base_scale] has a valid value. + Returns [code]false[/code] if it doesn't. The value must be greater than [code]0.0[/code] to be considered valid. </description> </method> <method name="has_default_font" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if this theme has a valid [member default_font] value. + Returns [code]true[/code] if [member default_font] has a valid value. + Returns [code]false[/code] if it doesn't. </description> </method> <method name="has_default_font_size" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if this theme has a valid [member default_font_size] value. + Returns [code]true[/code] if [member default_font_size] has a valid value. + Returns [code]false[/code] if it doesn't. The value must be greater than [code]0[/code] to be considered valid. </description> </method> <method name="has_font" qualifiers="const"> @@ -296,8 +317,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if [Font] with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the [Font] property defined by [code]name[/code] and [code]theme_type[/code] exists, or if the default theme font is set up (see [method has_default_font]). + Returns [code]false[/code] if neither exist. Use [method set_font] to define the property. </description> </method> <method name="has_font_size" qualifiers="const"> @@ -305,8 +326,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if font size with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the font size property defined by [code]name[/code] and [code]theme_type[/code] exists, or if the default theme font size is set up (see [method has_default_font_size]). + Returns [code]false[/code] if neither exist. Use [method set_font_size] to define the property. </description> </method> <method name="has_icon" qualifiers="const"> @@ -314,8 +335,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if icon [Texture2D] with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the icon property defined by [code]name[/code] and [code]theme_type[/code] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_icon] to define it. </description> </method> <method name="has_stylebox" qualifiers="const"> @@ -323,8 +344,8 @@ <argument index="0" name="name" type="StringName" /> <argument index="1" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if [StyleBox] with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the [StyleBox] property defined by [code]name[/code] and [code]theme_type[/code] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_stylebox] to define it. </description> </method> <method name="has_theme_item" qualifiers="const"> @@ -333,8 +354,9 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Returns [code]true[/code] if a theme item of [code]data_type[/code] with [code]name[/code] is in [code]theme_type[/code]. - Returns [code]false[/code] if the theme does not have [code]theme_type[/code]. + Returns [code]true[/code] if the theme property of [code]data_type[/code] defined by [code]name[/code] and [code]theme_type[/code] exists. + Returns [code]false[/code] if it doesn't exist. Use [method set_theme_item] to define it. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="is_type_variation" qualifiers="const"> @@ -342,14 +364,14 @@ <argument index="0" name="theme_type" type="StringName" /> <argument index="1" name="base_type" type="StringName" /> <description> - Returns [code]true[/code] if [code]theme_type[/code] is marked as a variation of [code]base_type[/code] in this theme. + Returns [code]true[/code] if [code]theme_type[/code] is marked as a variation of [code]base_type[/code]. </description> </method> <method name="merge_with"> <return type="void" /> <argument index="0" name="other" type="Theme" /> <description> - Adds missing and overrides existing definitions with values from the [code]other[/code] [Theme]. + Adds missing and overrides existing definitions with values from the [code]other[/code] theme resource. [b]Note:[/b] This modifies the current theme. If you want to merge two themes together without modifying either one, create a new empty theme and merge the other two into it one after another. </description> </method> @@ -359,7 +381,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames the [Color] at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the [Color] property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_color] to check for existence, and [method clear_color] to remove the existing property. </description> </method> <method name="rename_constant"> @@ -368,7 +391,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames the constant at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the constant property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_constant] to check for existence, and [method clear_constant] to remove the existing property. </description> </method> <method name="rename_font"> @@ -377,7 +401,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames the [Font] at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the [Font] property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_font] to check for existence, and [method clear_font] to remove the existing property. </description> </method> <method name="rename_font_size"> @@ -386,7 +411,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames the font size [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the font size property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_font_size] to check for existence, and [method clear_font_size] to remove the existing property. </description> </method> <method name="rename_icon"> @@ -395,7 +421,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames the icon at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the icon property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_icon] to check for existence, and [method clear_icon] to remove the existing property. </description> </method> <method name="rename_stylebox"> @@ -404,7 +431,8 @@ <argument index="1" name="name" type="StringName" /> <argument index="2" name="theme_type" type="StringName" /> <description> - Renames [StyleBox] at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the [StyleBox] property defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_stylebox] to check for existence, and [method clear_stylebox] to remove the existing property. </description> </method> <method name="rename_theme_item"> @@ -414,7 +442,9 @@ <argument index="2" name="name" type="StringName" /> <argument index="3" name="theme_type" type="StringName" /> <description> - Renames the theme item of [code]data_type[/code] at [code]old_name[/code] to [code]name[/code] if the theme has [code]theme_type[/code]. If [code]name[/code] is already taken, this method fails. + Renames the theme property of [code]data_type[/code] defined by [code]old_name[/code] and [code]theme_type[/code] to [code]name[/code], if it exists. + Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_theme_item] to check for existence, and [method clear_theme_item] to remove the existing property. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="set_color"> @@ -423,8 +453,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="color" type="Color" /> <description> - Sets the theme's [Color] to [code]color[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the [Color] property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_color] to remove the property. </description> </method> <method name="set_constant"> @@ -433,8 +462,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="constant" type="int" /> <description> - Sets the theme's constant to [code]constant[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the constant property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_constant] to remove the property. </description> </method> <method name="set_font"> @@ -443,8 +471,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="font" type="Font" /> <description> - Sets the theme's [Font] to [code]font[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the [Font] property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_font] to remove the property. </description> </method> <method name="set_font_size"> @@ -453,8 +480,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="font_size" type="int" /> <description> - Sets the theme's font size to [code]font_size[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the font size property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_font_size] to remove the property. </description> </method> <method name="set_icon"> @@ -463,8 +489,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="texture" type="Texture2D" /> <description> - Sets the theme's icon [Texture2D] to [code]texture[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the icon property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_icon] to remove the property. </description> </method> <method name="set_stylebox"> @@ -473,8 +498,7 @@ <argument index="1" name="theme_type" type="StringName" /> <argument index="2" name="texture" type="StyleBox" /> <description> - Sets theme's [StyleBox] to [code]stylebox[/code] at [code]name[/code] in [code]theme_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the [StyleBox] property defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_stylebox] to remove the property. </description> </method> <method name="set_theme_item"> @@ -484,9 +508,9 @@ <argument index="2" name="theme_type" type="StringName" /> <argument index="3" name="value" type="Variant" /> <description> - Sets the theme item of [code]data_type[/code] to [code]value[/code] at [code]name[/code] in [code]theme_type[/code]. - Does nothing if the [code]value[/code] type does not match [code]data_type[/code]. - Creates [code]theme_type[/code] if the theme does not have it. + Creates or changes the value of the theme property of [code]data_type[/code] defined by [code]name[/code] and [code]theme_type[/code]. Use [method clear_theme_item] to remove the property. + Fails if the [code]value[/code] type is not accepted by [code]data_type[/code]. + [b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic. </description> </method> <method name="set_type_variation"> @@ -494,25 +518,25 @@ <argument index="0" name="theme_type" type="StringName" /> <argument index="1" name="base_type" type="StringName" /> <description> - Marks [code]theme_type[/code] as being a variation of [code]base_type[/code]. + Marks [code]theme_type[/code] as a variation of [code]base_type[/code]. This adds [code]theme_type[/code] as a suggested option for [member Control.theme_type_variation] on a [Control] that is of the [code]base_type[/code] class. - Variations can also be nested, i.e. [code]base_type[/code] can be another variation. If a chain of variations ends with a [code]base_type[/code] matching a class of a [Control], the whole chain is going to be suggested as options. - [b]Note:[/b] Suggestions only show up if this [Theme] is set as the project default theme. See [member ProjectSettings.gui/theme/custom]. + Variations can also be nested, i.e. [code]base_type[/code] can be another variation. If a chain of variations ends with a [code]base_type[/code] matching the class of the [Control], the whole chain is going to be suggested as options. + [b]Note:[/b] Suggestions only show up if this theme resource is set as the project default theme. See [member ProjectSettings.gui/theme/custom]. </description> </method> </methods> <members> <member name="default_base_scale" type="float" setter="set_default_base_scale" getter="get_default_base_scale" default="0.0"> - The default base scale factor of this [Theme] resource. Used by some controls to scale their visual properties based on a global scale factor. If this value is set to [code]0.0[/code], the global scale factor is used. + The default base scale factor of this theme resource. Used by some controls to scale their visual properties based on the global scale factor. If this value is set to [code]0.0[/code], the global scale factor is used. Use [method has_default_base_scale] to check if this value is valid. </member> <member name="default_font" type="Font" setter="set_default_font" getter="get_default_font"> - The default font of this [Theme] resource. Used as a fallback value for font items defined in this theme, but having invalid values. If this value is also invalid, the global default value is used. + The default font of this theme resource. Used as the default value when trying to fetch a font resource that doesn't exist in this theme or is in invalid state. If the default font is also missing or invalid, the engine fallback value is used. Use [method has_default_font] to check if this value is valid. </member> <member name="default_font_size" type="int" setter="set_default_font_size" getter="get_default_font_size" default="-1"> - The default font size of this [Theme] resource. Used as a fallback value for font size items defined in this theme, but having invalid values. If this value is set to [code]-1[/code], the global default value is used. - Use [method has_default_font_size] to check if this value is valid. + The default font size of this theme resource. Used as the default value when trying to fetch a font size value that doesn't exist in this theme or is in invalid state. If the default font size is also missing or invalid, the engine fallback value is used. + Values below [code]0[/code] are invalid and can be used to unset the property. Use [method has_default_font_size] to check if this value is valid. </member> </members> <constants> diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index 3bb6603646..ab8f51ced5 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -398,6 +398,11 @@ Fullscreen window mode. Note that this is not [i]exclusive[/i] fullscreen. On Windows and Linux, a borderless window is used to emulate fullscreen. On macOS, a new desktop is used to display the running project. Regardless of the platform, enabling fullscreen will change the window size to match the monitor's size. Therefore, make sure your project supports [url=$DOCS_URL/tutorials/rendering/multiple_resolutions.html]multiple resolutions[/url] when enabling fullscreen mode. </constant> + <constant name="MODE_EXCLUSIVE_FULLSCREEN" value="4" enum="Mode"> + Exclusive fullscreen window mode. This mode is implemented on Windows only. On other platforms, it is equivalent to [constant MODE_FULLSCREEN]. + Only one window in exclusive fullscreen mode can be visible on a given screen at a time. If multiple windows are in exclusive fullscreen mode for the same screen, the last one being set to this mode takes precedence. + Regardless of the platform, enabling fullscreen will change the window size to match the monitor's size. Therefore, make sure your project supports [url=$DOCS_URL/tutorials/rendering/multiple_resolutions.html]multiple resolutions[/url] when enabling fullscreen mode. + </constant> <constant name="FLAG_RESIZE_DISABLED" value="0" enum="Flags"> The window's ability to be resized. </constant> diff --git a/doc/classes/bool.xml b/doc/classes/bool.xml index 49f2d2dd7f..243d19d94f 100644 --- a/doc/classes/bool.xml +++ b/doc/classes/bool.xml @@ -52,7 +52,7 @@ [codeblocks] [gdscript] var _can_shoot = true - onready var _cool_down = $CoolDownTimer + @onready var _cool_down = $CoolDownTimer func shoot(): if _can_shoot and Input.is_action_pressed("shoot"): diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 21aa5153d1..7c54558cd0 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -764,7 +764,7 @@ void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) { 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 (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) { + if (!is_ascii_identifier_char(c)) { if (c == ' ') { // Replace spaces with underlines. c = '_'; diff --git a/editor/editor_atlas_packer.cpp b/editor/editor_atlas_packer.cpp index b6ec5d1bad..aad32968de 100644 --- a/editor/editor_atlas_packer.cpp +++ b/editor/editor_atlas_packer.cpp @@ -30,6 +30,9 @@ #include "editor_atlas_packer.h" +#include "core/math/vector2.h" +#include "core/math/vector2i.h" + void EditorAtlasPacker::_plot_triangle(Ref<BitMap> p_bitmap, Vector2i *vertices) { int width = p_bitmap->get_size().width; int height = p_bitmap->get_size().height; diff --git a/editor/editor_atlas_packer.h b/editor/editor_atlas_packer.h index 133c516d80..169a6bead8 100644 --- a/editor/editor_atlas_packer.h +++ b/editor/editor_atlas_packer.h @@ -31,11 +31,12 @@ #ifndef EDITOR_ATLAS_PACKER_H #define EDITOR_ATLAS_PACKER_H -#include "core/math/vector2.h" - #include "core/templates/vector.h" #include "scene/resources/bit_map.h" +struct Vector2; +struct Vector2i; + class EditorAtlasPacker { public: struct Chart { diff --git a/editor/editor_export.cpp b/editor/editor_export.cpp index 792897e451..9bdfb66235 100644 --- a/editor/editor_export.cpp +++ b/editor/editor_export.cpp @@ -818,7 +818,7 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & int v = 0; if (i * 2 < script_key.length()) { char32_t ct = script_key[i * 2]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; @@ -828,7 +828,7 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & if (i * 2 + 1 < script_key.length()) { char32_t ct = script_key[i * 2 + 1]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; @@ -1215,7 +1215,7 @@ Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, c int v = 0; if (i * 2 < script_key.length()) { char32_t ct = script_key[i * 2]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; @@ -1225,7 +1225,7 @@ Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, c if (i * 2 + 1 < script_key.length()) { char32_t ct = script_key[i * 2 + 1]; - if (ct >= '0' && ct <= '9') { + if (is_digit(ct)) { ct = ct - '0'; } else if (ct >= 'a' && ct <= 'f') { ct = 10 + ct - 'a'; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index dfc95fb676..96c0f3a209 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -2007,7 +2007,7 @@ FindBar::FindBar() { hide_button = memnew(TextureButton); add_child(hide_button); hide_button->set_focus_mode(FOCUS_NONE); - hide_button->set_expand(true); + hide_button->set_ignore_texture_size(true); hide_button->set_stretch_mode(TextureButton::STRETCH_KEEP_CENTERED); hide_button->connect("pressed", callable_mp(this, &FindBar::_hide_bar)); } diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 34cace0239..425b1fc98d 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -1780,7 +1780,7 @@ Array EditorInspectorArray::_extract_properties_as_array(const List<PropertyInfo int to_char_index = 0; while (to_char_index < str.length()) { - if (str[to_char_index] < '0' || str[to_char_index] > '9') { + if (!is_digit(str[to_char_index])) { break; } to_char_index++; @@ -2512,7 +2512,7 @@ void EditorInspector::update_tree() { String str = p.name.trim_prefix(array_prefix); int to_char_index = 0; while (to_char_index < str.length()) { - if (str[to_char_index] < '0' || str[to_char_index] > '9') { + if (!is_digit(str[to_char_index])) { break; } to_char_index++; diff --git a/editor/editor_locale_dialog.cpp b/editor/editor_locale_dialog.cpp index 5c4ece7065..48a326d6d4 100644 --- a/editor/editor_locale_dialog.cpp +++ b/editor/editor_locale_dialog.cpp @@ -37,14 +37,6 @@ #include "scene/gui/option_button.h" #include "scene/gui/tree.h" -static _FORCE_INLINE_ bool is_upper_case(char32_t c) { - return (c >= 'A' && c <= 'Z'); -} - -static _FORCE_INLINE_ bool is_lower_case(char32_t c) { - return (c >= 'a' && c <= 'z'); -} - void EditorLocaleDialog::_bind_methods() { ADD_SIGNAL(MethodInfo("locale_selected", PropertyInfo(Variant::STRING, "locale"))); } @@ -363,16 +355,16 @@ void EditorLocaleDialog::set_locale(const String &p_locale) { Vector<String> locale_elements = p_locale.split("_"); lang_code->set_text(locale_elements[0]); if (locale_elements.size() >= 2) { - if (locale_elements[1].length() == 4 && is_upper_case(locale_elements[1][0]) && is_lower_case(locale_elements[1][1]) && is_lower_case(locale_elements[1][2]) && is_lower_case(locale_elements[1][3])) { + if (locale_elements[1].length() == 4 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_lower_case(locale_elements[1][1]) && is_ascii_lower_case(locale_elements[1][2]) && is_ascii_lower_case(locale_elements[1][3])) { script_code->set_text(locale_elements[1]); advanced->set_pressed(true); } - if (locale_elements[1].length() == 2 && is_upper_case(locale_elements[1][0]) && is_upper_case(locale_elements[1][1])) { + if (locale_elements[1].length() == 2 && is_ascii_upper_case(locale_elements[1][0]) && is_ascii_upper_case(locale_elements[1][1])) { country_code->set_text(locale_elements[1]); } } if (locale_elements.size() >= 3) { - if (locale_elements[2].length() == 2 && is_upper_case(locale_elements[2][0]) && is_upper_case(locale_elements[2][1])) { + if (locale_elements[2].length() == 2 && is_ascii_upper_case(locale_elements[2][0]) && is_ascii_upper_case(locale_elements[2][1])) { country_code->set_text(locale_elements[2]); } else { variant_code->set_text(locale_elements[2].to_lower()); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 716d4a50c3..c9417ee57c 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -162,6 +162,7 @@ #include "editor/plugins/path_3d_editor_plugin.h" #include "editor/plugins/physical_bone_3d_editor_plugin.h" #include "editor/plugins/polygon_2d_editor_plugin.h" +#include "editor/plugins/replication_editor_plugin.h" #include "editor/plugins/resource_preloader_editor_plugin.h" #include "editor/plugins/root_motion_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h" @@ -7021,6 +7022,7 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(InputEventEditorPlugin(this))); add_editor_plugin(memnew(SubViewportPreviewEditorPlugin(this))); add_editor_plugin(memnew(TextControlEditorPlugin(this))); + add_editor_plugin(memnew(ReplicationEditorPlugin(this))); for (int i = 0; i < EditorPlugins::get_plugin_count(); i++) { add_editor_plugin(EditorPlugins::create(i, this)); diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp index 131ecc3b12..dd72def6ad 100644 --- a/editor/find_in_files.cpp +++ b/editor/find_in_files.cpp @@ -53,11 +53,6 @@ inline void pop_back(T &container) { container.resize(container.size() - 1); } -// TODO: Copied from TextEdit private, would be nice to extract it in a single place. -static bool is_text_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; -} - static bool find_next(const String &line, String pattern, int from, bool match_case, bool whole_words, int &out_begin, int &out_end) { int end = from; @@ -73,10 +68,10 @@ static bool find_next(const String &line, String pattern, int from, bool match_c out_end = end; if (whole_words) { - if (begin > 0 && is_text_char(line[begin - 1])) { + if (begin > 0 && (is_ascii_identifier_char(line[begin - 1]))) { continue; } - if (end < line.size() && is_text_char(line[end])) { + if (end < line.size() && (is_ascii_identifier_char(line[end]))) { continue; } } diff --git a/editor/import/resource_importer_scene.cpp b/editor/import/resource_importer_scene.cpp index 0fefa0f3c4..d9448dd4a9 100644 --- a/editor/import/resource_importer_scene.cpp +++ b/editor/import/resource_importer_scene.cpp @@ -316,7 +316,7 @@ static bool _teststr(const String &p_what, const String &p_str) { String what = p_what; //remove trailing spaces and numbers, some apps like blender add ".number" to duplicates so also compensate for this - while (what.length() && ((what[what.length() - 1] >= '0' && what[what.length() - 1] <= '9') || what[what.length() - 1] <= 32 || what[what.length() - 1] == '.')) { + while (what.length() && (is_digit(what[what.length() - 1]) || what[what.length() - 1] <= 32 || what[what.length() - 1] == '.')) { what = what.substr(0, what.length() - 1); } @@ -336,7 +336,7 @@ static String _fixstr(const String &p_what, const String &p_str) { String what = p_what; //remove trailing spaces and numbers, some apps like blender add ".number" to duplicates so also compensate for this - while (what.length() && ((what[what.length() - 1] >= '0' && what[what.length() - 1] <= '9') || what[what.length() - 1] <= 32 || what[what.length() - 1] == '.')) { + while (what.length() && (is_digit(what[what.length() - 1]) || what[what.length() - 1] <= 32 || what[what.length() - 1] == '.')) { what = what.substr(0, what.length() - 1); } diff --git a/editor/plugins/editor_preview_plugins.cpp b/editor/plugins/editor_preview_plugins.cpp index cef505181a..7e0019faac 100644 --- a/editor/plugins/editor_preview_plugins.cpp +++ b/editor/plugins/editor_preview_plugins.cpp @@ -462,10 +462,6 @@ EditorMaterialPreviewPlugin::~EditorMaterialPreviewPlugin() { /////////////////////////////////////////////////////////////////////////// -static bool _is_text_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; -} - bool EditorScriptPreviewPlugin::handles(const String &p_type) const { return ClassDB::is_parent_class(p_type, "Script"); } @@ -538,15 +534,15 @@ Ref<Texture2D> EditorScriptPreviewPlugin::generate(const RES &p_from, const Size if (in_comment) { color = comment_color; } else { - if (c != '_' && ((c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') || (c >= '{' && c <= '~') || c == '\t')) { + if (is_symbol(c)) { //make symbol a little visible color = symbol_color; in_control_flow_keyword = false; in_keyword = false; - } else if (!prev_is_text && _is_text_char(c)) { + } else if (!prev_is_text && is_ascii_identifier_char(c)) { int pos = i; - while (_is_text_char(code[pos])) { + while (is_ascii_identifier_char(code[pos])) { pos++; } String word = code.substr(i, pos - i); @@ -556,7 +552,7 @@ Ref<Texture2D> EditorScriptPreviewPlugin::generate(const RES &p_from, const Size in_keyword = true; } - } else if (!_is_text_char(c)) { + } else if (!is_ascii_identifier_char(c)) { in_keyword = false; } @@ -571,7 +567,7 @@ Ref<Texture2D> EditorScriptPreviewPlugin::generate(const RES &p_from, const Size img->set_pixel(col, y0 + line * 2, bg_color.blend(ul)); img->set_pixel(col, y0 + line * 2 + 1, color); - prev_is_text = _is_text_char(c); + prev_is_text = is_ascii_identifier_char(c); } col++; } else { diff --git a/editor/plugins/replication_editor_plugin.cpp b/editor/plugins/replication_editor_plugin.cpp new file mode 100644 index 0000000000..93f4a853f3 --- /dev/null +++ b/editor/plugins/replication_editor_plugin.cpp @@ -0,0 +1,390 @@ +/*************************************************************************/ +/* replication_editor_plugin.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 "replication_editor_plugin.h" + +#include "editor/editor_scale.h" +#include "editor/inspector_dock.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/tree.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" + +/// ReplicationEditor +ReplicationEditor::ReplicationEditor(EditorNode *p_editor) { + editor = p_editor; + set_v_size_flags(SIZE_EXPAND_FILL); + set_custom_minimum_size(Size2(0, 200) * EDSCALE); + + delete_dialog = memnew(ConfirmationDialog); + delete_dialog->connect("cancelled", callable_mp(this, &ReplicationEditor::_dialog_closed), varray(false)); + delete_dialog->connect("confirmed", callable_mp(this, &ReplicationEditor::_dialog_closed), varray(true)); + add_child(delete_dialog); + + error_dialog = memnew(AcceptDialog); + error_dialog->get_ok_button()->set_text(TTR("Close")); + error_dialog->set_title(TTR("Error!")); + add_child(error_dialog); + + VBoxContainer *vb = memnew(VBoxContainer); + vb->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(vb); + + HBoxContainer *hb = memnew(HBoxContainer); + vb->add_child(hb); + np_line_edit = memnew(LineEdit); + np_line_edit->set_placeholder(":property"); + np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + hb->add_child(np_line_edit); + add_button = memnew(Button); + add_button->connect("pressed", callable_mp(this, &ReplicationEditor::_add_pressed)); + add_button->set_text(TTR("Add")); + hb->add_child(add_button); + + tree = memnew(Tree); + tree->set_hide_root(true); + tree->set_columns(4); + tree->set_column_titles_visible(true); + tree->set_column_title(0, TTR("Properties")); + tree->set_column_expand(0, true); + tree->set_column_title(1, TTR("Spawn")); + tree->set_column_expand(1, false); + tree->set_column_custom_minimum_width(1, 100); + tree->set_column_title(2, TTR("Sync")); + tree->set_column_custom_minimum_width(2, 100); + tree->set_column_expand(2, false); + tree->set_column_expand(3, false); + tree->create_item(); + tree->connect("button_pressed", callable_mp(this, &ReplicationEditor::_tree_button_pressed)); + tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited)); + tree->set_v_size_flags(SIZE_EXPAND_FILL); + vb->add_child(tree); +} + +void ReplicationEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_update_config"), &ReplicationEditor::_update_config); + ClassDB::bind_method(D_METHOD("_update_checked", "property", "column", "checked"), &ReplicationEditor::_update_checked); + ADD_SIGNAL(MethodInfo("keying_changed")); +} + +void ReplicationEditor::_notification(int p_what) { + if (p_what == NOTIFICATION_ENTER_TREE || p_what == EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED) { + add_theme_style_override("panel", editor->get_gui_base()->get_theme_stylebox(SNAME("panel"), SNAME("Panel"))); + } else if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { + update_keying(); + } +} + +void ReplicationEditor::_add_pressed() { + if (!current) { + error_dialog->set_text(TTR("Please select a MultiplayerSynchronizer first.")); + error_dialog->popup_centered(); + return; + } + if (current->get_root_path().is_empty()) { + error_dialog->set_text(TTR("The MultiplayerSynchronizer needs a root path.")); + error_dialog->popup_centered(); + return; + } + String np_text = np_line_edit->get_text(); + if (np_text.find(":") == -1) { + np_text = ":" + np_text; + } + NodePath prop = NodePath(np_text); + if (prop.is_empty()) { + return; + } + UndoRedo *undo_redo = editor->get_undo_redo(); + undo_redo->create_action(TTR("Add property")); + config = current->get_replication_config(); + if (config.is_null()) { + config.instantiate(); + current->set_replication_config(config); + undo_redo->add_do_method(current, "set_replication_config", config); + undo_redo->add_undo_method(current, "set_replication_config", Ref<SceneReplicationConfig>()); + _update_config(); + } + undo_redo->add_do_method(config.ptr(), "add_property", prop); + undo_redo->add_undo_method(config.ptr(), "remove_property", prop); + undo_redo->add_do_method(this, "_update_config"); + undo_redo->add_undo_method(this, "_update_config"); + undo_redo->commit_action(); +} + +void ReplicationEditor::_tree_item_edited() { + TreeItem *ti = tree->get_edited(); + if (!ti || config.is_null()) { + return; + } + int column = tree->get_edited_column(); + ERR_FAIL_COND(column < 1 || column > 2); + const NodePath prop = ti->get_metadata(0); + UndoRedo *undo_redo = editor->get_undo_redo(); + bool value = ti->is_checked(column); + String method; + if (column == 1) { + undo_redo->create_action(TTR("Set spawn property")); + method = "property_set_spawn"; + } else { + undo_redo->create_action(TTR("Set sync property")); + method = "property_set_sync"; + } + undo_redo->add_do_method(config.ptr(), method, prop, value); + undo_redo->add_undo_method(config.ptr(), method, prop, !value); + undo_redo->add_do_method(this, "_update_checked", prop, column, value); + undo_redo->add_undo_method(this, "_update_checked", prop, column, !value); + undo_redo->commit_action(); +} + +void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id) { + TreeItem *ti = Object::cast_to<TreeItem>(p_item); + if (!ti) { + return; + } + deleting = ti->get_metadata(0); + delete_dialog->set_text(TTR("Delete Property?") + "\n\"" + ti->get_text(0) + "\""); + delete_dialog->popup_centered(); +} + +void ReplicationEditor::_dialog_closed(bool p_confirmed) { + if (deleting.is_empty() || config.is_null()) { + return; + } + if (p_confirmed) { + const NodePath prop = deleting; + int idx = config->property_get_index(prop); + bool spawn = config->property_get_spawn(prop); + bool sync = config->property_get_sync(prop); + UndoRedo *undo_redo = editor->get_undo_redo(); + undo_redo->create_action(TTR("Remove Property")); + undo_redo->add_do_method(config.ptr(), "remove_property", prop); + undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx); + undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn); + undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync); + undo_redo->add_do_method(this, "_update_config"); + undo_redo->add_undo_method(this, "_update_config"); + undo_redo->commit_action(); + } + deleting = NodePath(); +} + +void ReplicationEditor::_update_checked(const NodePath &p_prop, int p_column, bool p_checked) { + if (!tree->get_root()) { + return; + } + TreeItem *ti = tree->get_root()->get_first_child(); + while (ti) { + if (ti->get_metadata(0).operator NodePath() == p_prop) { + ti->set_checked(p_column, p_checked); + return; + } + ti = ti->get_next(); + } +} + +void ReplicationEditor::update_keying() { + /// TODO make keying usable. +#if 0 + bool keying_enabled = false; + EditorHistory *editor_history = EditorNode::get_singleton()->get_editor_history(); + if (is_visible_in_tree() && config.is_valid() && editor_history->get_path_size() > 0) { + Object *obj = ObjectDB::get_instance(editor_history->get_path_object(0)); + keying_enabled = Object::cast_to<Node>(obj) != nullptr; + } + + if (keying_enabled == keying) { + return; + } + + keying = keying_enabled; + emit_signal(SNAME("keying_changed")); +#endif +} + +void ReplicationEditor::_update_config() { + deleting = NodePath(); + tree->clear(); + tree->create_item(); + if (!config.is_valid()) { + update_keying(); + return; + } + TypedArray<NodePath> props = config->get_properties(); + for (int i = 0; i < props.size(); i++) { + const NodePath path = props[i]; + _add_property(path, config->property_get_spawn(path), config->property_get_sync(path)); + } + update_keying(); +} + +void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) { + if (current == p_sync) { + return; + } + current = p_sync; + if (current) { + config = current->get_replication_config(); + } else { + config.unref(); + } + _update_config(); +} + +Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) { + if (!p_node || !has_theme_icon(p_node->get_class(), "EditorIcons")) { + return get_theme_icon("ImportFail", "EditorIcons"); + } + return get_theme_icon(p_node->get_class(), "EditorIcons"); +} + +void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) { + String prop = String(p_property); + TreeItem *item = tree->create_item(); + item->set_selectable(0, false); + item->set_selectable(1, false); + item->set_selectable(2, false); + item->set_selectable(3, false); + item->set_text(0, prop); + item->set_metadata(0, prop); + Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr; + Ref<Texture2D> icon = _get_class_icon(root_node); + if (root_node) { + String path = prop.substr(0, prop.find(":")); + String subpath = prop.substr(path.size()); + Node *node = root_node->get_node_or_null(path); + if (!node) { + node = root_node; + } + item->set_text(0, String(node->get_name()) + ":" + subpath); + icon = _get_class_icon(node); + } + item->set_icon(0, icon); + item->add_button(3, get_theme_icon("Remove", "EditorIcons")); + item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER); + item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); + item->set_checked(1, p_spawn); + item->set_editable(1, true); + item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER); + item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK); + item->set_checked(2, p_sync); + item->set_editable(2, true); +} + +void ReplicationEditor::property_keyed(const String &p_property) { + ERR_FAIL_COND(!current || config.is_null()); + Node *root = current->get_node(current->get_root_path()); + ERR_FAIL_COND(!root); + EditorHistory *history = editor->get_editor_history(); + ERR_FAIL_COND(history->get_path_size() == 0); + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(history->get_path_object(0))); + ERR_FAIL_COND(!node); + if (node->is_class("MultiplayerSynchronizer")) { + error_dialog->set_text(TTR("Properties of 'MultiplayerSynchronizer' cannot be configured for replication.")); + error_dialog->popup_centered(); + return; + } + if (history->get_path_size() > 1 || p_property.get_slice_count(":") > 1) { + error_dialog->set_text(TTR("Subresources cannot yet be configured for replication.")); + error_dialog->popup_centered(); + return; + } + + String path = root->get_path_to(node); + for (int i = 1; i < history->get_path_size(); i++) { + String prop = history->get_path_property(i); + ERR_FAIL_COND(prop == ""); + path += ":" + prop; + } + path += ":" + p_property; + + NodePath prop = path; + UndoRedo *undo_redo = editor->get_undo_redo(); + undo_redo->create_action(TTR("Add property")); + undo_redo->add_do_method(config.ptr(), "add_property", prop); + undo_redo->add_undo_method(config.ptr(), "remove_property", prop); + undo_redo->add_do_method(this, "_update_config"); + undo_redo->add_undo_method(this, "_update_config"); + undo_redo->commit_action(); +} + +/// ReplicationEditorPlugin +ReplicationEditorPlugin::ReplicationEditorPlugin(EditorNode *p_node) { + editor = p_node; + repl_editor = memnew(ReplicationEditor(editor)); + editor->add_bottom_panel_item(TTR("Replication"), repl_editor); +} + +ReplicationEditorPlugin::~ReplicationEditorPlugin() { +} + +void ReplicationEditorPlugin::_keying_changed() { + // TODO make lock usable. + //InspectorDock::get_inspector_singleton()->set_keying(repl_editor->has_keying(), this); +} + +void ReplicationEditorPlugin::_property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance) { + if (!repl_editor->has_keying()) { + return; + } + repl_editor->property_keyed(p_keyed); +} + +void ReplicationEditorPlugin::_notification(int p_what) { + if (p_what == NOTIFICATION_ENTER_TREE) { + //Node3DEditor::get_singleton()->connect("transform_key_request", callable_mp(this, &AnimationPlayerEditorPlugin::_transform_key_request)); + InspectorDock::get_inspector_singleton()->connect("property_keyed", callable_mp(this, &ReplicationEditorPlugin::_property_keyed)); + repl_editor->connect("keying_changed", callable_mp(this, &ReplicationEditorPlugin::_keying_changed)); + // TODO make lock usable. + //InspectorDock::get_inspector_singleton()->connect("object_inspected", callable_mp(repl_editor, &ReplicationEditor::update_keying)); + get_tree()->connect("node_removed", callable_mp(this, &ReplicationEditorPlugin::_node_removed)); + } +} + +void ReplicationEditorPlugin::_node_removed(Node *p_node) { + if (p_node && p_node == repl_editor->get_current()) { + repl_editor->edit(nullptr); + if (repl_editor->is_visible_in_tree()) { + editor->hide_bottom_panel(); + } + } +} + +void ReplicationEditorPlugin::edit(Object *p_object) { + repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object)); +} + +bool ReplicationEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("MultiplayerSynchronizer"); +} + +void ReplicationEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + editor->make_bottom_panel_item_visible(repl_editor); + } +} diff --git a/editor/plugins/replication_editor_plugin.h b/editor/plugins/replication_editor_plugin.h new file mode 100644 index 0000000000..049eda99cc --- /dev/null +++ b/editor/plugins/replication_editor_plugin.h @@ -0,0 +1,108 @@ +/*************************************************************************/ +/* replication_editor_plugin.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 REPLICATION_EDITOR_PLUGIN_H +#define REPLICATION_EDITOR_PLUGIN_H + +#include "editor/editor_node.h" +#include "editor/editor_plugin.h" +#include "scene/resources/scene_replication_config.h" + +class ConfirmationDialog; +class MultiplayerSynchronizer; +class Tree; + +class ReplicationEditor : public VBoxContainer { + GDCLASS(ReplicationEditor, VBoxContainer); + +private: + EditorNode *editor; + MultiplayerSynchronizer *current = nullptr; + + AcceptDialog *error_dialog = nullptr; + ConfirmationDialog *delete_dialog = nullptr; + Button *add_button = nullptr; + LineEdit *np_line_edit = nullptr; + + Ref<SceneReplicationConfig> config; + NodePath deleting; + Tree *tree; + bool keying = false; + + Ref<Texture2D> _get_class_icon(const Node *p_node); + + void _add_pressed(); + void _tree_item_edited(); + void _tree_button_pressed(Object *p_item, int p_column, int p_id); + void _update_checked(const NodePath &p_prop, int p_column, bool p_checked); + void _update_config(); + void _dialog_closed(bool p_confirmed); + void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true); + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +public: + void update_keying(); + void edit(MultiplayerSynchronizer *p_object); + bool has_keying() const { return keying; } + MultiplayerSynchronizer *get_current() const { return current; } + void property_keyed(const String &p_property); + + ReplicationEditor(EditorNode *p_node); + ~ReplicationEditor() {} +}; + +class ReplicationEditorPlugin : public EditorPlugin { + GDCLASS(ReplicationEditorPlugin, EditorPlugin); + +private: + EditorNode *editor; + ReplicationEditor *repl_editor; + + void _node_removed(Node *p_node); + void _keying_changed(); + void _property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance); + +protected: + void _notification(int p_what); + +public: + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + virtual void make_visible(bool p_visible) override; + + ReplicationEditorPlugin(EditorNode *p_node); + ~ReplicationEditorPlugin(); +}; + +#endif // REPLICATION_EDITOR_PLUGIN_H diff --git a/editor/plugins/tiles/tile_map_editor.cpp b/editor/plugins/tiles/tile_map_editor.cpp index 89027b174d..6e3724ead9 100644 --- a/editor/plugins/tiles/tile_map_editor.cpp +++ b/editor/plugins/tiles/tile_map_editor.cpp @@ -2337,7 +2337,7 @@ Map<Vector2i, TileMapCell> TileMapEditorTerrainsPlugin::_draw_terrains(const Map bool to_replace_modified = true; while (to_replace_modified) { // Get the constraints from the removed cells. - removed_cells_constraints_set = tile_map->get_terrain_constraints_from_removed_cells_list(tile_map_layer, to_replace, p_terrain_set); + removed_cells_constraints_set = tile_map->get_terrain_constraints_from_removed_cells_list(tile_map_layer, to_replace, p_terrain_set, false); // Filter the sources to make sure they are in the potential_to_replace. Map<TileMap::TerrainConstraint, Set<Vector2i>> per_constraint_tiles; @@ -3735,7 +3735,7 @@ void TileMapEditor::_move_tile_map_array_element(Object *p_undo_redo, Object *p_ String str = pi.name.trim_prefix(p_array_prefix); int to_char_index = 0; while (to_char_index < str.length()) { - if (str[to_char_index] < '0' || str[to_char_index] > '9') { + if (!is_digit(str[to_char_index])) { break; } to_char_index++; diff --git a/editor/plugins/tiles/tile_set_editor.cpp b/editor/plugins/tiles/tile_set_editor.cpp index be261927ee..ab355d4658 100644 --- a/editor/plugins/tiles/tile_set_editor.cpp +++ b/editor/plugins/tiles/tile_set_editor.cpp @@ -458,7 +458,7 @@ void TileSetEditor::_move_tile_set_array_element(Object *p_undo_redo, Object *p_ String str = pi.name.trim_prefix(p_array_prefix); int to_char_index = 0; while (to_char_index < str.length()) { - if (str[to_char_index] < '0' || str[to_char_index] > '9') { + if (!is_digit(str[to_char_index])) { break; } to_char_index++; diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp index eda5801905..0c916bf56a 100644 --- a/editor/script_create_dialog.cpp +++ b/editor/script_create_dialog.cpp @@ -153,7 +153,7 @@ bool ScriptCreateDialog::_validate_class(const String &p_string) { } } - bool valid_char = (p_string[i] >= '0' && p_string[i] <= '9') || (p_string[i] >= 'a' && p_string[i] <= 'z') || (p_string[i] >= 'A' && p_string[i] <= 'Z') || p_string[i] == '_' || p_string[i] == '.'; + bool valid_char = is_ascii_identifier_char(p_string[i]) || p_string[i] == '.'; if (!valid_char) { return false; diff --git a/modules/gdnative/gdnative/packed_arrays.cpp b/modules/gdnative/gdnative/packed_arrays.cpp index bb6f0324a8..0c49694e0b 100644 --- a/modules/gdnative/gdnative/packed_arrays.cpp +++ b/modules/gdnative/gdnative/packed_arrays.cpp @@ -32,7 +32,7 @@ #include "core/variant/variant.h" -#include "core/math/vector2.h" +#include "core/math/vector2i.h" #include "core/math/vector3i.h" static_assert(sizeof(godot_packed_byte_array) == sizeof(PackedByteArray), "PackedByteArray size mismatch"); diff --git a/modules/gdnative/gdnative/rect2.cpp b/modules/gdnative/gdnative/rect2.cpp index f4674850e3..7e0ce76c26 100644 --- a/modules/gdnative/gdnative/rect2.cpp +++ b/modules/gdnative/gdnative/rect2.cpp @@ -31,6 +31,8 @@ #include "gdnative/rect2.h" #include "core/math/rect2.h" +#include "core/math/rect2i.h" +#include "core/os/memory.h" static_assert(sizeof(godot_rect2) == sizeof(Rect2), "Rect2 size mismatch"); static_assert(sizeof(godot_rect2i) == sizeof(Rect2i), "Rect2i size mismatch"); diff --git a/modules/gdnative/gdnative/transform2d.cpp b/modules/gdnative/gdnative/transform2d.cpp index 45ba790dc1..7dc07024e5 100644 --- a/modules/gdnative/gdnative/transform2d.cpp +++ b/modules/gdnative/gdnative/transform2d.cpp @@ -31,6 +31,7 @@ #include "gdnative/transform2d.h" #include "core/math/transform_2d.h" +#include "core/os/memory.h" static_assert(sizeof(godot_transform2d) == sizeof(Transform2D), "Transform2D size mismatch"); diff --git a/modules/gdnative/gdnative/vector2.cpp b/modules/gdnative/gdnative/vector2.cpp index eb8ffd74cd..a8d4281d25 100644 --- a/modules/gdnative/gdnative/vector2.cpp +++ b/modules/gdnative/gdnative/vector2.cpp @@ -31,6 +31,8 @@ #include "gdnative/vector2.h" #include "core/math/vector2.h" +#include "core/math/vector2i.h" +#include "core/os/memory.h" static_assert(sizeof(godot_vector2) == sizeof(Vector2), "Vector2 size mismatch"); static_assert(sizeof(godot_vector2i) == sizeof(Vector2i), "Vector2i size mismatch"); diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index 2f571874ae..ac6684a29c 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -33,18 +33,6 @@ #include "../gdscript_tokenizer.h" #include "editor/editor_settings.h" -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - -static bool _is_hex_symbol(char32_t c) { - return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); -} - -static bool _is_bin_symbol(char32_t c) { - return (c == '0' || c == '1'); -} - Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) { Dictionary color_map; @@ -102,7 +90,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l color = font_color; bool is_char = !is_symbol(str[j]); bool is_a_symbol = is_symbol(str[j]); - bool is_number = (str[j] >= '0' && str[j] <= '9'); + bool is_number = is_digit(str[j]); /* color regions */ if (is_a_symbol || in_region != -1) { @@ -241,14 +229,14 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } // allow ABCDEF in hex notation - if (is_hex_notation && (_is_hex_symbol(str[j]) || is_number)) { + if (is_hex_notation && (is_hex_digit(str[j]) || is_number)) { is_number = true; } else { is_hex_notation = false; } // disallow anything not a 0 or 1 - if (is_bin_notation && (_is_bin_symbol(str[j]))) { + if (is_bin_notation && (is_binary_digit(str[j]))) { is_number = true; } else if (is_bin_notation) { is_bin_notation = false; @@ -270,7 +258,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } } - if (!in_word && _is_char(str[j]) && !is_number) { + if (!in_word && (is_ascii_char(str[j]) || is_underscore(str[j])) && !is_number) { in_word = true; } diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 5b40c94110..a80874d785 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -2022,8 +2022,6 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { "preload", "signal", "super", - "trait", - "yield", // var "const", "enum", @@ -2040,6 +2038,11 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { "return", "match", "while", + // These keywords are not implemented currently, but reserved for (potential) future use. + // We highlight them as keywords to make errors easier to understand. + "trait", + "namespace", + "yield", nullptr }; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index d11174227a..9ff52347e9 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -108,7 +108,7 @@ static GDScriptParser::DataType make_native_enum_type(const StringName &p_native GDScriptParser::DataType type; type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; type.kind = GDScriptParser::DataType::ENUM; - type.builtin_type = Variant::OBJECT; + type.builtin_type = Variant::INT; type.is_constant = true; type.is_meta_type = true; @@ -650,9 +650,9 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas datatype = specified_type; if (member.variable->initializer != nullptr) { - if (!is_type_compatible(datatype, member.variable->initializer->get_datatype(), true)) { + if (!is_type_compatible(datatype, member.variable->initializer->get_datatype(), true, member.variable->initializer)) { // Try reverse test since it can be a masked subtype. - if (!is_type_compatible(member.variable->initializer->get_datatype(), datatype, true)) { + if (!is_type_compatible(member.variable->initializer->get_datatype(), datatype, true, member.variable->initializer)) { push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", member.variable->initializer->get_datatype().to_string(), datatype.to_string()), member.variable->initializer); } else { // TODO: Add warning. @@ -1400,9 +1400,9 @@ void GDScriptAnalyzer::resolve_variable(GDScriptParser::VariableNode *p_variable type.is_meta_type = false; if (p_variable->initializer != nullptr) { - if (!is_type_compatible(type, p_variable->initializer->get_datatype(), true)) { + if (!is_type_compatible(type, p_variable->initializer->get_datatype(), true, p_variable->initializer)) { // Try reverse test since it can be a masked subtype. - if (!is_type_compatible(p_variable->initializer->get_datatype(), type, true)) { + if (!is_type_compatible(p_variable->initializer->get_datatype(), type, true, p_variable->initializer)) { push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", p_variable->initializer->get_datatype().to_string(), type.to_string()), p_variable->initializer); } else { // TODO: Add warning. @@ -1877,11 +1877,11 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig if (!assignee_type.is_variant() && assigned_value_type.is_hard_type()) { if (compatible) { - compatible = is_type_compatible(assignee_type, op_type, true); + compatible = is_type_compatible(assignee_type, op_type, true, p_assignment->assigned_value); if (!compatible) { if (assignee_type.is_hard_type()) { // Try reverse test since it can be a masked subtype. - if (!is_type_compatible(op_type, assignee_type, true)) { + if (!is_type_compatible(op_type, assignee_type, true, p_assignment->assigned_value)) { push_error(vformat(R"(Cannot assign a value of type "%s" to a target of type "%s".)", assigned_value_type.to_string(), assignee_type.to_string()), p_assignment->assigned_value); } else { // TODO: Add warning. @@ -2416,6 +2416,11 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } validate_call_arg(par_types, default_arg_count, is_vararg, p_call); + if (base_type.kind == GDScriptParser::DataType::ENUM && base_type.is_meta_type) { + // Enum type is treated as a dictionary value for function calls. + base_type.is_meta_type = false; + } + if (is_self && parser->current_function != nullptr && parser->current_function->is_static && !is_static) { push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parser->current_function->identifier->name), p_call->callee); } else if (!is_self && base_type.is_meta_type && !is_static) { @@ -2474,17 +2479,24 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { GDScriptParser::DataType cast_type = resolve_datatype(p_cast->cast_type); if (!cast_type.is_set()) { + mark_node_unsafe(p_cast); return; } - cast_type.is_meta_type = false; // The casted value won't be a type name. + cast_type = type_from_metatype(cast_type); // The casted value won't be a type name. p_cast->set_datatype(cast_type); if (!cast_type.is_variant()) { GDScriptParser::DataType op_type = p_cast->operand->get_datatype(); if (!op_type.is_variant()) { bool valid = false; - if (op_type.kind == GDScriptParser::DataType::BUILTIN && cast_type.kind == GDScriptParser::DataType::BUILTIN) { + if (op_type.kind == GDScriptParser::DataType::ENUM && cast_type.kind == GDScriptParser::DataType::ENUM) { + // Enum types are compatible between each other, so it's a safe cast. + valid = true; + } else if (op_type.kind == GDScriptParser::DataType::BUILTIN && op_type.builtin_type == Variant::INT && cast_type.kind == GDScriptParser::DataType::ENUM) { + // Convertint int to enum is always valid. + valid = true; + } else if (op_type.kind == GDScriptParser::DataType::BUILTIN && cast_type.kind == GDScriptParser::DataType::BUILTIN) { valid = Variant::can_convert(op_type.builtin_type, cast_type.builtin_type); } else if (op_type.kind != GDScriptParser::DataType::BUILTIN && cast_type.kind != GDScriptParser::DataType::BUILTIN) { valid = is_type_compatible(cast_type, op_type) || is_type_compatible(op_type, cast_type); @@ -2586,6 +2598,34 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod const StringName &name = p_identifier->name; + if (base.kind == GDScriptParser::DataType::ENUM) { + if (base.is_meta_type) { + if (base.enum_values.has(name)) { + p_identifier->is_constant = true; + p_identifier->reduced_value = base.enum_values[name]; + + GDScriptParser::DataType result; + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::ENUM; + result.is_constant = true; + result.builtin_type = Variant::INT; + result.native_type = base.native_type; + result.enum_type = base.enum_type; + p_identifier->set_datatype(result); + return; + } else { + // Consider as a Dictionary, so it can be anything. + // This will be evaluated in the next if block. + base.kind = GDScriptParser::DataType::BUILTIN; + base.builtin_type = Variant::DICTIONARY; + base.is_meta_type = false; + } + } else { + push_error(R"(Cannot get property from enum value.)", p_identifier); + return; + } + } + if (base.kind == GDScriptParser::DataType::BUILTIN) { if (base.is_meta_type) { bool valid = true; @@ -2632,32 +2672,6 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod return; } - if (base.kind == GDScriptParser::DataType::ENUM) { - if (base.is_meta_type) { - if (base.enum_values.has(name)) { - p_identifier->is_constant = true; - p_identifier->reduced_value = base.enum_values[name]; - - GDScriptParser::DataType result; - result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - result.kind = GDScriptParser::DataType::ENUM_VALUE; - result.is_constant = true; - result.builtin_type = Variant::INT; - result.native_type = base.native_type; - result.enum_type = name; - p_identifier->set_datatype(result); - } else { - // Consider as a Dictionary - GDScriptParser::DataType dummy; - dummy.kind = GDScriptParser::DataType::VARIANT; - p_identifier->set_datatype(dummy); - } - } else { - push_error(R"(Cannot get property from enum value.)", p_identifier); - } - return; - } - GDScriptParser::ClassNode *base_class = base.class_type; // TODO: Switch current class/function/suite here to avoid misrepresenting identifiers (in recursive reduce calls). @@ -2793,7 +2807,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident if (element.identifier->name == p_identifier->name) { GDScriptParser::DataType type; type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - type.kind = element.parent_enum->identifier ? GDScriptParser::DataType::ENUM_VALUE : GDScriptParser::DataType::BUILTIN; + type.kind = element.parent_enum->identifier ? GDScriptParser::DataType::ENUM : GDScriptParser::DataType::BUILTIN; type.builtin_type = Variant::INT; type.is_constant = true; if (element.parent_enum->identifier) { @@ -3493,6 +3507,9 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_metatype(const GDScriptPars GDScriptParser::DataType result = p_meta_type; result.is_meta_type = false; result.is_constant = false; + if (p_meta_type.kind == GDScriptParser::DataType::ENUM) { + result.builtin_type = Variant::INT; + } return result; } @@ -3549,6 +3566,18 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::CallNode *p_source r_default_arg_count = 0; StringName function_name = p_function; + if (p_base_type.kind == GDScriptParser::DataType::ENUM) { + if (p_base_type.is_meta_type) { + // Enum type can be treated as a dictionary value. + p_base_type.kind = GDScriptParser::DataType::BUILTIN; + p_base_type.builtin_type = Variant::DICTIONARY; + p_base_type.is_meta_type = false; + } else { + push_error("Cannot call function on enum value.", p_source); + return false; + } + } + if (p_base_type.kind == GDScriptParser::DataType::BUILTIN) { // Construct a base type to get methods. Callable::CallError err; @@ -3799,6 +3828,22 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator Variant::Type a_type = p_a.builtin_type; Variant::Type b_type = p_b.builtin_type; + + if (p_a.kind == GDScriptParser::DataType::ENUM) { + if (p_a.is_meta_type) { + a_type = Variant::DICTIONARY; + } else { + a_type = Variant::INT; + } + } + if (p_b.kind == GDScriptParser::DataType::ENUM) { + if (p_b.is_meta_type) { + b_type = Variant::DICTIONARY; + } else { + b_type = Variant::INT; + } + } + Variant::ValidatedOperatorEvaluator op_eval = Variant::get_validated_operator_evaluator(p_operation, a_type, b_type); bool hard_operation = p_a.is_hard_type() && p_b.is_hard_type(); @@ -3828,7 +3873,7 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator } // TODO: Add safe/unsafe return variable (for variant cases) -bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion) const { +bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion, const GDScriptParser::Node *p_source_node) { // These return "true" so it doesn't affect users negatively. ERR_FAIL_COND_V_MSG(!p_target.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset target type"); ERR_FAIL_COND_V_MSG(!p_source.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset value type"); @@ -3848,7 +3893,7 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ if (!valid && p_allow_implicit_conversion) { valid = Variant::can_convert_strict(p_source.builtin_type, p_target.builtin_type); } - if (!valid && p_target.builtin_type == Variant::INT && p_source.kind == GDScriptParser::DataType::ENUM_VALUE) { + if (!valid && p_target.builtin_type == Variant::INT && p_source.kind == GDScriptParser::DataType::ENUM && !p_source.is_meta_type) { // Enum value is also integer. valid = true; } @@ -3869,6 +3914,11 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ if (p_target.kind == GDScriptParser::DataType::ENUM) { if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) { +#ifdef DEBUG_ENABLED + if (p_source_node) { + parser->push_warning(p_source_node, GDScriptWarning::INT_ASSIGNED_TO_ENUM); + } +#endif return true; } if (p_source.kind == GDScriptParser::DataType::ENUM) { @@ -3876,11 +3926,6 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ return true; } } - if (p_source.kind == GDScriptParser::DataType::ENUM_VALUE) { - if (p_source.native_type == p_target.native_type && p_target.enum_values.has(p_source.enum_type)) { - return true; - } - } return false; } @@ -3935,7 +3980,6 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ case GDScriptParser::DataType::VARIANT: case GDScriptParser::DataType::BUILTIN: case GDScriptParser::DataType::ENUM: - case GDScriptParser::DataType::ENUM_VALUE: case GDScriptParser::DataType::UNRESOLVED: break; // Already solved before. } @@ -3972,7 +4016,6 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ case GDScriptParser::DataType::VARIANT: case GDScriptParser::DataType::BUILTIN: case GDScriptParser::DataType::ENUM: - case GDScriptParser::DataType::ENUM_VALUE: case GDScriptParser::DataType::UNRESOLVED: break; // Already solved before. } diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index 4cee5cb44a..2697a6ec2b 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -112,7 +112,7 @@ class GDScriptAnalyzer { GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, const GDScriptParser::DataType &p_b, bool &r_valid, const GDScriptParser::Node *p_source); GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, bool &r_valid, const GDScriptParser::Node *p_source); void update_array_literal_element_type(const GDScriptParser::DataType &p_base_type, GDScriptParser::ArrayNode *p_array_literal); - bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false) const; + bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr); void push_error(const String &p_message, const GDScriptParser::Node *p_origin); void mark_node_unsafe(const GDScriptParser::Node *p_node); bool class_exists(const StringName &p_class) const; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index ca125d3a07..108c988add 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -141,10 +141,13 @@ GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::D } } break; case GDScriptParser::DataType::ENUM: - case GDScriptParser::DataType::ENUM_VALUE: result.has_type = true; result.kind = GDScriptDataType::BUILTIN; - result.builtin_type = Variant::INT; + if (p_datatype.is_meta_type) { + result.builtin_type = Variant::DICTIONARY; + } else { + result.builtin_type = Variant::INT; + } break; case GDScriptParser::DataType::UNRESOLVED: { ERR_PRINT("Parser bug: converting unresolved type."); @@ -469,7 +472,14 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code } break; case GDScriptParser::Node::CAST: { const GDScriptParser::CastNode *cn = static_cast<const GDScriptParser::CastNode *>(p_expression); - GDScriptDataType cast_type = _gdtype_from_datatype(cn->cast_type->get_datatype()); + GDScriptParser::DataType og_cast_type = cn->cast_type->get_datatype(); + GDScriptDataType cast_type = _gdtype_from_datatype(og_cast_type); + + if (og_cast_type.kind == GDScriptParser::DataType::ENUM) { + // Enum types are usually treated as dictionaries, but in this case we want to cast to an integer. + cast_type.kind = GDScriptDataType::BUILTIN; + cast_type.builtin_type = Variant::INT; + } // Create temporary for result first since it will be deleted last. GDScriptCodeGenerator::Address result = codegen.add_temporary(cast_type); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index e04f6063ec..33a88dd2dd 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -610,7 +610,7 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio case GDScriptParser::Node::SUBSCRIPT: { const GDScriptParser::SubscriptNode *sub = static_cast<const GDScriptParser::SubscriptNode *>(par->default_value); if (sub->is_constant) { - if (sub->datatype.kind == GDScriptParser::DataType::ENUM_VALUE) { + if (sub->datatype.kind == GDScriptParser::DataType::ENUM) { def_val = sub->get_datatype().to_string(); } else if (sub->reduced) { const Variant::Type vt = sub->reduced_value.get_type(); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 460bd85a86..cfad832a6c 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3740,8 +3740,6 @@ String GDScriptParser::DataType::to_string() const { } case ENUM: return enum_type.operator String() + " (enum)"; - case ENUM_VALUE: - return enum_type.operator String() + " (enum value)"; case UNRESOLVED: return "<unresolved type>"; } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index e4311d2d5e..c09b07282f 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -106,8 +106,7 @@ public: NATIVE, SCRIPT, CLASS, // GDScript. - ENUM, // Full enumeration. - ENUM_VALUE, // Value from enumeration. + ENUM, // Enumeration. VARIANT, // Can be any type. UNRESOLVED, }; @@ -185,8 +184,6 @@ public: return builtin_type == p_other.builtin_type; case NATIVE: case ENUM: - return native_type == p_other.native_type; - case ENUM_VALUE: return native_type == p_other.native_type && enum_type == p_other.enum_type; case SCRIPT: return script_type == p_other.script_type; diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index 9977b88aa1..d3287ab345 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -312,22 +312,6 @@ GDScriptTokenizer::Token GDScriptTokenizer::pop_error() { return error; } -static bool _is_alphanumeric(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; -} - -static bool _is_digit(char32_t c) { - return (c >= '0' && c <= '9'); -} - -static bool _is_hex_digit(char32_t c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} - -static bool _is_binary_digit(char32_t c) { - return (c == '0' || c == '1'); -} - GDScriptTokenizer::Token GDScriptTokenizer::make_token(Token::Type p_type) { Token token(p_type); token.start_line = start_line; @@ -448,10 +432,10 @@ GDScriptTokenizer::Token GDScriptTokenizer::check_vcs_marker(char32_t p_test, To } GDScriptTokenizer::Token GDScriptTokenizer::annotation() { - if (!_is_alphanumeric(_peek())) { + if (!is_ascii_identifier_char(_peek())) { push_error("Expected annotation identifier after \"@\"."); } - while (_is_alphanumeric(_peek())) { + while (is_ascii_identifier_char(_peek())) { // Consume all identifier characters. _advance(); } @@ -526,7 +510,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { #define MAX_KEYWORD_LENGTH 10 // Consume all alphanumeric characters. - while (_is_alphanumeric(_peek())) { + while (is_ascii_identifier_char(_peek())) { _advance(); } @@ -612,7 +596,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { bool has_decimal = false; bool has_exponent = false; bool has_error = false; - bool (*digit_check_func)(char32_t) = _is_digit; + bool (*digit_check_func)(char32_t) = is_digit; if (_peek(-1) == '.') { has_decimal = true; @@ -620,20 +604,20 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { if (_peek() == 'x') { // Hexadecimal. base = 16; - digit_check_func = _is_hex_digit; + digit_check_func = is_hex_digit; _advance(); } else if (_peek() == 'b') { // Binary. base = 2; - digit_check_func = _is_binary_digit; + digit_check_func = is_binary_digit; _advance(); } } // Allow '_' to be used in a number, for readability. bool previous_was_underscore = false; - while (digit_check_func(_peek()) || _peek() == '_') { - if (_peek() == '_') { + while (digit_check_func(_peek()) || is_underscore(_peek())) { + if (is_underscore(_peek())) { if (previous_was_underscore) { Token error = make_error(R"(Only one underscore can be used as a numeric separator.)"); error.start_column = column; @@ -682,7 +666,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { _advance(); // Consume decimal digits. - while (_is_digit(_peek()) || _peek() == '_') { + while (is_digit(_peek()) || is_underscore(_peek())) { _advance(); } } @@ -696,7 +680,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { _advance(); } // Consume exponent digits. - if (!_is_digit(_peek())) { + if (!is_digit(_peek())) { Token error = make_error(R"(Expected exponent value after "e".)"); error.start_column = column; error.leftmost_column = column; @@ -705,8 +689,8 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { push_error(error); } previous_was_underscore = false; - while (_is_digit(_peek()) || _peek() == '_') { - if (_peek() == '_') { + while (is_digit(_peek()) || is_underscore(_peek())) { + if (is_underscore(_peek())) { if (previous_was_underscore) { Token error = make_error(R"(Only one underscore can be used as a numeric separator.)"); error.start_column = column; @@ -733,7 +717,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { error.rightmost_column = column + 1; push_error(error); has_error = true; - } else if (_is_alphanumeric(_peek())) { + } else if (is_ascii_identifier_char(_peek())) { // Letter at the end of the number. push_error("Invalid numeric notation."); } @@ -865,7 +849,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::string() { char32_t digit = _peek(); char32_t value = 0; - if (digit >= '0' && digit <= '9') { + if (is_digit(digit)) { value = digit - '0'; } else if (digit >= 'a' && digit <= 'f') { value = digit - 'a'; @@ -1322,9 +1306,9 @@ GDScriptTokenizer::Token GDScriptTokenizer::scan() { line_continuation = false; - if (_is_digit(c)) { + if (is_digit(c)) { return number(); - } else if (_is_alphanumeric(c)) { + } else if (is_ascii_identifier_char(c)) { return potential_identifier(); } @@ -1392,7 +1376,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::scan() { if (_peek() == '.') { _advance(); return make_token(Token::PERIOD_PERIOD); - } else if (_is_digit(_peek())) { + } else if (is_digit(_peek())) { // Number starting with '.'. return number(); } else { diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 73536f5f8e..ad96e36640 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -152,6 +152,9 @@ String GDScriptWarning::get_message() const { CHECK_SYMBOLS(3); return vformat(R"(The %s '%s' has the same name as a %s.)", symbols[0], symbols[1], symbols[2]); } + case INT_ASSIGNED_TO_ENUM: { + return "Integer used when an enum value is expected. If this is intended cast the integer to the enum type."; + } case WARNING_MAX: break; // Can't happen, but silences warning } @@ -199,6 +202,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "REDUNDANT_AWAIT", "EMPTY_FILE", "SHADOWED_GLOBAL_IDENTIFIER", + "INT_ASSIGNED_TO_ENUM", }; static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 112b40781a..82efe3568f 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -70,6 +70,7 @@ public: REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine). EMPTY_FILE, // A script file is empty. SHADOWED_GLOBAL_IDENTIFIER, // A global class or function has the same name as variable. + INT_ASSIGNED_TO_ENUM, // An integer value was assigned to an enum-typed variable without casting. WARNING_MAX, }; diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index 49f5303ae6..17886181d5 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -541,7 +541,7 @@ String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position & for (int c = p_position.character; c >= 0; c--) { start_pos = c; char32_t ch = line[c]; - bool valid_char = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; + bool valid_char = is_ascii_identifier_char(ch); if (!valid_char) { break; } @@ -550,7 +550,7 @@ String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position & int end_pos = p_position.character; for (int c = p_position.character; c < line.length(); c++) { char32_t ch = line[c]; - bool valid_char = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; + bool valid_char = is_ascii_identifier_char(ch); if (!valid_char) { break; } diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index 47772b8039..73a424dae4 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -134,12 +134,14 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l if (do_init_languages) { init_language(p_source_dir); } +#ifdef DEBUG_ENABLED // Enable all warnings for GDScript, so we can test them. ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { String warning = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)i).to_lower(); ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/" + warning, true); } +#endif // Enable printing to show results _print_line_enabled = true; @@ -153,6 +155,21 @@ GDScriptTestRunner::~GDScriptTestRunner() { } } +#ifndef DEBUG_ENABLED +static String strip_warnings(const String &p_expected) { + // On release builds we don't have warnings. Here we remove them from the output before comparison + // so it doesn't fail just because of difference in warnings. + String expected_no_warnings; + for (String line : p_expected.split("\n")) { + if (line.begins_with(">> ")) { + continue; + } + expected_no_warnings += line + "\n"; + } + return expected_no_warnings.strip_edges() + "\n"; +} +#endif + int GDScriptTestRunner::run_tests() { if (!make_tests()) { FAIL("An error occurred while making the tests."); @@ -170,6 +187,9 @@ int GDScriptTestRunner::run_tests() { GDScriptTest::TestResult result = test.run_test(); String expected = FileAccess::get_file_as_string(test.get_output_file()); +#ifndef DEBUG_ENABLED + expected = strip_warnings(expected); +#endif INFO(test.get_source_file()); if (!result.passed) { INFO(expected); @@ -233,6 +253,22 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { } } else { if (next.get_extension().to_lower() == "gd") { +#ifndef DEBUG_ENABLED + // On release builds, skip tests marked as debug only. + Error open_err = OK; + FileAccessRef script_file(FileAccess::open(current_dir.plus_file(next), FileAccess::READ, &open_err)); + if (open_err != OK) { + ERR_PRINT(vformat(R"(Couldn't open test file "%s".)", next)); + next = dir->get_next(); + continue; + } else { + if (script_file->get_line() == "#debug-only") { + next = dir->get_next(); + continue; + } + } +#endif + String out_file = next.get_basename() + ".out"; if (!is_generating && !dir->file_exists(out_file)) { ERR_FAIL_V_MSG(false, "Could not find output file for " + next); @@ -387,6 +423,10 @@ bool GDScriptTest::check_output(const String &p_output) const { String got = p_output.strip_edges(); // TODO: may be hacky. got += "\n"; // Make sure to insert newline for CI static checks. +#ifndef DEBUG_ENABLED + expected = strip_warnings(expected); +#endif + return got == expected; } @@ -469,6 +509,7 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { return result; } +#ifdef DEBUG_ENABLED StringBuilder warning_string; for (const GDScriptWarning &E : parser.get_warnings()) { const GDScriptWarning warning = E; @@ -482,6 +523,7 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { warning_string.append("\n"); } result.output += warning_string.as_string(); +#endif // Test compiling. GDScriptCompiler compiler; diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.gd new file mode 100644 index 0000000000..928c886650 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.gd @@ -0,0 +1,10 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } +enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 } + +# Different enum types can't be assigned without casting. +var class_var: MyEnum = MyEnum.ENUM_VALUE_1 + +func test(): + print(class_var) + class_var = MyOtherEnum.OTHER_ENUM_VALUE_2 + print(class_var) diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out new file mode 100644 index 0000000000..fde7e92f8c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot assign a value of type "MyOtherEnum (enum)" to a target of type "MyEnum (enum)". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.gd new file mode 100644 index 0000000000..03a1711d7b --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.gd @@ -0,0 +1,8 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } +enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 } + +# Different enum types can't be assigned without casting. +var class_var: MyEnum = MyOtherEnum.OTHER_ENUM_VALUE_1 + +func test(): + print(class_var) diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out new file mode 100644 index 0000000000..b1710c798d --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Value of type "MyOtherEnum (enum)" cannot be assigned to a variable of type "MyEnum (enum)". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.gd new file mode 100644 index 0000000000..d08d3dd7b2 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.gd @@ -0,0 +1,8 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } +enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 } + +func test(): + var local_var: MyEnum = MyEnum.ENUM_VALUE_1 + print(local_var) + local_var = MyOtherEnum.OTHER_ENUM_VALUE_2 + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out new file mode 100644 index 0000000000..fde7e92f8c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot assign a value of type "MyOtherEnum (enum)" to a target of type "MyEnum (enum)". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.gd new file mode 100644 index 0000000000..ca6d892218 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.gd @@ -0,0 +1,6 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } +enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 } + +func test(): + var local_var: MyEnum = MyOtherEnum.OTHER_ENUM_VALUE_1 + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out new file mode 100644 index 0000000000..b1710c798d --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Value of type "MyOtherEnum (enum)" cannot be assigned to a variable of type "MyEnum (enum)". diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.gd new file mode 100644 index 0000000000..edb785c8b6 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.gd @@ -0,0 +1,13 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } + +var class_var: int = MyEnum.ENUM_VALUE_1 + +func test(): + print(class_var) + class_var = MyEnum.ENUM_VALUE_2 + print(class_var) + + var local_var: int = MyEnum.ENUM_VALUE_1 + print(local_var) + local_var = MyEnum.ENUM_VALUE_2 + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.out b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.out new file mode 100644 index 0000000000..5f53802c33 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_enum_to_int_typed_var.out @@ -0,0 +1,5 @@ +GDTEST_OK +0 +1 +0 +1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.gd new file mode 100644 index 0000000000..726e4fd413 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.gd @@ -0,0 +1,13 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } + +var class_var: MyEnum = 0 as MyEnum + +func test(): + print(class_var) + class_var = 1 as MyEnum + print(class_var) + + var local_var: MyEnum = 0 as MyEnum + print(local_var) + local_var = 1 as MyEnum + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.out b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.out new file mode 100644 index 0000000000..5f53802c33 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_int_cast_to_same_enum.out @@ -0,0 +1,5 @@ +GDTEST_OK +0 +1 +0 +1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.gd new file mode 100644 index 0000000000..798912c987 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.gd @@ -0,0 +1,14 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } +enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 } + +var class_var: MyEnum = MyOtherEnum.OTHER_ENUM_VALUE_1 as MyEnum + +func test(): + print(class_var) + class_var = MyOtherEnum.OTHER_ENUM_VALUE_2 as MyEnum + print(class_var) + + var local_var: MyEnum = MyOtherEnum.OTHER_ENUM_VALUE_1 as MyEnum + print(local_var) + local_var = MyOtherEnum.OTHER_ENUM_VALUE_2 as MyEnum + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.out b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.out new file mode 100644 index 0000000000..5f53802c33 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_other_enum_cast_to_same_enum.out @@ -0,0 +1,5 @@ +GDTEST_OK +0 +1 +0 +1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.gd new file mode 100644 index 0000000000..2bfb318c3c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.gd @@ -0,0 +1,13 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } + +var class_var: MyEnum = MyEnum.ENUM_VALUE_1 + +func test(): + print(class_var) + class_var = MyEnum.ENUM_VALUE_2 + print(class_var) + + var local_var: MyEnum = MyEnum.ENUM_VALUE_1 + print(local_var) + local_var = MyEnum.ENUM_VALUE_2 + print(local_var) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.out b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.out new file mode 100644 index 0000000000..5f53802c33 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_assign_same_enum.out @@ -0,0 +1,5 @@ +GDTEST_OK +0 +1 +0 +1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.gd new file mode 100644 index 0000000000..7022d14566 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.gd @@ -0,0 +1,21 @@ +# Enum is equivalent to int for comparisons and operations. +enum MyEnum { + ZERO, + ONE, + TWO, +} + +enum OtherEnum { + ZERO, + ONE, + TWO, +} + +func test(): + print(MyEnum.ZERO == OtherEnum.ZERO) + print(MyEnum.ZERO == 1) + print(MyEnum.ZERO != OtherEnum.ONE) + print(MyEnum.ZERO != 0) + + print(MyEnum.ONE + OtherEnum.TWO) + print(2 - MyEnum.ONE) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.out b/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.out new file mode 100644 index 0000000000..c8f34c11db --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_is_treated_as_int.out @@ -0,0 +1,7 @@ +GDTEST_OK +true +false +true +false +3 +1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.gd new file mode 100644 index 0000000000..885d70408a --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.gd @@ -0,0 +1,13 @@ +enum MyEnum { + ZERO, + ONE, + TWO, +} + +func test(): + for key in MyEnum.keys(): + prints(key, MyEnum[key]) + + # https://github.com/godotengine/godot/issues/55491 + for key in MyEnum: + prints(key, MyEnum[key]) diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.out b/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.out new file mode 100644 index 0000000000..d29f53109c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enum_type_is_treated_as_dictionary.out @@ -0,0 +1,7 @@ +GDTEST_OK +ZERO 0 +ONE 1 +TWO 2 +ZERO 0 +ONE 1 +TWO 2 diff --git a/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.gd b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.gd new file mode 100644 index 0000000000..2be1024214 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.gd @@ -0,0 +1,15 @@ +enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 } + +# Assigning int value to enum-typed variable without explicit cast causes a warning. +# While it is valid it may be a mistake in the assignment. +var class_var: MyEnum = 0 + +func test(): + print(class_var) + class_var = 1 + print(class_var) + + var local_var: MyEnum = 0 + print(local_var) + local_var = 1 + print(local_var) diff --git a/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out new file mode 100644 index 0000000000..eef13bbff8 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out @@ -0,0 +1,21 @@ +GDTEST_OK +>> WARNING +>> Line: 5 +>> INT_ASSIGNED_TO_ENUM +>> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. +>> WARNING +>> Line: 9 +>> INT_ASSIGNED_TO_ENUM +>> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. +>> WARNING +>> Line: 12 +>> INT_ASSIGNED_TO_ENUM +>> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. +>> WARNING +>> Line: 14 +>> INT_ASSIGNED_TO_ENUM +>> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. +0 +1 +0 +1 diff --git a/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.gd b/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.gd index 10780b5379..7b3c112fe9 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.gd +++ b/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.gd @@ -1,3 +1,4 @@ +#debug-only func test(): var node := Node.new() var inside_tree = node.is_inside_tree diff --git a/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.out b/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.out index e585c374e2..fe48ade26b 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.out +++ b/modules/gdscript/tests/scripts/runtime/errors/callable_call_after_free_object.out @@ -2,5 +2,5 @@ GDTEST_RUNTIME_ERROR >> SCRIPT ERROR >> on function: test() >> runtime/errors/callable_call_after_free_object.gd ->> 5 +>> 6 >> Attempt to call function 'null::is_inside_tree (Callable)' on a null instance. diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index e54e51eb8d..baa39a3b80 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -50,6 +50,7 @@ #include "core/io/file_access.h" #include "core/io/file_access_memory.h" #include "core/io/json.h" +#include "core/io/stream_peer.h" #include "core/math/disjoint_set.h" #include "core/math/vector2.h" #include "core/variant/dictionary.h" diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index f29b339c0e..7a4641dbbc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -183,7 +183,7 @@ namespace GodotTools.OpenVisualStudio continue; // The digits after the colon are the process ID - if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.16.0:[0-9]")) + if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.1[6-7].0:[0-9]")) continue; if (pprot.GetObject(moniker[0], out object ppunkObject) == 0) diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 1b4ab0ef4b..1de41821f9 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -690,11 +690,11 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI continue; } - if (parts[curr_prefix_length][0] >= '0' && parts[curr_prefix_length][0] <= '9') { + if (is_digit(parts[curr_prefix_length][0])) { // The name of enum constants may begin with a numeric digit when strip from the enum prefix, // so we make the prefix for this constant one word shorter in those cases. for (curr_prefix_length = curr_prefix_length - 1; curr_prefix_length > 0; curr_prefix_length--) { - if (parts[curr_prefix_length][0] < '0' || parts[curr_prefix_length][0] > '9') { + if (!is_digit(parts[curr_prefix_length][0])) { break; } } diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index e50a5337cb..c7511f587e 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -303,22 +303,6 @@ _FORCE_INLINE_ bool is_connected_to_prev(char32_t p_chr, char32_t p_pchr) { return (prop != U_JT_RIGHT_JOINING) && (prop != U_JT_NON_JOINING) ? !is_ligature(p_pchr, p_chr) : false; } -_FORCE_INLINE_ bool is_control(char32_t p_char) { - return (p_char <= 0x001f) || (p_char >= 0x007f && p_char <= 0x009F); -} - -_FORCE_INLINE_ bool is_whitespace(char32_t p_char) { - return (p_char == 0x0020) || (p_char == 0x00A0) || (p_char == 0x1680) || (p_char >= 0x2000 && p_char <= 0x200a) || (p_char == 0x202f) || (p_char == 0x205f) || (p_char == 0x3000) || (p_char == 0x2028) || (p_char == 0x2029) || (p_char >= 0x0009 && p_char <= 0x000d) || (p_char == 0x0085); -} - -_FORCE_INLINE_ bool is_linebreak(char32_t p_char) { - return (p_char >= 0x000a && p_char <= 0x000d) || (p_char == 0x0085) || (p_char == 0x2028) || (p_char == 0x2029); -} - -_FORCE_INLINE_ bool is_underscore(char32_t p_char) { - return (p_char == 0x005F); -} - /*************************************************************************/ String TextServerAdvanced::interface_name = "ICU / HarfBuzz / Graphite"; diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index dd520a2e40..182d2a02ad 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -44,30 +44,6 @@ #endif /*************************************************************************/ -/* Character properties. */ -/*************************************************************************/ - -_FORCE_INLINE_ bool is_control(char32_t p_char) { - return (p_char <= 0x001f) || (p_char >= 0x007f && p_char <= 0x009F); -} - -_FORCE_INLINE_ bool is_whitespace(char32_t p_char) { - return (p_char == 0x0020) || (p_char == 0x00A0) || (p_char == 0x1680) || (p_char >= 0x2000 && p_char <= 0x200a) || (p_char == 0x202f) || (p_char == 0x205f) || (p_char == 0x3000) || (p_char == 0x2028) || (p_char == 0x2029) || (p_char >= 0x0009 && p_char <= 0x000d) || (p_char == 0x0085); -} - -_FORCE_INLINE_ bool is_linebreak(char32_t p_char) { - return (p_char >= 0x000a && p_char <= 0x000d) || (p_char == 0x0085) || (p_char == 0x2028) || (p_char == 0x2029); -} - -_FORCE_INLINE_ bool is_punct(char32_t p_char) { - return (p_char >= 0x0020 && p_char <= 0x002F) || (p_char >= 0x003A && p_char <= 0x0040) || (p_char >= 0x005B && p_char <= 0x005E) || (p_char == 0x0060) || (p_char >= 0x007B && p_char <= 0x007E) || (p_char >= 0x2000 && p_char <= 0x206F) || (p_char >= 0x3000 && p_char <= 0x303F); -} - -_FORCE_INLINE_ bool is_underscore(char32_t p_char) { - return (p_char == 0x005F); -} - -/*************************************************************************/ String TextServerFallback::interface_name = "Fallback"; uint32_t TextServerFallback::interface_features = 0; // Nothing is supported. diff --git a/modules/visual_script/visual_script_expression.cpp b/modules/visual_script/visual_script_expression.cpp index 17a3566ed2..e8942b9788 100644 --- a/modules/visual_script/visual_script_expression.cpp +++ b/modules/visual_script/visual_script_expression.cpp @@ -377,13 +377,13 @@ Error VisualScriptExpression::_get_token(Token &r_token) { r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + if (!is_hex_digit(c)) { _set_error("Malformed hex constant in string"); r_token.type = TK_ERROR; return ERR_PARSE_ERROR; } char32_t v; - if (c >= '0' && c <= '9') { + if (is_digit(c)) { v = c - '0'; } else if (c >= 'a' && c <= 'f') { v = c - 'a'; @@ -457,7 +457,7 @@ Error VisualScriptExpression::_get_token(Token &r_token) { break; } - if (cchar >= '0' && cchar <= '9') { + if (is_digit(cchar)) { //a number String num; @@ -476,7 +476,7 @@ Error VisualScriptExpression::_get_token(Token &r_token) { while (true) { switch (reading) { case READING_INT: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { //pass } else if (c == '.') { reading = READING_DEC; @@ -489,7 +489,7 @@ Error VisualScriptExpression::_get_token(Token &r_token) { } break; case READING_DEC: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { } else if (c == 'e') { reading = READING_EXP; @@ -499,7 +499,7 @@ Error VisualScriptExpression::_get_token(Token &r_token) { } break; case READING_EXP: { - if (c >= '0' && c <= '9') { + if (is_digit(c)) { exp_beg = true; } else if ((c == '-' || c == '+') && !exp_sign && !exp_beg) { @@ -532,11 +532,11 @@ Error VisualScriptExpression::_get_token(Token &r_token) { } return OK; - } else if ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_') { + } else if (is_ascii_char(cchar) || cchar == '_') { String id; bool first = true; - while ((cchar >= 'A' && cchar <= 'Z') || (cchar >= 'a' && cchar <= 'z') || cchar == '_' || (!first && cchar >= '0' && cchar <= '9')) { + while (is_ascii_char(cchar) || cchar == '_' || (!first && is_digit(cchar))) { id += String::chr(cchar); cchar = GET_CHAR(); first = false; diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 3d0dabc56e..b0f16337ed 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -161,6 +161,16 @@ int DisplayServerAndroid::screen_get_dpi(int p_screen) const { return godot_io_java->get_screen_dpi(); } +float DisplayServerAndroid::screen_get_refresh_rate(int p_screen) const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + if (!godot_io_java) { + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; + } + + return godot_io_java->get_screen_refresh_rate(SCREEN_REFRESH_RATE_FALLBACK); +} + bool DisplayServerAndroid::screen_is_touchscreen(int p_screen) const { return true; } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index e52e07bf1a..23077a6529 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -106,6 +106,7 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override; diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 61d2f897ef..df2d32e152 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -416,10 +416,10 @@ String EditorExportPlatformAndroid::get_package_name(const String &p_package) co bool first = true; for (int i = 0; i < basename.length(); i++) { char32_t c = basename[i]; - if (c >= '0' && c <= '9' && first) { + if (is_digit(c) && first) { continue; } - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + if (is_ascii_alphanumeric_char(c)) { name += String::chr(c); first = false; } @@ -462,19 +462,19 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, first = true; continue; } - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) { + if (!is_ascii_identifier_char(c)) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Android application package names."), String::chr(c)); } return false; } - if (first && (c >= '0' && c <= '9')) { + if (first && is_digit(c)) { if (r_error) { *r_error = TTR("A digit cannot be the first character in a package segment."); } return false; } - if (first && c == '_') { + if (first && is_underscore(c)) { if (r_error) { *r_error = vformat(TTR("The character '%s' cannot be the first character in a package segment."), String::chr(c)); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index d679fd92c0..b151e7eec1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -226,6 +226,14 @@ public class GodotIO { return (int)(metrics.density * 160f); } + public double getScreenRefreshRate(double fallback) { + Display display = activity.getWindowManager().getDefaultDisplay(); + if (display != null) { + return display.getRefreshRate(); + } + return fallback; + } + public int[] screenGetUsableRect() { DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); Display display = activity.getWindowManager().getDefaultDisplay(); diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index e0a535f16e..8a2788e848 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -53,6 +53,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); + _get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D"); _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); @@ -136,6 +137,19 @@ int GodotIOJavaWrapper::get_screen_dpi() { } } +float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { + if (_get_screen_refresh_rate) { + JNIEnv *env = get_jni_env(); + if (env == nullptr) { + ERR_PRINT("An error occured while trying to get screen refresh rate."); + return fallback; + } + return (float)env->CallDoubleMethod(godot_io_instance, _get_screen_refresh_rate, (double)fallback); + } + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return fallback; +} + void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { if (_screen_get_usable_rect) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index c96abf1101..38a2b710a9 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -51,6 +51,7 @@ private: jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; + jmethodID _get_screen_refresh_rate = 0; jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; jmethodID _show_keyboard = 0; @@ -71,6 +72,7 @@ public: String get_locale(); String get_model(); int get_screen_dpi(); + float get_screen_refresh_rate(float fallback); void screen_get_usable_rect(int (&p_rect_xywh)[4]); String get_unique_id(); bool has_vk(); diff --git a/platform/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h index 6434483641..7441550f67 100644 --- a/platform/iphone/display_server_iphone.h +++ b/platform/iphone/display_server_iphone.h @@ -129,6 +129,7 @@ public: virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Vector<DisplayServer::WindowID> get_window_list() const override; diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index 48bda89fc3..9491c9cf90 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -393,6 +393,10 @@ int DisplayServerIPhone::screen_get_dpi(int p_screen) const { } } +float DisplayServerIPhone::screen_get_refresh_rate(int p_screen) const { + return [UIScreen mainScreen].maximumFramesPerSecond; +} + float DisplayServerIPhone::screen_get_scale(int p_screen) const { return [UIScreen mainScreen].nativeScale; } diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index 756bca14dd..93b23f7ee2 100644 --- a/platform/iphone/export/export_plugin.h +++ b/platform/iphone/export/export_plugin.h @@ -130,7 +130,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { for (int i = 0; i < pname.length(); i++) { char32_t c = pname[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { + if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); } diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 2842fc2f5e..a0e1246c55 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -663,7 +663,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive godot_js_config_canvas_id_get(canvas_id, 256); // Handle contextmenu, webglcontextlost - godot_js_display_setup_canvas(p_resolution.x, p_resolution.y, p_window_mode == WINDOW_MODE_FULLSCREEN, OS::get_singleton()->is_hidpi_allowed() ? 1 : 0); + godot_js_display_setup_canvas(p_resolution.x, p_resolution.y, (p_window_mode == WINDOW_MODE_FULLSCREEN || p_window_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), OS::get_singleton()->is_hidpi_allowed() ? 1 : 0); // Check if it's windows. swap_cancel_ok = godot_js_display_is_swap_ok_cancel() == 1; @@ -794,6 +794,10 @@ float DisplayServerJavaScript::screen_get_scale(int p_screen) const { return godot_js_display_pixel_ratio_get(); } +float DisplayServerJavaScript::screen_get_refresh_rate(int p_screen) const { + return SCREEN_REFRESH_RATE_FALLBACK; // Javascript doesn't have much of a need for the screen refresh rate, and there's no native way to do so. +} + Vector<DisplayServer::WindowID> DisplayServerJavaScript::get_window_list() const { Vector<WindowID> ret; ret.push_back(MAIN_WINDOW_ID); @@ -897,6 +901,7 @@ void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_wind } window_mode = WINDOW_MODE_WINDOWED; } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { int result = godot_js_display_fullscreen_request(); ERR_FAIL_COND_MSG(result, "The request was denied. Remember that enabling fullscreen is only possible from an input callback for the HTML5 platform."); diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index 1ae5d68787..b50956d91c 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -139,6 +139,7 @@ public: virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override; virtual void virtual_keyboard_hide() override; diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 198eacd1f3..844b5616c4 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -1054,6 +1054,66 @@ int DisplayServerX11::screen_get_dpi(int p_screen) const { return 96; } +float DisplayServerX11::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + if (p_screen == SCREEN_OF_MAIN_WINDOW) { + p_screen = window_get_current_screen(); + } + + //invalid screen? + ERR_FAIL_INDEX_V(p_screen, get_screen_count(), SCREEN_REFRESH_RATE_FALLBACK); + + //Use xrandr to get screen refresh rate. + if (xrandr_ext_ok) { + XRRScreenResources *screen_info = XRRGetScreenResources(x11_display, windows[MAIN_WINDOW_ID].x11_window); + if (screen_info) { + RRMode current_mode = 0; + xrr_monitor_info *monitors = nullptr; + + if (xrr_get_monitors) { + int count = 0; + monitors = xrr_get_monitors(x11_display, windows[MAIN_WINDOW_ID].x11_window, true, &count); + ERR_FAIL_INDEX_V(p_screen, count, SCREEN_REFRESH_RATE_FALLBACK); + } else { + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; + } + + bool found_active_mode = false; + for (int crtc = 0; crtc < screen_info->ncrtc; crtc++) { // Loop through outputs to find which one is currently outputting. + XRRCrtcInfo *monitor_info = XRRGetCrtcInfo(x11_display, screen_info, screen_info->crtcs[crtc]); + if (monitor_info->x != monitors[p_screen].x || monitor_info->y != monitors[p_screen].y) { // If X and Y aren't the same as the monitor we're looking for, this isn't the right monitor. Continue. + continue; + } + + if (monitor_info->mode != None) { + current_mode = monitor_info->mode; + found_active_mode = true; + break; + } + } + + if (found_active_mode) { + for (int mode = 0; mode < screen_info->nmode; mode++) { + XRRModeInfo m_info = screen_info->modes[mode]; + if (m_info.id == current_mode) { + return (float)m_info.dotClock / ((float)m_info.hTotal * (float)m_info.vTotal); + } + } + } + + ERR_PRINT("An error occured while trying to get the screen refresh rate."); // We should have returned the refresh rate by now. An error must have occured. + return SCREEN_REFRESH_RATE_FALLBACK; + } else { + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; + } + } + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; +} + bool DisplayServerX11::screen_is_touchscreen(int p_screen) const { _THREAD_SAFE_METHOD_ @@ -1825,6 +1885,7 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { XSendEvent(x11_display, DefaultRootWindow(x11_display), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev); } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { //Remove full-screen wd.fullscreen = false; @@ -1877,6 +1938,7 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { XSendEvent(x11_display, DefaultRootWindow(x11_display), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev); } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { wd.last_position_before_fs = wd.position; @@ -2417,7 +2479,7 @@ Key DisplayServerX11::keyboard_get_keycode_from_physical(Key p_keycode) const { Key keycode_no_mod = p_keycode & KeyModifierMask::CODE_MASK; unsigned int xkeycode = KeyMappingX11::get_xlibcode(keycode_no_mod); KeySym xkeysym = XkbKeycodeToKeysym(x11_display, xkeycode, 0, 0); - if (xkeysym >= 'a' && xkeysym <= 'z') { + if (is_ascii_lower_case(xkeysym)) { xkeysym -= ('a' - 'A'); } diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index c17e120db5..2d07361deb 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -303,6 +303,7 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; #if defined(DBUS_ENABLED) diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index 719a45c2a8..4fc733fe2c 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -230,6 +230,7 @@ public: virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_max_scale() const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Vector<int> get_window_list() const override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index f7add5b688..000be79852 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -2203,6 +2203,24 @@ Rect2i DisplayServerOSX::screen_get_usable_rect(int p_screen) const { return Rect2i(); } +float DisplayServerOSX::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + if (p_screen == SCREEN_OF_MAIN_WINDOW) { + p_screen = window_get_current_screen(); + } + + NSArray *screenArray = [NSScreen screens]; + if ((NSUInteger)p_screen < [screenArray count]) { + NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; + const CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + const double displayRefreshRate = CGDisplayModeGetRefreshRate(displayMode); + return (float)displayRefreshRate; + } + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; +} + Vector<DisplayServer::WindowID> DisplayServerOSX::get_window_list() const { _THREAD_SAFE_METHOD_ @@ -2652,6 +2670,7 @@ void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { case WINDOW_MODE_MINIMIZED: { [wd.window_object deminiaturize:nil]; } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { [wd.window_object setLevel:NSNormalWindowLevel]; if (wd.layered_window) { @@ -2685,6 +2704,7 @@ void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { case WINDOW_MODE_MINIMIZED: { [wd.window_object performMiniaturize:nil]; } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { if (wd.layered_window) _set_window_per_pixel_transparency_enabled(false, p_window); diff --git a/platform/osx/export/export_plugin.h b/platform/osx/export/export_plugin.h index 0c2ac90206..931ce7e41a 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -87,7 +87,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { for (int i = 0; i < pname.length(); i++) { char32_t c = pname[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { + if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); } diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index d288c27016..b340129a16 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -326,6 +326,12 @@ typedef struct { Rect2i rect; } EnumRectData; +typedef struct { + int count; + int screen; + float rate; +} EnumRefreshRateData; + static BOOL CALLBACK _MonitorEnumProcSize(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { EnumSizeData *data = (EnumSizeData *)dwData; if (data->count == data->screen) { @@ -363,6 +369,26 @@ static BOOL CALLBACK _MonitorEnumProcUsableSize(HMONITOR hMonitor, HDC hdcMonito return TRUE; } +static BOOL CALLBACK _MonitorEnumProcRefreshRate(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { + EnumRefreshRateData *data = (EnumRefreshRateData *)dwData; + if (data->count == data->screen) { + MONITORINFOEXW minfo; + memset(&minfo, 0, sizeof(minfo)); + minfo.cbSize = sizeof(minfo); + GetMonitorInfoW(hMonitor, &minfo); + + DEVMODEW dm; + memset(&dm, 0, sizeof(dm)); + dm.dmSize = sizeof(dm); + EnumDisplaySettingsW(minfo.szDevice, ENUM_CURRENT_SETTINGS, &dm); + + data->rate = dm.dmDisplayFrequency; + } + + data->count++; + return TRUE; +} + Rect2i DisplayServerWindows::screen_get_usable_rect(int p_screen) const { _THREAD_SAFE_METHOD_ @@ -446,6 +472,13 @@ int DisplayServerWindows::screen_get_dpi(int p_screen) const { EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcDpi, (LPARAM)&data); return data.dpi; } +float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + EnumRefreshRateData data = { 0, p_screen == SCREEN_OF_MAIN_WINDOW ? window_get_current_screen() : p_screen, SCREEN_REFRESH_RATE_FALLBACK }; + EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcRefreshRate, (LPARAM)&data); + return data.rate; +} bool DisplayServerWindows::screen_is_touchscreen(int p_screen) const { #ifndef _MSC_VER @@ -506,7 +539,7 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) { wd.borderless = true; } - if (p_flags & WINDOW_FLAG_ALWAYS_ON_TOP_BIT && p_mode != WINDOW_MODE_FULLSCREEN) { + if (p_flags & WINDOW_FLAG_ALWAYS_ON_TOP_BIT && p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { wd.always_on_top = true; } if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { @@ -946,7 +979,7 @@ Size2i DisplayServerWindows::window_get_real_size(WindowID p_window) const { return Size2(); } -void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) { +void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) { // Windows docs for window styles: // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles @@ -959,6 +992,9 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre if (p_fullscreen || p_borderless) { r_style |= WS_POPUP; // p_borderless was WS_EX_TOOLWINDOW in the past. + if (p_fullscreen && p_multiwindow_fs) { + r_style |= WS_BORDER; // Allows child windows to be displayed on top of full screen. + } } else { if (p_resizable) { if (p_maximized) { @@ -989,7 +1025,7 @@ void DisplayServerWindows::_update_window_style(WindowID p_window, bool p_repain DWORD style = 0; DWORD style_ex = 0; - _get_window_style(p_window == MAIN_WINDOW_ID, wd.fullscreen, wd.borderless, wd.resizable, wd.maximized, wd.no_focus, style, style_ex); + _get_window_style(p_window == MAIN_WINDOW_ID, wd.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.maximized, wd.no_focus, style, style_ex); SetWindowLongPtr(wd.hWnd, GWL_STYLE, style); SetWindowLongPtr(wd.hWnd, GWL_EXSTYLE, style_ex); @@ -1009,10 +1045,11 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd = windows[p_window]; - if (wd.fullscreen && p_mode != WINDOW_MODE_FULLSCREEN) { + if (wd.fullscreen && p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { RECT rect; wd.fullscreen = false; + wd.multiwindow_fs = false; wd.maximized = wd.was_maximized; if (wd.pre_fs_valid) { @@ -1051,7 +1088,15 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) wd.minimized = true; } - if (p_mode == WINDOW_MODE_FULLSCREEN && !wd.fullscreen) { + if (p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { + wd.multiwindow_fs = false; + _update_window_style(false); + } else { + wd.multiwindow_fs = true; + _update_window_style(false); + } + + if ((p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) && !wd.fullscreen) { if (wd.minimized) { ShowWindow(wd.hWnd, SW_RESTORE); } @@ -1098,7 +1143,11 @@ DisplayServer::WindowMode DisplayServerWindows::window_get_mode(WindowID p_windo const WindowData &wd = windows[p_window]; if (wd.fullscreen) { - return WINDOW_MODE_FULLSCREEN; + if (wd.multiwindow_fs) { + return WINDOW_MODE_FULLSCREEN; + } else { + return WINDOW_MODE_EXCLUSIVE_FULLSCREEN; + } } else if (wd.minimized) { return WINDOW_MODE_MINIMIZED; } else if (wd.maximized) { @@ -2649,98 +2698,72 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } break; - case WM_MOVE: { - if (!IsIconic(windows[window_id].hWnd)) { - int x = int16_t(LOWORD(lParam)); - int y = int16_t(HIWORD(lParam)); - windows[window_id].last_pos = Point2(x, y); - - if (!windows[window_id].rect_changed_callback.is_null()) { - Variant size = Rect2i(windows[window_id].last_pos.x, windows[window_id].last_pos.y, windows[window_id].width, windows[window_id].height); - Variant *sizep = &size; - Variant ret; - Callable::CallError ce; - windows[window_id].rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); + + case WM_WINDOWPOSCHANGED: { + Rect2i window_client_rect; + { + RECT rect; + GetClientRect(hWnd, &rect); + ClientToScreen(hWnd, (POINT *)&rect.left); + ClientToScreen(hWnd, (POINT *)&rect.right); + window_client_rect = Rect2i(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + } + + WINDOWPOS *window_pos_params = (WINDOWPOS *)lParam; + WindowData &window = windows[window_id]; + + bool rect_changed = false; + if (!(window_pos_params->flags & SWP_NOSIZE) || window_pos_params->flags & SWP_FRAMECHANGED) { + int screen_id = window_get_current_screen(window_id); + Size2i screen_size = screen_get_size(screen_id); + Point2i screen_position = screen_get_position(screen_id); + + window.maximized = false; + window.minimized = false; + window.fullscreen = false; + + if (IsIconic(hWnd)) { + window.minimized = true; + } else if (IsZoomed(hWnd)) { + window.maximized = true; + } else if (window_client_rect.position == screen_position && window_client_rect.size == screen_size) { + window.fullscreen = true; } - } - } break; - case WM_SIZE: { - // Ignore window size change when a SIZE_MINIMIZED event is triggered. - if (wParam != SIZE_MINIMIZED) { - // The new width and height of the client area. - int window_w = LOWORD(lParam); - int window_h = HIWORD(lParam); - - // Set new value to the size if it isn't preserved. - if (window_w > 0 && window_h > 0 && !windows[window_id].preserve_window_size) { - windows[window_id].width = window_w; - windows[window_id].height = window_h; + + if (!window.minimized) { + window.width = window_client_rect.size.width; + window.height = window_client_rect.size.height; #if defined(VULKAN_ENABLED) if (context_vulkan && window_created) { - context_vulkan->window_resize(window_id, windows[window_id].width, windows[window_id].height); + context_vulkan->window_resize(window_id, window.width, window.height); } #endif + rect_changed = true; + } + } - } else { // If the size is preserved. - windows[window_id].preserve_window_size = false; + if (!window.minimized && (!(window_pos_params->flags & SWP_NOMOVE) || window_pos_params->flags & SWP_FRAMECHANGED)) { + window.last_pos = window_client_rect.position; + rect_changed = true; + } - // Restore the old size. - window_set_size(Size2(windows[window_id].width, windows[window_id].height), window_id); + if (rect_changed) { + if (!window.rect_changed_callback.is_null()) { + Variant size = Rect2i(window.last_pos.x, window.last_pos.y, window.width, window.height); + const Variant *args[] = { &size }; + Variant ret; + Callable::CallError ce; + window.rect_changed_callback.call(args, 1, ret, ce); } - } else { // When the window has been minimized, preserve its size. - windows[window_id].preserve_window_size = true; } - // Call windows rect change callback. - if (!windows[window_id].rect_changed_callback.is_null()) { - Variant size = Rect2i(windows[window_id].last_pos.x, windows[window_id].last_pos.y, windows[window_id].width, windows[window_id].height); - Variant *size_ptr = &size; - Variant ret; - Callable::CallError ce; - windows[window_id].rect_changed_callback.call((const Variant **)&size_ptr, 1, ret, ce); - } - - // The window has been maximized. - if (wParam == SIZE_MAXIMIZED) { - windows[window_id].maximized = true; - windows[window_id].minimized = false; - } - // The window has been minimized. - else if (wParam == SIZE_MINIMIZED) { - windows[window_id].maximized = false; - windows[window_id].minimized = true; - windows[window_id].preserve_window_size = false; - } - // The window has been resized, but neither the SIZE_MINIMIZED nor SIZE_MAXIMIZED value applies. - else if (wParam == SIZE_RESTORED) { - windows[window_id].maximized = false; - windows[window_id].minimized = false; - } -#if 0 - if (is_layered_allowed() && layered_window) { - DeleteObject(hBitmap); - - RECT r; - GetWindowRect(hWnd, &r); - dib_size = Size2i(r.right - r.left, r.bottom - r.top); - - BITMAPINFO bmi; - ZeroMemory(&bmi, sizeof(BITMAPINFO)); - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = dib_size.x; - bmi.bmiHeader.biHeight = dib_size.y; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - bmi.bmiHeader.biSizeImage = dib_size.x * dib_size.y * 4; - hBitmap = CreateDIBSection(hDC_dib, &bmi, DIB_RGB_COLORS, (void **)&dib_data, nullptr, 0x0); - SelectObject(hDC_dib, hBitmap); - - ZeroMemory(dib_data, dib_size.x * dib_size.y * 4); - } -#endif + // Return here to prevent WM_MOVE and WM_SIZE from being sent + // See: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged#remarks + return 0; + } break; + case WM_ENTERSIZEMOVE: { Input::get_singleton()->release_pressed_events(); windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); @@ -3088,7 +3111,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, DWORD dwExStyle; DWORD dwStyle; - _get_window_style(window_id_counter == MAIN_WINDOW_ID, p_mode == WINDOW_MODE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MAXIMIZED, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT), dwStyle, dwExStyle); + _get_window_style(window_id_counter == MAIN_WINDOW_ID, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MAXIMIZED, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT), dwStyle, dwExStyle); RECT WindowRect; @@ -3097,7 +3120,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, WindowRect.top = p_rect.position.y; WindowRect.bottom = p_rect.position.y + p_rect.size.y; - if (p_mode == WINDOW_MODE_FULLSCREEN) { + if (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { int nearest_area = 0; Rect2i screen_rect; for (int i = 0; i < get_screen_count(); i++) { @@ -3140,7 +3163,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, windows.erase(id); return INVALID_WINDOW_ID; } - if (p_mode != WINDOW_MODE_FULLSCREEN) { + if (p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { wd.pre_fs_valid = true; } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 803c2d4836..d36ca97ebe 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -326,12 +326,12 @@ class DisplayServerWindows : public DisplayServer { Vector<Vector2> mpath; - bool preserve_window_size = false; bool pre_fs_valid = false; RECT pre_fs_rect; bool maximized = false; bool minimized = false; bool fullscreen = false; + bool multiwindow_fs = false; bool borderless = false; bool resizable = true; bool window_focused = false; @@ -401,7 +401,7 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; void _send_window_event(const WindowData &wd, WindowEvent p_event); - void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); + void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); MouseMode mouse_mode; int restore_mouse_trails = 0; @@ -458,6 +458,7 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void screen_set_orientation(ScreenOrientation p_orientation, int p_screen = SCREEN_OF_MAIN_WINDOW) override; diff --git a/scene/SCsub b/scene/SCsub index 92288211bb..a7b23af598 100644 --- a/scene/SCsub +++ b/scene/SCsub @@ -9,6 +9,7 @@ env.add_source_files(env.scene_sources, "*.cpp") # Chain load SCsubs SConscript("main/SCsub") +SConscript("multiplayer/SCsub") SConscript("gui/SCsub") if not env["disable_3d"]: SConscript("3d/SCsub") diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 7e21a43ab6..8cb8a78e8d 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -34,14 +34,6 @@ #include "core/string/string_builder.h" #include "core/string/ustring.h" -static bool _is_whitespace(char32_t c) { - return c == '\t' || c == ' '; -} - -static bool _is_char(char32_t c) { - return !is_symbol(c); -} - void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: @@ -607,9 +599,9 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; - if (has_string_delimiter(chr) && cc > 0 && _is_char(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + if (has_string_delimiter(chr) && cc > 0 && !is_symbol(get_line(cl)[cc - 1]) && post_brace_pair == -1) { insert_text_at_caret(chr); - } else if (cc < get_line(cl).length() && _is_char(get_line(cl)[cc])) { + } else if (cc < get_line(cl).length() && !is_symbol(get_line(cl)[cc])) { insert_text_at_caret(chr); } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); @@ -1001,7 +993,7 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } /* Make sure this is the last char, trailing whitespace or comments are okay. */ - if (should_indent && (!_is_whitespace(c) && is_in_comment(cl, cc) == -1)) { + if (should_indent && (!is_whitespace(c) && is_in_comment(cl, cc) == -1)) { should_indent = false; } } @@ -1817,7 +1809,7 @@ void CodeEdit::request_code_completion(bool p_force) { String line = get_line(get_caret_line()); int ofs = CLAMP(get_caret_column(), 0, line.length()); - if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { emit_signal(SNAME("code_completion_requested")); } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) { emit_signal(SNAME("code_completion_requested")); @@ -1926,7 +1918,7 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (merge_text) { for (; caret_col < line.length(); caret_col++) { - if (!_is_char(line[caret_col])) { + if (is_symbol(line[caret_col])) { break; } } @@ -2562,7 +2554,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c region = E->value(); in_region = true; for (int i = E->key() - 2; i >= 0; i--) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2581,7 +2573,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c } for (int i = end_col; i < line.length(); i++) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2797,11 +2789,11 @@ void CodeEdit::_filter_code_completion_candidates_impl() { while (ofs > 0 && line[ofs] == ' ') { ofs--; } - prev_is_word = _is_char(line[ofs]); + prev_is_word = !is_symbol(line[ofs]); /* Otherwise get current word and set cofs to the start. */ } else { int start_cofs = cofs; - while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || _is_char(line[cofs - 1]))) { + while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || !is_symbol(line[cofs - 1]))) { cofs--; } string_to_complete = line.substr(cofs, start_cofs - cofs); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 0ee4a6af4e..bb259843b8 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -43,18 +43,6 @@ #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { - return !is_symbol(c); -} - -static bool _is_whitespace(char32_t c) { - return c == '\t' || c == ' '; -} - -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - /////////////////////////////////////////////////////////////////////////////// /// TEXT /// /////////////////////////////////////////////////////////////////////////////// @@ -820,8 +808,8 @@ void TextEdit::_notification(int p_what) { int xpos = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * j)) + tabs; bool out_of_bounds = (xpos >= xmargin_end + minimap_width); - bool is_whitespace = _is_whitespace(str[j]); - if (!is_whitespace) { + bool whitespace = is_whitespace(str[j]); + if (!whitespace) { characters++; if (j < str.length() - 1 && color == previous_color && !out_of_bounds) { @@ -843,7 +831,7 @@ void TextEdit::_notification(int p_what) { if (characters > 0) { previous_color.a *= 0.6; // take one for zero indexing, and if we hit whitespace / the end of a word. - int chars = MAX(0, (j - (characters - 1)) - (is_whitespace ? 1 : 0)) + 1; + int chars = MAX(0, (j - (characters - 1)) - (whitespace ? 1 : 0)) + 1; int char_x_ofs = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * chars)) + tabs; if (rtl) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(size.width - char_x_ofs - minimap_char_size.x * characters, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); @@ -1144,7 +1132,7 @@ void TextEdit::_notification(int p_what) { } if (!clipped && lookup_symbol_word.length() != 0) { // Highlight word - if (_is_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '.') { + if (is_ascii_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '_' || lookup_symbol_word[0] == '.') { int highlighted_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); while (highlighted_word_col != -1) { Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_word_col + start, highlighted_word_col + lookup_symbol_word.length() + start); @@ -3037,7 +3025,7 @@ int TextEdit::get_first_non_whitespace_column(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); int col = 0; - while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) { + while (col < text[p_line].length() && is_whitespace(text[p_line][col])) { col++; } return col; @@ -3622,9 +3610,9 @@ Point2i TextEdit::search(const String &p_key, uint32_t p_search_flags, int p_fro if (pos != -1 && (p_search_flags & SEARCH_WHOLE_WORDS)) { // Validate for whole words. - if (pos > 0 && _is_text_char(text_line[pos - 1])) { + if (pos > 0 && !is_symbol(text_line[pos - 1])) { is_match = false; - } else if (pos + p_key.length() < text_line.length() && _is_text_char(text_line[pos + p_key.length()])) { + } else if (pos + p_key.length() < text_line.length() && !is_symbol(text_line[pos + p_key.length()])) { is_match = false; } } @@ -5779,9 +5767,9 @@ int TextEdit::_get_column_pos_of_word(const String &p_key, const String &p_searc if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { p_from_column = col; - if (col > 0 && _is_text_char(p_search[col - 1])) { + if (col > 0 && !is_symbol(p_search[col - 1])) { col = -1; - } else if ((col + p_key.length()) < p_search.length() && _is_text_char(p_search[col + p_key.length()])) { + } else if ((col + p_key.length()) < p_search.length() && !is_symbol(p_search[col + p_key.length()])) { col = -1; } } diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index 89a17ae854..26acfaaa70 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -37,7 +37,7 @@ Size2 TextureButton::get_minimum_size() const { Size2 rscale = Control::get_minimum_size(); - if (!expand) { + if (!ignore_texture_size) { if (normal.is_null()) { if (pressed.is_null()) { if (hover.is_null()) { @@ -182,50 +182,48 @@ void TextureButton::_notification(int p_what) { size = texdraw->get_size(); _texture_region = Rect2(Point2(), texdraw->get_size()); _tile = false; - if (expand) { - switch (stretch_mode) { - case STRETCH_KEEP: - size = texdraw->get_size(); - break; - case STRETCH_SCALE: - size = get_size(); - break; - case STRETCH_TILE: - size = get_size(); - _tile = true; - break; - case STRETCH_KEEP_CENTERED: - ofs = (get_size() - texdraw->get_size()) / 2; - size = texdraw->get_size(); - break; - case STRETCH_KEEP_ASPECT_CENTERED: - case STRETCH_KEEP_ASPECT: { - Size2 _size = get_size(); - float tex_width = texdraw->get_width() * _size.height / texdraw->get_height(); - float tex_height = _size.height; - - if (tex_width > _size.width) { - tex_width = _size.width; - tex_height = texdraw->get_height() * tex_width / texdraw->get_width(); - } + switch (stretch_mode) { + case STRETCH_KEEP: + size = texdraw->get_size(); + break; + case STRETCH_SCALE: + size = get_size(); + break; + case STRETCH_TILE: + size = get_size(); + _tile = true; + break; + case STRETCH_KEEP_CENTERED: + ofs = (get_size() - texdraw->get_size()) / 2; + size = texdraw->get_size(); + break; + case STRETCH_KEEP_ASPECT_CENTERED: + case STRETCH_KEEP_ASPECT: { + Size2 _size = get_size(); + float tex_width = texdraw->get_width() * _size.height / texdraw->get_height(); + float tex_height = _size.height; + + if (tex_width > _size.width) { + tex_width = _size.width; + tex_height = texdraw->get_height() * tex_width / texdraw->get_width(); + } - if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { - ofs.x = (_size.width - tex_width) / 2; - ofs.y = (_size.height - tex_height) / 2; - } - size.width = tex_width; - size.height = tex_height; - } break; - case STRETCH_KEEP_ASPECT_COVERED: { - size = get_size(); - Size2 tex_size = texdraw->get_size(); - Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); - float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; - Size2 scaled_tex_size = tex_size * scale; - Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f; - _texture_region = Rect2(ofs2, size / scale); - } break; - } + if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { + ofs.x = (_size.width - tex_width) / 2; + ofs.y = (_size.height - tex_height) / 2; + } + size.width = tex_width; + size.height = tex_height; + } break; + case STRETCH_KEEP_ASPECT_COVERED: { + size = get_size(); + Size2 tex_size = texdraw->get_size(); + Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); + float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; + Size2 scaled_tex_size = tex_size * scale; + Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f; + _texture_region = Rect2(ofs2, size / scale); + } break; } _position_rect = Rect2(ofs, size); @@ -258,7 +256,7 @@ void TextureButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_disabled_texture", "texture"), &TextureButton::set_disabled_texture); ClassDB::bind_method(D_METHOD("set_focused_texture", "texture"), &TextureButton::set_focused_texture); ClassDB::bind_method(D_METHOD("set_click_mask", "mask"), &TextureButton::set_click_mask); - ClassDB::bind_method(D_METHOD("set_expand", "expand"), &TextureButton::set_expand); + ClassDB::bind_method(D_METHOD("set_ignore_texture_size", "ignore"), &TextureButton::set_ignore_texture_size); ClassDB::bind_method(D_METHOD("set_stretch_mode", "mode"), &TextureButton::set_stretch_mode); ClassDB::bind_method(D_METHOD("set_flip_h", "enable"), &TextureButton::set_flip_h); ClassDB::bind_method(D_METHOD("is_flipped_h"), &TextureButton::is_flipped_h); @@ -271,7 +269,7 @@ void TextureButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_disabled_texture"), &TextureButton::get_disabled_texture); ClassDB::bind_method(D_METHOD("get_focused_texture"), &TextureButton::get_focused_texture); ClassDB::bind_method(D_METHOD("get_click_mask"), &TextureButton::get_click_mask); - ClassDB::bind_method(D_METHOD("get_expand"), &TextureButton::get_expand); + ClassDB::bind_method(D_METHOD("get_ignore_texture_size"), &TextureButton::get_ignore_texture_size); ClassDB::bind_method(D_METHOD("get_stretch_mode"), &TextureButton::get_stretch_mode); ADD_GROUP("Textures", "texture_"); @@ -281,7 +279,7 @@ void TextureButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_disabled", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_disabled_texture", "get_disabled_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_focused", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_focused_texture", "get_focused_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_click_mask", PROPERTY_HINT_RESOURCE_TYPE, "BitMap"), "set_click_mask", "get_click_mask"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_expand", "get_expand"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "ignore_texture_size", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_ignore_texture_size", "get_ignore_texture_size"); ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Scale,Tile,Keep,Keep Centered,Keep Aspect,Keep Aspect Centered,Keep Aspect Covered"), "set_stretch_mode", "get_stretch_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_h", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_h", "is_flipped_h"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_v", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_v", "is_flipped_v"); @@ -352,12 +350,12 @@ void TextureButton::set_focused_texture(const Ref<Texture2D> &p_focused) { focused = p_focused; }; -bool TextureButton::get_expand() const { - return expand; +bool TextureButton::get_ignore_texture_size() const { + return ignore_texture_size; } -void TextureButton::set_expand(bool p_expand) { - expand = p_expand; +void TextureButton::set_ignore_texture_size(bool p_ignore) { + ignore_texture_size = p_ignore; update_minimum_size(); update(); } diff --git a/scene/gui/texture_button.h b/scene/gui/texture_button.h index 1428a79a1d..5762949acd 100644 --- a/scene/gui/texture_button.h +++ b/scene/gui/texture_button.h @@ -54,8 +54,8 @@ private: Ref<Texture2D> disabled; Ref<Texture2D> focused; Ref<BitMap> click_mask; - bool expand = false; - StretchMode stretch_mode = STRETCH_SCALE; + bool ignore_texture_size = false; + StretchMode stretch_mode = STRETCH_KEEP; Rect2 _texture_region; Rect2 _position_rect; @@ -85,8 +85,8 @@ public: Ref<Texture2D> get_focused_texture() const; Ref<BitMap> get_click_mask() const; - bool get_expand() const; - void set_expand(bool p_expand); + bool get_ignore_texture_size() const; + void set_ignore_texture_size(bool p_ignore); void set_stretch_mode(StretchMode p_stretch_mode); StretchMode get_stretch_mode() const; diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 2665a5695b..6b9d8ab211 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -32,6 +32,7 @@ #include "core/core_string_names.h" #include "core/io/resource_loader.h" +#include "core/multiplayer/multiplayer_api.h" #include "core/object/message_queue.h" #include "core/string/print_string.h" #include "instance_placeholder.h" @@ -110,9 +111,6 @@ void Node::_notification(int p_notification) { memdelete(data.path_cache); data.path_cache = nullptr; } - if (data.scene_file_path.length()) { - get_multiplayer()->scene_enter_exit_notify(data.scene_file_path, this, false); - } } break; case NOTIFICATION_PATH_RENAMED: { if (data.path_cache) { @@ -141,12 +139,6 @@ void Node::_notification(int p_notification) { } GDVIRTUAL_CALL(_ready); - - if (data.scene_file_path.length()) { - ERR_FAIL_COND(!is_inside_tree()); - get_multiplayer()->scene_enter_exit_notify(data.scene_file_path, this, true); - } - } break; case NOTIFICATION_POSTINITIALIZE: { data.in_constructor = false; @@ -1061,7 +1053,7 @@ void Node::_generate_serial_child_name(const Node *p_child, StringName &name) co String nums; for (int i = name_string.length() - 1; i >= 0; i--) { char32_t n = name_string[i]; - if (n >= '0' && n <= '9') { + if (is_digit(n)) { nums = String::chr(name_string[i]) + nums; } else { break; diff --git a/scene/main/node.h b/scene/main/node.h index a1fc672a15..0ac10f4381 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -212,7 +212,6 @@ protected: static String _get_name_num_separator(); friend class SceneState; - friend class MultiplayerReplicator; void _add_child_nocheck(Node *p_child, const StringName &p_name); void _set_owner_nocheck(Node *p_owner); @@ -467,7 +466,7 @@ public: bool is_displayed_folded() const; /* NETWORK */ - void set_multiplayer_authority(int p_peer_id, bool p_recursive = true); + virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true); int get_multiplayer_authority() const; bool is_multiplayer_authority() const; diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 45f04b28b9..0e4a6a4b5c 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -36,6 +36,7 @@ #include "core/io/dir_access.h" #include "core/io/marshalls.h" #include "core/io/resource_loader.h" +#include "core/multiplayer/multiplayer_api.h" #include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index 1dff1dab4f..a5cd52b4ca 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -31,7 +31,6 @@ #ifndef SCENE_TREE_H #define SCENE_TREE_H -#include "core/multiplayer/multiplayer_api.h" #include "core/os/main_loop.h" #include "core/os/thread_safe.h" #include "core/templates/self_list.h" @@ -46,6 +45,7 @@ class Node; class Window; class Material; class Mesh; +class MultiplayerAPI; class SceneDebugger; class Tween; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 09880ad6cf..522997cdf5 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -1231,7 +1231,7 @@ void Viewport::_gui_show_tooltip() { base_tooltip->set_anchors_and_offsets_preset(Control::PRESET_WIDE); - panel->set_transient(false); + panel->set_transient(true); panel->set_flag(Window::FLAG_NO_FOCUS, true); panel->set_wrap_controls(true); panel->add_child(base_tooltip); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index fbc0bc5301..f2ebe50fa3 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -1614,6 +1614,7 @@ void Window::_bind_methods() { BIND_ENUM_CONSTANT(MODE_MINIMIZED); BIND_ENUM_CONSTANT(MODE_MAXIMIZED); BIND_ENUM_CONSTANT(MODE_FULLSCREEN); + BIND_ENUM_CONSTANT(MODE_EXCLUSIVE_FULLSCREEN); BIND_ENUM_CONSTANT(FLAG_RESIZE_DISABLED); BIND_ENUM_CONSTANT(FLAG_BORDERLESS); diff --git a/scene/main/window.h b/scene/main/window.h index 2dd1dd6601..f37689f905 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -46,6 +46,7 @@ public: MODE_MINIMIZED = DisplayServer::WINDOW_MODE_MINIMIZED, MODE_MAXIMIZED = DisplayServer::WINDOW_MODE_MAXIMIZED, MODE_FULLSCREEN = DisplayServer::WINDOW_MODE_FULLSCREEN, + MODE_EXCLUSIVE_FULLSCREEN = DisplayServer::WINDOW_MODE_EXCLUSIVE_FULLSCREEN, }; enum Flags { diff --git a/scene/multiplayer/SCsub b/scene/multiplayer/SCsub new file mode 100644 index 0000000000..fc61250247 --- /dev/null +++ b/scene/multiplayer/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.scene_sources, "*.cpp") diff --git a/scene/multiplayer/multiplayer_spawner.cpp b/scene/multiplayer/multiplayer_spawner.cpp new file mode 100644 index 0000000000..4f2a9d9e83 --- /dev/null +++ b/scene/multiplayer/multiplayer_spawner.cpp @@ -0,0 +1,227 @@ +/*************************************************************************/ +/* multiplayer_spawner.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 "multiplayer_spawner.h" + +#include "core/io/marshalls.h" +#include "core/multiplayer/multiplayer_api.h" +#include "scene/main/window.h" +#include "scene/scene_string_names.h" + +void MultiplayerSpawner::_bind_methods() { + ClassDB::bind_method(D_METHOD("spawn", "data"), &MultiplayerSpawner::spawn, DEFVAL(Variant())); + + ClassDB::bind_method(D_METHOD("get_spawnable_scenes"), &MultiplayerSpawner::get_spawnable_scenes); + ClassDB::bind_method(D_METHOD("set_spawnable_scenes", "scenes"), &MultiplayerSpawner::set_spawnable_scenes); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "replication", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_spawnable_scenes", "get_spawnable_scenes"); + + ClassDB::bind_method(D_METHOD("get_spawn_path"), &MultiplayerSpawner::get_spawn_path); + ClassDB::bind_method(D_METHOD("set_spawn_path", "path"), &MultiplayerSpawner::set_spawn_path); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "spawn_path", PROPERTY_HINT_NONE, ""), "set_spawn_path", "get_spawn_path"); + + ClassDB::bind_method(D_METHOD("get_spawn_limit"), &MultiplayerSpawner::get_spawn_limit); + ClassDB::bind_method(D_METHOD("set_spawn_limit", "limit"), &MultiplayerSpawner::set_spawn_limit); + ADD_PROPERTY(PropertyInfo(Variant::INT, "spawn_limit", PROPERTY_HINT_RANGE, "0,1024,1,or_greater"), "set_spawn_limit", "get_spawn_limit"); + + ClassDB::bind_method(D_METHOD("set_auto_spawning", "enabled"), &MultiplayerSpawner::set_auto_spawning); + ClassDB::bind_method(D_METHOD("is_auto_spawning"), &MultiplayerSpawner::is_auto_spawning); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_spawn"), "set_auto_spawning", "is_auto_spawning"); + + GDVIRTUAL_BIND(_spawn_custom, "data"); + + ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); + ADD_SIGNAL(MethodInfo("spawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); +} + +void MultiplayerSpawner::_update_spawn_node() { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return; + } +#endif + if (spawn_node.is_valid()) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)); + if (node && node->is_connected("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added))) { + node->disconnect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added)); + } + } + Node *node = spawn_path.is_empty() && is_inside_tree() ? nullptr : get_node_or_null(spawn_path); + if (node) { + spawn_node = node->get_instance_id(); + if (auto_spawn) { + node->connect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added)); + } + } else { + spawn_node = ObjectID(); + } +} + +void MultiplayerSpawner::_notification(int p_what) { + if (p_what == NOTIFICATION_POST_ENTER_TREE) { + _update_spawn_node(); + } else if (p_what == NOTIFICATION_EXIT_TREE) { + _update_spawn_node(); + const ObjectID *oid = nullptr; + while ((oid = tracked_nodes.next(oid))) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(*oid)); + ERR_CONTINUE(!node); + node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit)); + // This is unlikely, but might still crash the engine. + if (node->is_connected(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready))) { + node->disconnect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready)); + } + get_multiplayer()->despawn(node, this); + } + tracked_nodes.clear(); + } +} + +void MultiplayerSpawner::_node_added(Node *p_node) { + if (!get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority()) { + return; + } + if (tracked_nodes.has(p_node->get_instance_id())) { + return; + } + const Node *parent = get_spawn_node(); + if (!parent || p_node->get_parent() != parent) { + return; + } + int id = get_scene_id(p_node->get_scene_file_path()); + if (id == INVALID_ID) { + return; + } + const String name = p_node->get_name(); + ERR_FAIL_COND_MSG(name.validate_node_name() != name, vformat("Unable to auto-spawn node with reserved name: %s. Make sure to add your replicated scenes via 'add_child(node, true)' to produce valid names.", name)); + _track(p_node, Variant(), id); +} + +void MultiplayerSpawner::set_auto_spawning(bool p_enabled) { + auto_spawn = p_enabled; + _update_spawn_node(); +} + +bool MultiplayerSpawner::is_auto_spawning() const { + return auto_spawn; +} + +TypedArray<PackedScene> MultiplayerSpawner::get_spawnable_scenes() { + return spawnable_scenes; +} + +void MultiplayerSpawner::set_spawnable_scenes(TypedArray<PackedScene> p_scenes) { + spawnable_scenes = p_scenes; +} + +NodePath MultiplayerSpawner::get_spawn_path() const { + return spawn_path; +} + +void MultiplayerSpawner::set_spawn_path(const NodePath &p_path) { + spawn_path = p_path; + _update_spawn_node(); +} + +void MultiplayerSpawner::_track(Node *p_node, const Variant &p_argument, int p_scene_id) { + ObjectID oid = p_node->get_instance_id(); + if (!tracked_nodes.has(oid)) { + tracked_nodes[oid] = SpawnInfo(p_argument.duplicate(true), p_scene_id); + p_node->connect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit), varray(p_node->get_instance_id()), CONNECT_ONESHOT); + p_node->connect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready), varray(p_node->get_instance_id()), CONNECT_ONESHOT); + } +} + +void MultiplayerSpawner::_node_ready(ObjectID p_id) { + get_multiplayer()->spawn(ObjectDB::get_instance(p_id), this); +} + +void MultiplayerSpawner::_node_exit(ObjectID p_id) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id)); + ERR_FAIL_COND(!node); + if (tracked_nodes.has(p_id)) { + tracked_nodes.erase(p_id); + get_multiplayer()->despawn(node, this); + } +} + +int MultiplayerSpawner::get_scene_id(const String &p_scene) const { + for (int i = 0; i < spawnable_scenes.size(); i++) { + Ref<PackedScene> ps = spawnable_scenes[i]; + ERR_CONTINUE(ps.is_null()); + if (ps->get_path() == p_scene) { + return i; + } + } + return INVALID_ID; +} + +int MultiplayerSpawner::get_spawn_id(const ObjectID &p_id) const { + const SpawnInfo *info = tracked_nodes.getptr(p_id); + return info ? info->id : INVALID_ID; +} + +const Variant MultiplayerSpawner::get_spawn_argument(const ObjectID &p_id) const { + const SpawnInfo *info = tracked_nodes.getptr(p_id); + return info ? info->args : Variant(); +} + +Node *MultiplayerSpawner::instantiate_scene(int p_id) { + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + ERR_FAIL_INDEX_V(p_id, spawnable_scenes.size(), nullptr); + Ref<PackedScene> scene = spawnable_scenes[p_id]; + ERR_FAIL_COND_V(scene.is_null(), nullptr); + return scene->instantiate(); +} + +Node *MultiplayerSpawner::instantiate_custom(const Variant &p_data) { + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + Object *obj = nullptr; + Node *node = nullptr; + if (GDVIRTUAL_CALL(_spawn_custom, p_data, obj)) { + node = Object::cast_to<Node>(obj); + } + return node; +} + +Node *MultiplayerSpawner::spawn(const Variant &p_data) { + ERR_FAIL_COND_V(!is_inside_tree() || !get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority(), nullptr); + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + ERR_FAIL_COND_V_MSG(!GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom), nullptr, "Custom spawn requires the '_spawn_custom' virtual method to be implemented via script."); + + Node *parent = get_spawn_node(); + ERR_FAIL_COND_V_MSG(!parent, nullptr, "Cannot find spawn node."); + + Node *node = instantiate_custom(p_data); + ERR_FAIL_COND_V_MSG(!node, nullptr, "The '_spawn_custom' implementation must return a valid Node."); + + _track(node, p_data); + parent->add_child(node, true); + return node; +} diff --git a/scene/multiplayer/multiplayer_spawner.h b/scene/multiplayer/multiplayer_spawner.h new file mode 100644 index 0000000000..63948e39a5 --- /dev/null +++ b/scene/multiplayer/multiplayer_spawner.h @@ -0,0 +1,101 @@ +/*************************************************************************/ +/* multiplayer_spawner.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 MULTIPLAYER_SPAWNER_H +#define MULTIPLAYER_SPAWNER_H + +#include "scene/main/node.h" + +#include "core/variant/typed_array.h" +#include "scene/resources/packed_scene.h" +#include "scene/resources/scene_replication_config.h" + +class MultiplayerSpawner : public Node { + GDCLASS(MultiplayerSpawner, Node); + +public: + enum { + INVALID_ID = 0xFF, + }; + +private: + TypedArray<PackedScene> spawnable_scenes; + Set<ResourceUID::ID> spawnable_ids; + NodePath spawn_path; + + struct SpawnInfo { + Variant args; + int id = INVALID_ID; + SpawnInfo(Variant p_args, int p_id) { + id = p_id; + args = p_args; + } + SpawnInfo() {} + }; + + ObjectID spawn_node; + HashMap<ObjectID, SpawnInfo> tracked_nodes; + bool auto_spawn = false; + uint32_t spawn_limit = 0; + + void _update_spawn_node(); + void _track(Node *p_node, const Variant &p_argument, int p_scene_id = INVALID_ID); + void _node_added(Node *p_node); + void _node_exit(ObjectID p_id); + void _node_ready(ObjectID p_id); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + Node *get_spawn_node() const { return spawn_node.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)) : nullptr; } + TypedArray<PackedScene> get_spawnable_scenes(); + void set_spawnable_scenes(TypedArray<PackedScene> p_scenes); + NodePath get_spawn_path() const; + void set_spawn_path(const NodePath &p_path); + uint32_t get_spawn_limit() const { return spawn_limit; } + void set_spawn_limit(uint32_t p_limit) { spawn_limit = p_limit; } + bool is_auto_spawning() const; + void set_auto_spawning(bool p_enabled); + + const Variant get_spawn_argument(const ObjectID &p_id) const; + int get_spawn_id(const ObjectID &p_id) const; + int get_scene_id(const String &p_path) const; + Node *spawn(const Variant &p_data = Variant()); + Node *instantiate_custom(const Variant &p_data); + Node *instantiate_scene(int p_idx); + + GDVIRTUAL1R(Object *, _spawn_custom, const Variant &); + + MultiplayerSpawner() {} +}; + +#endif // MULTIPLAYER_SPAWNER_H diff --git a/scene/multiplayer/multiplayer_synchronizer.cpp b/scene/multiplayer/multiplayer_synchronizer.cpp new file mode 100644 index 0000000000..fbe1b99cc9 --- /dev/null +++ b/scene/multiplayer/multiplayer_synchronizer.cpp @@ -0,0 +1,158 @@ +/*************************************************************************/ +/* multiplayer_synchronizer.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 "multiplayer_synchronizer.h" + +#include "core/config/engine.h" +#include "core/multiplayer/multiplayer_api.h" + +Object *MultiplayerSynchronizer::_get_prop_target(Object *p_obj, const NodePath &p_path) { + if (p_path.get_name_count() == 0) { + return p_obj; + } + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V_MSG(!node || !node->has_node(p_path), nullptr, vformat("Node '%s' not found.", p_path)); + return node->get_node(p_path); +} + +void MultiplayerSynchronizer::_stop() { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (node) { + get_multiplayer()->replication_stop(node, this); + } +} + +void MultiplayerSynchronizer::_start() { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (node) { + get_multiplayer()->replication_start(node, this); + } +} + +Error MultiplayerSynchronizer::get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs) { + ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER); + r_variant.resize(p_properties.size()); + r_variant_ptrs.resize(r_variant.size()); + int i = 0; + for (const NodePath &prop : p_properties) { + bool valid = false; + const Object *obj = _get_prop_target(p_obj, prop); + ERR_FAIL_COND_V(!obj, FAILED); + r_variant.write[i] = obj->get(prop.get_concatenated_subnames(), &valid); + r_variant_ptrs.write[i] = &r_variant[i]; + ERR_FAIL_COND_V_MSG(!valid, ERR_INVALID_DATA, vformat("Property '%s' not found.", prop)); + i++; + } + return OK; +} + +Error MultiplayerSynchronizer::set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state) { + ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER); + int i = 0; + for (const NodePath &prop : p_properties) { + Object *obj = _get_prop_target(p_obj, prop); + ERR_FAIL_COND_V(!obj, FAILED); + obj->set(prop.get_concatenated_subnames(), p_state[i]); + i += 1; + } + return OK; +} + +void MultiplayerSynchronizer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_root_path", "path"), &MultiplayerSynchronizer::set_root_path); + ClassDB::bind_method(D_METHOD("get_root_path"), &MultiplayerSynchronizer::get_root_path); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path"); + + ClassDB::bind_method(D_METHOD("set_replication_interval", "milliseconds"), &MultiplayerSynchronizer::set_replication_interval); + ClassDB::bind_method(D_METHOD("get_replication_interval"), &MultiplayerSynchronizer::get_replication_interval); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001"), "set_replication_interval", "get_replication_interval"); + + ClassDB::bind_method(D_METHOD("set_replication_config", "config"), &MultiplayerSynchronizer::set_replication_config); + ClassDB::bind_method(D_METHOD("get_replication_config"), &MultiplayerSynchronizer::get_replication_config); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig"), "set_replication_config", "get_replication_config"); +} + +void MultiplayerSynchronizer::_notification(int p_what) { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return; + } +#endif + if (root_path.is_empty()) { + return; + } + if (p_what == NOTIFICATION_ENTER_TREE) { + _start(); + } else if (p_what == NOTIFICATION_EXIT_TREE) { + _stop(); + } +} + +void MultiplayerSynchronizer::set_replication_interval(double p_interval) { + ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)"); + interval_msec = uint64_t(p_interval * 1000); +} + +double MultiplayerSynchronizer::get_replication_interval() const { + return double(interval_msec) / 1000.0; +} + +uint64_t MultiplayerSynchronizer::get_replication_interval_msec() const { + return interval_msec; +} + +void MultiplayerSynchronizer::set_replication_config(Ref<SceneReplicationConfig> p_config) { + replication_config = p_config; +} + +Ref<SceneReplicationConfig> MultiplayerSynchronizer::get_replication_config() { + return replication_config; +} + +void MultiplayerSynchronizer::set_root_path(const NodePath &p_path) { + _stop(); + root_path = p_path; + _start(); +} + +NodePath MultiplayerSynchronizer::get_root_path() const { + return root_path; +} + +void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_recursive) { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (!node) { + Node::set_multiplayer_authority(p_peer_id, p_recursive); + return; + } + get_multiplayer()->replication_stop(node, this); + Node::set_multiplayer_authority(p_peer_id, p_recursive); + get_multiplayer()->replication_start(node, this); +} diff --git a/scene/multiplayer/multiplayer_synchronizer.h b/scene/multiplayer/multiplayer_synchronizer.h new file mode 100644 index 0000000000..e856745379 --- /dev/null +++ b/scene/multiplayer/multiplayer_synchronizer.h @@ -0,0 +1,72 @@ +/*************************************************************************/ +/* multiplayer_synchronizer.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 MULTIPLAYER_SYNCHRONIZER_H +#define MULTIPLAYER_SYNCHRONIZER_H + +#include "scene/main/node.h" + +#include "scene/resources/scene_replication_config.h" + +class MultiplayerSynchronizer : public Node { + GDCLASS(MultiplayerSynchronizer, Node); + +private: + Ref<SceneReplicationConfig> replication_config; + NodePath root_path; + uint64_t interval_msec = 0; + + static Object *_get_prop_target(Object *p_obj, const NodePath &p_prop); + void _start(); + void _stop(); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + static Error get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs); + static Error set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state); + + void set_replication_interval(double p_interval); + double get_replication_interval() const; + uint64_t get_replication_interval_msec() const; + + void set_replication_config(Ref<SceneReplicationConfig> p_config); + Ref<SceneReplicationConfig> get_replication_config(); + + void set_root_path(const NodePath &p_path); + NodePath get_root_path() const; + virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true) override; + + MultiplayerSynchronizer() {} +}; + +#endif // MULTIPLAYER_SYNCHRONIZER_H diff --git a/scene/multiplayer/scene_replication_interface.cpp b/scene/multiplayer/scene_replication_interface.cpp new file mode 100644 index 0000000000..7155935084 --- /dev/null +++ b/scene/multiplayer/scene_replication_interface.cpp @@ -0,0 +1,415 @@ +/*************************************************************************/ +/* scene_replication_interface.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 "scene_replication_interface.h" + +#include "core/io/marshalls.h" +#include "scene/main/node.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" + +#define MAKE_ROOM(m_amount) \ + if (packet_cache.size() < m_amount) \ + packet_cache.resize(m_amount); + +MultiplayerReplicationInterface *SceneReplicationInterface::_create(MultiplayerAPI *p_multiplayer) { + return memnew(SceneReplicationInterface(p_multiplayer)); +} + +void SceneReplicationInterface::make_default() { + MultiplayerAPI::create_default_replication_interface = _create; +} + +void SceneReplicationInterface::_free_remotes(int p_id) { + const HashMap<uint32_t, ObjectID> remotes = rep_state->peer_get_remotes(p_id); + const uint32_t *k = nullptr; + while ((k = remotes.next(k))) { + Node *node = rep_state->get_node(remotes.get(*k)); + ERR_CONTINUE(!node); + node->queue_delete(); + } +} + +void SceneReplicationInterface::on_peer_change(int p_id, bool p_connected) { + if (p_connected) { + rep_state->on_peer_change(p_id, p_connected); + for (const ObjectID &oid : rep_state->get_spawned_nodes()) { + _send_spawn(rep_state->get_node(oid), rep_state->get_spawner(oid), p_id); + } + for (const ObjectID &oid : rep_state->get_path_only_nodes()) { + Node *node = rep_state->get_node(oid); + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_CONTINUE(!node || !sync); + if (sync->is_multiplayer_authority()) { + rep_state->peer_add_node(p_id, oid); + } + } + } else { + _free_remotes(p_id); + rep_state->on_peer_change(p_id, p_connected); + } +} + +void SceneReplicationInterface::on_reset() { + for (int pid : rep_state->get_peers()) { + _free_remotes(pid); + } + rep_state->reset(); +} + +void SceneReplicationInterface::on_network_process() { + uint64_t msec = OS::get_singleton()->get_ticks_msec(); + for (int peer : rep_state->get_peers()) { + _send_sync(peer, msec); + } +} + +Error SceneReplicationInterface::on_spawn(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!spawner, ERR_INVALID_PARAMETER); + Error err = rep_state->config_add_spawn(node, spawner); + ERR_FAIL_COND_V(err != OK, err); + return _send_spawn(node, spawner, 0); +} + +Error SceneReplicationInterface::on_despawn(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!p_obj || !spawner, ERR_INVALID_PARAMETER); + Error err = rep_state->config_del_spawn(node, spawner); + ERR_FAIL_COND_V(err != OK, err); + return _send_despawn(node, 0); +} + +Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!sync, ERR_INVALID_PARAMETER); + rep_state->config_add_sync(node, sync); + // Try to apply initial state if spawning (hack to apply if before ready). + if (pending_spawn == p_obj->get_instance_id()) { + pending_spawn = ObjectID(); // Make sure this only happens once. + const List<NodePath> props = sync->get_replication_config()->get_spawn_properties(); + Vector<Variant> vars; + vars.resize(props.size()); + int consumed; + Error err = MultiplayerAPI::decode_and_decompress_variants(vars, pending_buffer, pending_buffer_size, consumed); + ERR_FAIL_COND_V(err, err); + err = MultiplayerSynchronizer::set_state(props, node, vars); + ERR_FAIL_COND_V(err, err); + } else if (multiplayer->has_multiplayer_peer() && sync->is_multiplayer_authority()) { + // Either it's a spawn or a static sync, in any case add it to the list of known nodes. + rep_state->peer_add_node(0, p_obj->get_instance_id()); + } + return OK; +} + +Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!p_obj || !sync, ERR_INVALID_PARAMETER); + return rep_state->config_del_sync(node, sync); +} + +Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable) { + ERR_FAIL_COND_V(!p_buffer || p_size < 1, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(!multiplayer, ERR_UNCONFIGURED); + ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); + Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); + peer->set_target_peer(p_peer); + peer->set_transfer_channel(0); + peer->set_transfer_mode(p_reliable ? Multiplayer::TRANSFER_MODE_RELIABLE : Multiplayer::TRANSFER_MODE_UNRELIABLE); + return peer->put_packet(p_buffer, p_size); +} + +Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer) { + ERR_FAIL_COND_V(p_peer < 0, ERR_BUG); + ERR_FAIL_COND_V(!multiplayer, ERR_BUG); + ERR_FAIL_COND_V(!p_spawner || !p_node, ERR_BUG); + + const ObjectID oid = p_node->get_instance_id(); + uint32_t nid = rep_state->ensure_net_id(oid); + + // Prepare custom arg and scene_id + uint8_t scene_id = p_spawner->get_spawn_id(oid); + bool is_custom = scene_id == MultiplayerSpawner::INVALID_ID; + Variant spawn_arg = p_spawner->get_spawn_argument(oid); + int spawn_arg_size = 0; + if (is_custom) { + Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, nullptr, spawn_arg_size, false); + ERR_FAIL_COND_V(err, err); + } + + // Prepare spawn state. + int state_size = 0; + Vector<Variant> state_vars; + Vector<const Variant *> state_varp; + MultiplayerSynchronizer *synchronizer = rep_state->get_synchronizer(oid); + if (synchronizer && synchronizer->get_replication_config().is_valid()) { + const List<NodePath> props = synchronizer->get_replication_config()->get_spawn_properties(); + Error err = MultiplayerSynchronizer::get_state(props, p_node, state_vars, state_varp); + ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to retrieve spawn state."); + err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), nullptr, state_size); + ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to encode spawn state."); + } + + // Prepare simplified path. + const Node *root_node = multiplayer->get_root_node(); + ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED); + NodePath rel_path = (root_node->get_path()).rel_path_to(p_spawner->get_path()); + + int path_id = 0; + multiplayer->send_confirm_path(p_spawner, rel_path, p_peer, path_id); + + // Encode name and parent ID. + CharString cname = p_node->get_name().operator String().utf8(); + int nlen = encode_cstring(cname.get_data(), nullptr); + MAKE_ROOM(1 + 1 + 4 + 4 + 4 + nlen + (is_custom ? 4 + spawn_arg_size : 0) + state_size); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = (uint8_t)MultiplayerAPI::NETWORK_COMMAND_SPAWN; + ptr[1] = scene_id; + int ofs = 2; + ofs += encode_uint32(path_id, &ptr[ofs]); + ofs += encode_uint32(nid, &ptr[ofs]); + ofs += encode_uint32(nlen, &ptr[ofs]); + ofs += encode_cstring(cname.get_data(), &ptr[ofs]); + // Write args + if (is_custom) { + ofs += encode_uint32(spawn_arg_size, &ptr[ofs]); + Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, &ptr[ofs], spawn_arg_size, false); + ERR_FAIL_COND_V(err, err); + ofs += spawn_arg_size; + } + // Write state. + if (state_size) { + Error err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), &ptr[ofs], state_size); + ERR_FAIL_COND_V(err, err); + ofs += state_size; + } + Error err = _send_raw(ptr, ofs, p_peer, true); + ERR_FAIL_COND_V(err, err); + return rep_state->peer_add_node(p_peer, oid); +} + +Error SceneReplicationInterface::_send_despawn(Node *p_node, int p_peer) { + const ObjectID oid = p_node->get_instance_id(); + MAKE_ROOM(5); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = (uint8_t)MultiplayerAPI::NETWORK_COMMAND_DESPAWN; + int ofs = 1; + uint32_t nid = rep_state->get_net_id(oid); + ofs += encode_uint32(nid, &ptr[ofs]); + Error err = _send_raw(ptr, ofs, p_peer, true); + ERR_FAIL_COND_V(err, err); + return rep_state->peer_del_node(p_peer, oid); +} + +Error SceneReplicationInterface::on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 14, ERR_INVALID_DATA, "Invalid spawn packet received"); + int ofs = 1; // The spawn/despawn command. + uint8_t scene_id = p_buffer[ofs]; + ofs += 1; + uint32_t node_target = decode_uint32(&p_buffer[ofs]); + ofs += 4; + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(multiplayer->get_cached_node(p_from, node_target)); + ERR_FAIL_COND_V(!spawner, ERR_DOES_NOT_EXIST); + ERR_FAIL_COND_V(p_from != spawner->get_multiplayer_authority(), ERR_UNAUTHORIZED); + + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + uint32_t name_len = decode_uint32(&p_buffer[ofs]); + ofs += 4; + ERR_FAIL_COND_V_MSG(name_len > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA, vformat("Invalid spawn packet size: %d, wants: %d", p_buffer_len, ofs + name_len)); + ERR_FAIL_COND_V_MSG(name_len < 1, ERR_INVALID_DATA, "Zero spawn name size."); + + // We need to make sure no trickery happens here, but we want to allow autogenerated ("@") node names. + const String name = String::utf8((const char *)&p_buffer[ofs], name_len); + ERR_FAIL_COND_V_MSG(name.validate_node_name() != name, ERR_INVALID_DATA, vformat("Invalid node name received: '%s'. Make sure to add nodes via 'add_child(node, true)' remotely.", name)); + ofs += name_len; + + // Check that we can spawn. + Node *parent = spawner->get_node_or_null(spawner->get_spawn_path()); + ERR_FAIL_COND_V(!parent, ERR_UNCONFIGURED); + ERR_FAIL_COND_V(parent->has_node(name), ERR_INVALID_DATA); + + Node *node = nullptr; + if (scene_id == MultiplayerSpawner::INVALID_ID) { + // Custom spawn. + ERR_FAIL_COND_V(p_buffer_len - ofs < 4, ERR_INVALID_DATA); + uint32_t arg_size = decode_uint32(&p_buffer[ofs]); + ofs += 4; + ERR_FAIL_COND_V(arg_size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA); + Variant v; + Error err = MultiplayerAPI::decode_and_decompress_variant(v, &p_buffer[ofs], arg_size, nullptr, false); + ERR_FAIL_COND_V(err != OK, err); + ofs += arg_size; + node = spawner->instantiate_custom(v); + } else { + // Scene based spawn. + node = spawner->instantiate_scene(scene_id); + } + ERR_FAIL_COND_V(!node, ERR_UNAUTHORIZED); + node->set_name(name); + rep_state->peer_add_remote(p_from, net_id, node, spawner); + // The initial state will be applied during the sync config (i.e. before _ready). + int state_len = p_buffer_len - ofs; + if (state_len) { + pending_spawn = node->get_instance_id(); + pending_buffer = &p_buffer[ofs]; + pending_buffer_size = state_len; + } + parent->add_child(node); + pending_spawn = ObjectID(); + pending_buffer = nullptr; + pending_buffer_size = 0; + return OK; +} + +Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 5, ERR_INVALID_DATA, "Invalid spawn packet received"); + int ofs = 1; // The spawn/despawn command. + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + Node *node = nullptr; + Error err = rep_state->peer_del_remote(p_from, net_id, &node); + ERR_FAIL_COND_V(err != OK, err); + ERR_FAIL_COND_V(!node, ERR_BUG); + node->queue_delete(); + return OK; +} + +void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) { + const Set<ObjectID> &known = rep_state->get_known_nodes(p_peer); + if (known.is_empty()) { + return; + } + MAKE_ROOM(sync_mtu); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC; + int ofs = 1; + ofs += encode_uint16(rep_state->peer_sync_next(p_peer), &ptr[1]); + // Can only send updates for already notified nodes. + // This is a lazy implementation, we could optimize much more here with by grouping by replication config. + for (const ObjectID &oid : known) { + if (!rep_state->update_sync_time(oid, p_msec)) { + continue; // nothing to sync. + } + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_CONTINUE(!sync); + Node *node = rep_state->get_node(oid); + ERR_CONTINUE(!node); + int size; + Vector<Variant> vars; + Vector<const Variant *> varp; + const List<NodePath> props = sync->get_replication_config()->get_sync_properties(); + Error err = MultiplayerSynchronizer::get_state(props, node, vars, varp); + ERR_CONTINUE_MSG(err != OK, "Unable to retrieve sync state."); + err = MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), nullptr, size); + ERR_CONTINUE_MSG(err != OK, "Unable to encode sync state."); + // TODO Handle single state above MTU. + ERR_CONTINUE_MSG(size > 3 + 4 + 4 + sync_mtu, vformat("Node states bigger then MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path())); + if (ofs + 4 + 4 + size > sync_mtu) { + // Send what we got, and reset write. + _send_raw(packet_cache.ptr(), ofs, p_peer, false); + ofs = 3; + } + if (size) { + uint32_t net_id = rep_state->get_net_id(oid); + if (net_id == 0) { + // First time path based ID. + const Node *root_node = multiplayer->get_root_node(); + ERR_FAIL_COND(!root_node); + NodePath rel_path = (root_node->get_path()).rel_path_to(sync->get_path()); + int path_id = 0; + multiplayer->send_confirm_path(sync, rel_path, p_peer, path_id); + net_id = path_id; + rep_state->set_net_id(oid, net_id | 0x80000000); + } + ofs += encode_uint32(rep_state->get_net_id(oid), &ptr[ofs]); + ofs += encode_uint32(size, &ptr[ofs]); + MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), &ptr[ofs], size); + ofs += size; + } + } + if (ofs > 3) { + // Got some left over to send. + _send_raw(packet_cache.ptr(), ofs, p_peer, false); + } +} + +Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 11, ERR_INVALID_DATA, "Invalid sync packet received"); + uint16_t time = decode_uint16(&p_buffer[1]); + int ofs = 3; + rep_state->peer_sync_recv(p_from, time); + while (ofs + 8 < p_buffer_len) { + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + uint32_t size = decode_uint32(&p_buffer[ofs]); + ofs += 4; + Node *node = nullptr; + if (net_id & 0x80000000) { + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_cached_node(p_from, net_id & 0x7FFFFFFF)); + ERR_FAIL_COND_V(!sync || sync->get_multiplayer_authority() != p_from, ERR_UNAUTHORIZED); + node = sync->get_node(sync->get_root_path()); + } else { + node = rep_state->peer_get_remote(p_from, net_id); + } + if (!node) { + // Not received yet. + ofs += size; + continue; + } + const ObjectID oid = node->get_instance_id(); + if (!rep_state->update_last_node_sync(oid, time)) { + // State is too old. + ofs += size; + continue; + } + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_FAIL_COND_V(!sync, ERR_BUG); + ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_BUG); + const List<NodePath> props = sync->get_replication_config()->get_sync_properties(); + Vector<Variant> vars; + vars.resize(props.size()); + int consumed; + Error err = MultiplayerAPI::decode_and_decompress_variants(vars, &p_buffer[ofs], size, consumed); + ERR_FAIL_COND_V(err, err); + err = MultiplayerSynchronizer::set_state(props, node, vars); + ERR_FAIL_COND_V(err, err); + ofs += size; + } + return OK; +} diff --git a/scene/multiplayer/scene_replication_interface.h b/scene/multiplayer/scene_replication_interface.h new file mode 100644 index 0000000000..855878d029 --- /dev/null +++ b/scene/multiplayer/scene_replication_interface.h @@ -0,0 +1,84 @@ +/*************************************************************************/ +/* scene_replication_interface.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 SCENE_TREE_REPLICATOR_INTERFACE_H +#define SCENE_TREE_REPLICATOR_INTERFACE_H + +#include "core/multiplayer/multiplayer_api.h" + +#include "scene/multiplayer/scene_replication_state.h" + +class SceneReplicationInterface : public MultiplayerReplicationInterface { + GDCLASS(SceneReplicationInterface, MultiplayerReplicationInterface); + +private: + void _send_sync(int p_peer, uint64_t p_msec); + Error _send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer); + Error _send_despawn(Node *p_node, int p_peer); + Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable); + + void _free_remotes(int p_peer); + + Ref<SceneReplicationState> rep_state; + MultiplayerAPI *multiplayer; + PackedByteArray packet_cache; + int sync_mtu = 1350; // Highly dependent on underlying protocol. + + // An hack to apply the initial state before ready. + ObjectID pending_spawn; + const uint8_t *pending_buffer = nullptr; + int pending_buffer_size = 0; + +protected: + static MultiplayerReplicationInterface *_create(MultiplayerAPI *p_multiplayer); + +public: + static void make_default(); + + virtual void on_reset() override; + virtual void on_peer_change(int p_id, bool p_connected) override; + + virtual Error on_spawn(Object *p_obj, Variant p_config) override; + virtual Error on_despawn(Object *p_obj, Variant p_config) override; + virtual Error on_replication_start(Object *p_obj, Variant p_config) override; + virtual Error on_replication_stop(Object *p_obj, Variant p_config) override; + virtual void on_network_process() override; + + virtual Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + virtual Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + virtual Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + + SceneReplicationInterface(MultiplayerAPI *p_multiplayer) { + rep_state.instantiate(); + multiplayer = p_multiplayer; + } +}; + +#endif // SCENE_TREE_REPLICATOR_INTERFACE_H diff --git a/scene/multiplayer/scene_replication_state.cpp b/scene/multiplayer/scene_replication_state.cpp new file mode 100644 index 0000000000..b8dadeff24 --- /dev/null +++ b/scene/multiplayer/scene_replication_state.cpp @@ -0,0 +1,258 @@ +/*************************************************************************/ +/* scene_replication_state.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 "scene/multiplayer/scene_replication_state.h" + +#include "core/multiplayer/multiplayer_api.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" +#include "scene/scene_string_names.h" + +SceneReplicationState::TrackedNode &SceneReplicationState::_track(const ObjectID &p_id) { + if (!tracked_nodes.has(p_id)) { + tracked_nodes[p_id] = TrackedNode(p_id); + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id)); + node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack), varray(p_id), Node::CONNECT_ONESHOT); + } + return tracked_nodes[p_id]; +} + +void SceneReplicationState::_untrack(const ObjectID &p_id) { + if (tracked_nodes.has(p_id)) { + uint32_t net_id = tracked_nodes[p_id].net_id; + uint32_t peer = tracked_nodes[p_id].remote_peer; + tracked_nodes.erase(p_id); + // If it was spawned by a remote, remove it from the received nodes. + if (peer && peers_info.has(peer)) { + peers_info[peer].recv_nodes.erase(net_id); + } + // If we spawned or synced it, we need to remove it from any peer it was sent to. + if (net_id || peer == 0) { + const int *k = nullptr; + while ((k = peers_info.next(k))) { + peers_info.get(*k).known_nodes.erase(p_id); + } + } + } +} + +const HashMap<uint32_t, ObjectID> SceneReplicationState::peer_get_remotes(int p_peer) const { + return peers_info.has(p_peer) ? peers_info[p_peer].recv_nodes : HashMap<uint32_t, ObjectID>(); +} + +bool SceneReplicationState::update_last_node_sync(const ObjectID &p_id, uint16_t p_time) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, false); + if (p_time <= tnode->last_sync && tnode->last_sync - p_time < 32767) { + return false; + } + tnode->last_sync = p_time; + return true; +} + +bool SceneReplicationState::update_sync_time(const ObjectID &p_id, uint64_t p_msec) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, false); + MultiplayerSynchronizer *sync = get_synchronizer(p_id); + if (!sync) { + return false; + } + if (tnode->last_sync_msec == p_msec) { + return true; + } + if (p_msec >= tnode->last_sync_msec + sync->get_replication_interval_msec()) { + tnode->last_sync_msec = p_msec; + return true; + } + return false; +} + +const Set<ObjectID> SceneReplicationState::get_known_nodes(int p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), Set<ObjectID>()); + return peers_info[p_peer].known_nodes; +} + +uint32_t SceneReplicationState::get_net_id(const ObjectID &p_id) const { + const TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, 0); + return tnode->net_id; +} + +void SceneReplicationState::set_net_id(const ObjectID &p_id, uint32_t p_net_id) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND(!tnode); + tnode->net_id = p_net_id; +} + +uint32_t SceneReplicationState::ensure_net_id(const ObjectID &p_id) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, 0); + if (tnode->net_id == 0) { + tnode->net_id = ++last_net_id; + } + return tnode->net_id; +} + +void SceneReplicationState::on_peer_change(int p_peer, bool p_connected) { + if (p_connected) { + peers_info[p_peer] = PeerInfo(); + known_peers.insert(p_peer); + } else { + peers_info.erase(p_peer); + known_peers.erase(p_peer); + } +} + +void SceneReplicationState::reset() { + peers_info.clear(); + known_peers.clear(); + // Tracked nodes are cleared on deletion, here we only reset the ids so they can be later re-assigned. + const ObjectID *oid = nullptr; + while ((oid = tracked_nodes.next(oid))) { + TrackedNode &tobj = tracked_nodes[*oid]; + tobj.net_id = 0; + tobj.remote_peer = 0; + tobj.last_sync = 0; + } +} + +Error SceneReplicationState::config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner) { + const ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.spawner != ObjectID(), ERR_ALREADY_IN_USE); + tobj.spawner = p_spawner->get_instance_id(); + spawned_nodes.insert(oid); + // The spawner may be notified after the synchronizer. + path_only_nodes.erase(oid); + return OK; +} + +Error SceneReplicationState::config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner) { + const ObjectID oid = p_node->get_instance_id(); + ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.spawner != p_spawner->get_instance_id(), ERR_INVALID_PARAMETER); + tobj.spawner = ObjectID(); + spawned_nodes.erase(oid); + return OK; +} + +Error SceneReplicationState::config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync) { + const ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.synchronizer != ObjectID(), ERR_ALREADY_IN_USE); + tobj.synchronizer = p_sync->get_instance_id(); + // If it doesn't have a spawner, we might need to assign ID for this node using it's path. + if (tobj.spawner.is_null()) { + path_only_nodes.insert(oid); + } + return OK; +} + +Error SceneReplicationState::config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync) { + const ObjectID oid = p_node->get_instance_id(); + ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.synchronizer != p_sync->get_instance_id(), ERR_INVALID_PARAMETER); + tobj.synchronizer = ObjectID(); + if (path_only_nodes.has(oid)) { + p_node->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack)); + _untrack(oid); + path_only_nodes.erase(oid); + } + return OK; +} + +Error SceneReplicationState::peer_add_node(int p_peer, const ObjectID &p_id) { + if (p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER); + peers_info[p_peer].known_nodes.insert(p_id); + } else { + const int *pid = nullptr; + while ((pid = peers_info.next(pid))) { + peers_info.get(*pid).known_nodes.insert(p_id); + } + } + return OK; +} + +Error SceneReplicationState::peer_del_node(int p_peer, const ObjectID &p_id) { + if (p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER); + peers_info[p_peer].known_nodes.erase(p_id); + } else { + const int *pid = nullptr; + while ((pid = peers_info.next(pid))) { + peers_info.get(*pid).known_nodes.erase(p_id); + } + } + return OK; +} + +Node *SceneReplicationState::peer_get_remote(int p_peer, uint32_t p_net_id) { + PeerInfo *info = peers_info.getptr(p_peer); + return info && info->recv_nodes.has(p_net_id) ? Object::cast_to<Node>(ObjectDB::get_instance(info->recv_nodes[p_net_id])) : nullptr; +} + +Error SceneReplicationState::peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner) { + ERR_FAIL_COND_V(!p_node || !p_spawner, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAVAILABLE); + PeerInfo &pinfo = peers_info[p_peer]; + ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + tobj.spawner = p_spawner->get_instance_id(); + tobj.net_id = p_net_id; + tobj.remote_peer = p_peer; + tobj.last_sync = pinfo.last_recv_sync; + // Also track as a remote. + ERR_FAIL_COND_V(pinfo.recv_nodes.has(p_net_id), ERR_ALREADY_IN_USE); + pinfo.recv_nodes[p_net_id] = oid; + return OK; +} + +Error SceneReplicationState::peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAUTHORIZED); + PeerInfo &info = peers_info[p_peer]; + ERR_FAIL_COND_V(!info.recv_nodes.has(p_net_id), ERR_UNAUTHORIZED); + *r_node = Object::cast_to<Node>(ObjectDB::get_instance(info.recv_nodes[p_net_id])); + info.recv_nodes.erase(p_net_id); + return OK; +} + +uint16_t SceneReplicationState::peer_sync_next(int p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), 0); + PeerInfo &info = peers_info[p_peer]; + return ++info.last_sent_sync; +} + +void SceneReplicationState::peer_sync_recv(int p_peer, uint16_t p_time) { + ERR_FAIL_COND(!peers_info.has(p_peer)); + peers_info[p_peer].last_recv_sync = p_time; +} diff --git a/scene/multiplayer/scene_replication_state.h b/scene/multiplayer/scene_replication_state.h new file mode 100644 index 0000000000..18e4d9fa39 --- /dev/null +++ b/scene/multiplayer/scene_replication_state.h @@ -0,0 +1,121 @@ +/*************************************************************************/ +/* scene_replication_state.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 SCENE_REPLICATON_STATE_H +#define SCENE_REPLICATON_STATE_H + +#include "core/object/ref_counted.h" + +class MultiplayerSpawner; +class MultiplayerSynchronizer; +class Node; + +class SceneReplicationState : public RefCounted { +private: + struct TrackedNode { + ObjectID id; + uint32_t net_id = 0; + uint32_t remote_peer = 0; + ObjectID spawner; + ObjectID synchronizer; + uint16_t last_sync = 0; + uint64_t last_sync_msec = 0; + + bool operator==(const ObjectID &p_other) { return id == p_other; } + + Node *get_node() const { return id.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(id)) : nullptr; } + MultiplayerSpawner *get_spawner() const { return spawner.is_valid() ? Object::cast_to<MultiplayerSpawner>(ObjectDB::get_instance(spawner)) : nullptr; } + MultiplayerSynchronizer *get_synchronizer() const { return synchronizer.is_valid() ? Object::cast_to<MultiplayerSynchronizer>(ObjectDB::get_instance(synchronizer)) : nullptr; } + TrackedNode() {} + TrackedNode(const ObjectID &p_id) { id = p_id; } + TrackedNode(const ObjectID &p_id, uint32_t p_net_id) { + id = p_id; + net_id = p_net_id; + } + }; + + struct PeerInfo { + Set<ObjectID> known_nodes; + HashMap<uint32_t, ObjectID> recv_nodes; + uint16_t last_sent_sync = 0; + uint16_t last_recv_sync = 0; + }; + + Set<int> known_peers; + uint32_t last_net_id = 0; + HashMap<ObjectID, TrackedNode> tracked_nodes; + HashMap<int, PeerInfo> peers_info; + Set<ObjectID> spawned_nodes; + Set<ObjectID> path_only_nodes; + + TrackedNode &_track(const ObjectID &p_id); + void _untrack(const ObjectID &p_id); + bool is_tracked(const ObjectID &p_id) const { return tracked_nodes.has(p_id); } + +public: + const Set<int> get_peers() const { return known_peers; } + const Set<ObjectID> get_spawned_nodes() const { return spawned_nodes; } + const Set<ObjectID> get_path_only_nodes() const { return path_only_nodes; } + + MultiplayerSynchronizer *get_synchronizer(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_synchronizer() : nullptr; } + MultiplayerSpawner *get_spawner(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_spawner() : nullptr; } + Node *get_node(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_node() : nullptr; } + bool update_last_node_sync(const ObjectID &p_id, uint16_t p_time); + bool update_sync_time(const ObjectID &p_id, uint64_t p_msec); + + const Set<ObjectID> get_known_nodes(int p_peer); + uint32_t get_net_id(const ObjectID &p_id) const; + void set_net_id(const ObjectID &p_id, uint32_t p_net_id); + uint32_t ensure_net_id(const ObjectID &p_id); + + void reset(); + void on_peer_change(int p_peer, bool p_connected); + + Error config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner); + Error config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner); + + Error config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync); + Error config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync); + + Error peer_add_node(int p_peer, const ObjectID &p_id); + Error peer_del_node(int p_peer, const ObjectID &p_id); + + const HashMap<uint32_t, ObjectID> peer_get_remotes(int p_peer) const; + Node *peer_get_remote(int p_peer, uint32_t p_net_id); + Error peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner); + Error peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node); + + uint16_t peer_sync_next(int p_peer); + void peer_sync_recv(int p_peer, uint16_t p_time); + + SceneReplicationState() {} +}; + +#endif // SCENE_REPLICATON_STATE_H diff --git a/scene/property_utils.cpp b/scene/property_utils.cpp index 2540a633a9..a9b7e9acbe 100644 --- a/scene/property_utils.cpp +++ b/scene/property_utils.cpp @@ -130,7 +130,7 @@ Variant PropertyUtils::get_property_default_value(const Object *p_object, const if (p != -1 && p < prop_str.length() - 1) { bool all_digits = true; for (int i = p + 1; i < prop_str.length(); i++) { - if (prop_str[i] < '0' || prop_str[i] > '9') { + if (!is_digit(prop_str[i])) { all_digits = false; break; } diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 838cef824b..f19b018982 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -134,6 +134,9 @@ #include "scene/main/timer.h" #include "scene/main/viewport.h" #include "scene/main/window.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" +#include "scene/multiplayer/scene_replication_interface.h" #include "scene/resources/audio_stream_sample.h" #include "scene/resources/bit_map.h" #include "scene/resources/box_shape_3d.h" @@ -301,6 +304,8 @@ void register_scene_types() { GDREGISTER_CLASS(SubViewport); GDREGISTER_CLASS(ViewportTexture); GDREGISTER_CLASS(HTTPRequest); + GDREGISTER_CLASS(MultiplayerSpawner); + GDREGISTER_CLASS(MultiplayerSynchronizer); GDREGISTER_CLASS(Timer); GDREGISTER_CLASS(CanvasLayer); GDREGISTER_CLASS(CanvasModulate); @@ -822,6 +827,8 @@ void register_scene_types() { GDREGISTER_CLASS(Font); GDREGISTER_CLASS(Curve); + GDREGISTER_CLASS(SceneReplicationConfig); + GDREGISTER_CLASS(TextLine); GDREGISTER_CLASS(TextParagraph); @@ -1050,6 +1057,7 @@ void register_scene_types() { } SceneDebugger::initialize(); + SceneReplicationInterface::make_default(); NativeExtensionManager::get_singleton()->initialize_extensions(NativeExtension::INITIALIZATION_LEVEL_SCENE); } diff --git a/scene/resources/scene_replication_config.cpp b/scene/resources/scene_replication_config.cpp new file mode 100644 index 0000000000..2acc0f1922 --- /dev/null +++ b/scene/resources/scene_replication_config.cpp @@ -0,0 +1,187 @@ +/*************************************************************************/ +/* scene_replication_config.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 "scene_replication_config.h" + +#include "core/multiplayer/multiplayer_api.h" +#include "scene/main/node.h" + +bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + + if (name.begins_with("properties/")) { + int idx = name.get_slicec('/', 1).to_int(); + String what = name.get_slicec('/', 2); + + if (properties.size() == idx && what == "path") { + ERR_FAIL_COND_V(p_value.get_type() != Variant::NODE_PATH, false); + NodePath path = p_value; + ERR_FAIL_COND_V(path.is_empty() || path.get_subname_count() == 0, false); + add_property(path); + return true; + } + ERR_FAIL_COND_V(p_value.get_type() != Variant::BOOL, false); + ERR_FAIL_INDEX_V(idx, properties.size(), false); + ReplicationProperty &prop = properties[idx]; + if (what == "sync") { + prop.sync = p_value; + sync_props.push_back(prop.name); + return true; + } else if (what == "spawn") { + prop.spawn = p_value; + spawn_props.push_back(prop.name); + return true; + } + } + return false; +} + +bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) const { + String name = p_name; + + if (name.begins_with("properties/")) { + int idx = name.get_slicec('/', 1).to_int(); + String what = name.get_slicec('/', 2); + ERR_FAIL_INDEX_V(idx, properties.size(), false); + const ReplicationProperty &prop = properties[idx]; + if (what == "path") { + r_ret = prop.name; + return true; + } else if (what == "sync") { + r_ret = prop.sync; + return true; + } else if (what == "spawn") { + r_ret = prop.spawn; + return true; + } + } + return false; +} + +void SceneReplicationConfig::_get_property_list(List<PropertyInfo> *p_list) const { + for (int i = 0; i < properties.size(); i++) { + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/spawn", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + } +} + +TypedArray<NodePath> SceneReplicationConfig::get_properties() const { + TypedArray<NodePath> paths; + for (const ReplicationProperty &prop : properties) { + paths.push_back(prop.name); + } + return paths; +} + +void SceneReplicationConfig::add_property(const NodePath &p_path, int p_index) { + ERR_FAIL_COND(properties.find(p_path)); + + if (p_index < 0 || p_index == properties.size()) { + properties.push_back(ReplicationProperty(p_path)); + return; + } + + ERR_FAIL_INDEX(p_index, properties.size()); + + List<ReplicationProperty>::Element *I = properties.front(); + int c = 0; + while (c < p_index) { + I = I->next(); + c++; + } + properties.insert_before(I, ReplicationProperty(p_path)); +} + +void SceneReplicationConfig::remove_property(const NodePath &p_path) { + properties.erase(p_path); +} + +int SceneReplicationConfig::property_get_index(const NodePath &p_path) const { + for (int i = 0; i < properties.size(); i++) { + if (properties[i].name == p_path) { + return i; + } + } + ERR_FAIL_V(-1); +} + +bool SceneReplicationConfig::property_get_spawn(const NodePath &p_path) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND_V(!E, false); + return E->get().spawn; +} + +void SceneReplicationConfig::property_set_spawn(const NodePath &p_path, bool p_enabled) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND(!E); + if (E->get().spawn == p_enabled) { + return; + } + E->get().spawn = p_enabled; + spawn_props.clear(); + for (const ReplicationProperty &prop : properties) { + if (prop.spawn) { + spawn_props.push_back(p_path); + } + } +} + +bool SceneReplicationConfig::property_get_sync(const NodePath &p_path) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND_V(!E, false); + return E->get().sync; +} + +void SceneReplicationConfig::property_set_sync(const NodePath &p_path, bool p_enabled) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND(!E); + if (E->get().sync == p_enabled) { + return; + } + E->get().sync = p_enabled; + sync_props.clear(); + for (const ReplicationProperty &prop : properties) { + if (prop.sync) { + sync_props.push_back(p_path); + } + } +} + +void SceneReplicationConfig::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_properties"), &SceneReplicationConfig::get_properties); + ClassDB::bind_method(D_METHOD("add_property", "path", "index"), &SceneReplicationConfig::add_property, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("remove_property", "path"), &SceneReplicationConfig::remove_property); + ClassDB::bind_method(D_METHOD("property_get_index", "path"), &SceneReplicationConfig::property_get_index); + ClassDB::bind_method(D_METHOD("property_get_spawn", "path"), &SceneReplicationConfig::property_get_spawn); + ClassDB::bind_method(D_METHOD("property_set_spawn", "path", "enabled"), &SceneReplicationConfig::property_set_spawn); + ClassDB::bind_method(D_METHOD("property_get_sync", "path"), &SceneReplicationConfig::property_get_sync); + ClassDB::bind_method(D_METHOD("property_set_sync", "path", "enabled"), &SceneReplicationConfig::property_set_sync); +} diff --git a/scene/resources/scene_replication_config.h b/scene/resources/scene_replication_config.h new file mode 100644 index 0000000000..b791be9414 --- /dev/null +++ b/scene/resources/scene_replication_config.h @@ -0,0 +1,90 @@ +/*************************************************************************/ +/* scene_replication_config.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 SCENE_REPLICATION_CONFIG_H +#define SCENE_REPLICATION_CONFIG_H + +#include "core/io/resource.h" + +#include "core/variant/typed_array.h" + +class SceneReplicationConfig : public Resource { + GDCLASS(SceneReplicationConfig, Resource); + OBJ_SAVE_TYPE(SceneReplicationConfig); + RES_BASE_EXTENSION("repl"); + +private: + struct ReplicationProperty { + NodePath name; + bool spawn = true; + bool sync = true; + + bool operator==(const ReplicationProperty &p_to) { + return name == p_to.name; + } + + ReplicationProperty() {} + + ReplicationProperty(const NodePath &p_name) { + name = p_name; + } + }; + + List<ReplicationProperty> properties; + List<NodePath> spawn_props; + List<NodePath> sync_props; + +protected: + static void _bind_methods(); + + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + +public: + TypedArray<NodePath> get_properties() const; + + void add_property(const NodePath &p_path, int p_index = -1); + void remove_property(const NodePath &p_path); + + int property_get_index(const NodePath &p_path) const; + bool property_get_spawn(const NodePath &p_path); + void property_set_spawn(const NodePath &p_path, bool p_enabled); + + bool property_get_sync(const NodePath &p_path); + void property_set_sync(const NodePath &p_path, bool p_enabled); + + const List<NodePath> &get_spawn_properties() { return spawn_props; } + const List<NodePath> &get_sync_properties() { return sync_props; } + + SceneReplicationConfig() {} +}; + +#endif // SCENE_REPLICATION_CONFIG_H diff --git a/scene/resources/syntax_highlighter.cpp b/scene/resources/syntax_highlighter.cpp index e0aa21ac37..f1eddd8ffc 100644 --- a/scene/resources/syntax_highlighter.cpp +++ b/scene/resources/syntax_highlighter.cpp @@ -116,14 +116,6 @@ void SyntaxHighlighter::_bind_methods() { //////////////////////////////////////////////////////////////////////////////// -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - -static bool _is_hex_symbol(char32_t c) { - return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); -} - Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { Dictionary color_map; @@ -166,7 +158,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { color = font_color; bool is_char = !is_symbol(str[j]); bool is_a_symbol = is_symbol(str[j]); - bool is_number = (str[j] >= '0' && str[j] <= '9'); + bool is_number = is_digit(str[j]); /* color regions */ if (is_a_symbol || in_region != -1) { @@ -304,7 +296,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { } // Allow ABCDEF in hex notation. - if (is_hex_notation && (_is_hex_symbol(str[j]) || is_number)) { + if (is_hex_notation && (is_hex_digit(str[j]) || is_number)) { is_number = true; } else { is_hex_notation = false; @@ -321,7 +313,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { } } - if (!in_word && _is_char(str[j]) && !is_number) { + if (!in_word && (is_ascii_char(str[j]) || is_underscore(str[j])) && !is_number) { in_word = true; } diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 0c7f069004..dd2f3d2202 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -1106,10 +1106,6 @@ String VisualShader::generate_preview_shader(Type p_type, int p_node, int p_port return final_code; } -#define IS_INITIAL_CHAR(m_d) (((m_d) >= 'a' && (m_d) <= 'z') || ((m_d) >= 'A' && (m_d) <= 'Z')) - -#define IS_SYMBOL_CHAR(m_d) (((m_d) >= 'a' && (m_d) <= 'z') || ((m_d) >= 'A' && (m_d) <= 'Z') || ((m_d) >= '0' && (m_d) <= '9') || (m_d) == '_') - String VisualShader::validate_port_name(const String &p_port_name, VisualShaderNode *p_node, int p_port_id, bool p_output) const { String name = p_port_name; @@ -1117,7 +1113,7 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN return String(); } - while (name.length() && !IS_INITIAL_CHAR(name[0])) { + while (name.length() && !is_ascii_char(name[0])) { name = name.substr(1, name.length() - 1); } @@ -1125,7 +1121,7 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN String valid_name; for (int i = 0; i < name.length(); i++) { - if (IS_SYMBOL_CHAR(name[i])) { + if (is_ascii_identifier_char(name[i])) { valid_name += String::chr(name[i]); } else if (name[i] == ' ') { valid_name += "_"; @@ -1162,14 +1158,14 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN String VisualShader::validate_uniform_name(const String &p_name, const Ref<VisualShaderNodeUniform> &p_uniform) const { String name = p_name; //validate name first - while (name.length() && !IS_INITIAL_CHAR(name[0])) { + while (name.length() && !is_ascii_char(name[0])) { name = name.substr(1, name.length() - 1); } if (!name.is_empty()) { String valid_name; for (int i = 0; i < name.length(); i++) { - if (IS_SYMBOL_CHAR(name[i])) { + if (is_ascii_identifier_char(name[i])) { valid_name += String::chr(name[i]); } else if (name[i] == ' ') { valid_name += "_"; @@ -1206,7 +1202,7 @@ String VisualShader::validate_uniform_name(const String &p_name, const Ref<Visua if (exists) { //remove numbers, put new and try again attempt++; - while (name.length() && name[name.length() - 1] >= '0' && name[name.length() - 1] <= '9') { + while (name.length() && is_digit(name[name.length() - 1])) { name = name.substr(0, name.length() - 1); } ERR_FAIL_COND_V(name.is_empty(), String()); diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 5ded5cf214..d880df2a9b 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -380,6 +380,7 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("screen_get_scale", "screen"), &DisplayServer::screen_get_scale, DEFVAL(SCREEN_OF_MAIN_WINDOW)); ClassDB::bind_method(D_METHOD("screen_is_touchscreen", "screen"), &DisplayServer::screen_is_touchscreen, DEFVAL(SCREEN_OF_MAIN_WINDOW)); ClassDB::bind_method(D_METHOD("screen_get_max_scale"), &DisplayServer::screen_get_max_scale); + ClassDB::bind_method(D_METHOD("screen_get_refresh_rate", "screen"), &DisplayServer::screen_get_refresh_rate, DEFVAL(SCREEN_OF_MAIN_WINDOW)); ClassDB::bind_method(D_METHOD("screen_set_orientation", "orientation", "screen"), &DisplayServer::screen_set_orientation, DEFVAL(SCREEN_OF_MAIN_WINDOW)); ClassDB::bind_method(D_METHOD("screen_get_orientation", "screen"), &DisplayServer::screen_get_orientation, DEFVAL(SCREEN_OF_MAIN_WINDOW)); @@ -539,6 +540,7 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(WINDOW_MODE_MINIMIZED); BIND_ENUM_CONSTANT(WINDOW_MODE_MAXIMIZED); BIND_ENUM_CONSTANT(WINDOW_MODE_FULLSCREEN); + BIND_ENUM_CONSTANT(WINDOW_MODE_EXCLUSIVE_FULLSCREEN); BIND_ENUM_CONSTANT(WINDOW_FLAG_RESIZE_DISABLED); BIND_ENUM_CONSTANT(WINDOW_FLAG_BORDERLESS); diff --git a/servers/display_server.h b/servers/display_server.h index 8c6586dc20..19bb111094 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -53,7 +53,8 @@ public: WINDOW_MODE_WINDOWED, WINDOW_MODE_MINIMIZED, WINDOW_MODE_MAXIMIZED, - WINDOW_MODE_FULLSCREEN + WINDOW_MODE_FULLSCREEN, + WINDOW_MODE_EXCLUSIVE_FULLSCREEN, }; // Keep the VSyncMode enum values in sync with the `display/window/vsync/vsync_mode` @@ -174,6 +175,8 @@ public: SCREEN_OF_MAIN_WINDOW = -1 }; + const float SCREEN_REFRESH_RATE_FALLBACK = 60.0; // Returned by screen_get_refresh_rate if the method fails. Most screens are 60hz as of 2022. + virtual int get_screen_count() const = 0; virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const = 0; virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const = 0; @@ -188,6 +191,7 @@ public: } return scale; } + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const = 0; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; // Keep the ScreenOrientation enum values in sync with the `display/window/handheld/orientation` diff --git a/servers/display_server_headless.h b/servers/display_server_headless.h index 4ef9dc622f..f74a8fad23 100644 --- a/servers/display_server_headless.h +++ b/servers/display_server_headless.h @@ -62,6 +62,7 @@ public: int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override { return 96; /* 0 might cause issues */ } float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override { return 1; } float screen_get_max_scale() const override { return 1; } + float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override { return SCREEN_REFRESH_RATE_FALLBACK; } Vector<DisplayServer::WindowID> get_window_list() const override { return Vector<DisplayServer::WindowID>(); } diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index 2e1ee41406..a433c666f3 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -35,18 +35,6 @@ #define HAS_WARNING(flag) (warning_flags & flag) -static bool _is_text_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; -} - -static bool _is_number(char32_t c) { - return (c >= '0' && c <= '9'); -} - -static bool _is_hex(char32_t c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} - String ShaderLanguage::get_operator_text(Operator p_op) { static const char *op_names[OP_MAX] = { "==", "!=", @@ -543,7 +531,7 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { default: { char_idx--; //go back one, since we have no idea what this is - if (_is_number(GETCHAR(0)) || (GETCHAR(0) == '.' && _is_number(GETCHAR(1)))) { + if (is_digit(GETCHAR(0)) || (GETCHAR(0) == '.' && is_digit(GETCHAR(1)))) { // parse number bool hexa_found = false; bool period_found = false; @@ -584,7 +572,7 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { const char32_t symbol = String::char_lowercase(GETCHAR(i)); bool error = false; - if (_is_number(symbol)) { + if (is_digit(symbol)) { if (end_suffix_found) { error = true; } @@ -617,8 +605,8 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { break; } } - } else if (!hexa_found || !_is_hex(symbol)) { - if (_is_text_char(symbol)) { + } else if (!hexa_found || !is_hex_digit(symbol)) { + if (is_ascii_identifier_char(symbol)) { error = true; } else { break; @@ -649,7 +637,7 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { return _make_token(TK_ERROR, "Invalid (hexadecimal) numeric constant"); } } else if (period_found || exponent_found || float_suffix_found) { // Float - if (exponent_found && (!_is_number(last_char) && last_char != 'f')) { // checks for eg: "2E", "2E-", "2E+" + if (exponent_found && (!is_digit(last_char) && last_char != 'f')) { // checks for eg: "2E", "2E-", "2E+" return _make_token(TK_ERROR, "Invalid (float) numeric constant"); } if (period_found) { @@ -660,7 +648,7 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { } } else { //checks for eg. "1." or "1.99" notations - if (last_char != '.' && !_is_number(last_char)) { + if (last_char != '.' && !is_digit(last_char)) { return _make_token(TK_ERROR, "Invalid (float) numeric constant"); } } @@ -723,11 +711,11 @@ ShaderLanguage::Token ShaderLanguage::_get_token() { return _make_token(TK_PERIOD); } - if (_is_text_char(GETCHAR(0))) { + if (is_ascii_identifier_char(GETCHAR(0))) { // parse identifier String str; - while (_is_text_char(GETCHAR(0))) { + while (is_ascii_identifier_char(GETCHAR(0))) { str += char32_t(GETCHAR(0)); char_idx++; } diff --git a/tests/core/math/test_math.cpp b/tests/core/math/test_math.cpp index a24a8fde2b..4182455b7a 100644 --- a/tests/core/math/test_math.cpp +++ b/tests/core/math/test_math.cpp @@ -227,7 +227,7 @@ class GetClassAndNamespace { return TK_SYMBOL; } - if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) { + if (code[idx] == '-' || is_digit(code[idx])) { //a number const char32_t *rptr; double number = String::to_float(&code[idx], &rptr); @@ -235,10 +235,10 @@ class GetClassAndNamespace { value = number; return TK_NUMBER; - } else if ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) { + } else if (is_ascii_char(code[idx]) || code[idx] > 127) { String id; - while ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) { + while (is_ascii_char(code[idx]) || code[idx] > 127) { id += code[idx]; idx++; } diff --git a/tests/core/math/test_rect2.h b/tests/core/math/test_rect2.h index d98a94b1b5..0b1106ac3c 100644 --- a/tests/core/math/test_rect2.h +++ b/tests/core/math/test_rect2.h @@ -32,15 +32,11 @@ #define TEST_RECT2_H #include "core/math/rect2.h" +#include "core/math/rect2i.h" #include "thirdparty/doctest/doctest.h" namespace TestRect2 { -// We also test Rect2i here, for consistency with the source code where Rect2 -// and Rect2i are defined in the same file. - -// Rect2 - TEST_CASE("[Rect2] Constructor methods") { const Rect2 rect = Rect2(0, 100, 1280, 720); const Rect2 rect_vector = Rect2(Vector2(0, 100), Vector2(1280, 720)); @@ -304,278 +300,6 @@ 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."); } - -// Rect2i - -TEST_CASE("[Rect2i] Constructor methods") { - Rect2i recti = Rect2i(0, 100, 1280, 720); - Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720)); - Rect2i recti_copy_recti = Rect2i(recti); - Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720)); - - CHECK_MESSAGE( - recti == recti_vector, - "Rect2is created with the same dimensions but by different methods should be equal."); - CHECK_MESSAGE( - recti == recti_copy_recti, - "Rect2is created with the same dimensions but by different methods should be equal."); - CHECK_MESSAGE( - recti == recti_copy_rect, - "Rect2is created with the same dimensions but by different methods should be equal."); -} - -TEST_CASE("[Rect2i] String conversion") { - // Note: This also depends on the Vector2 string representation. - CHECK_MESSAGE( - String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]", - "The string representation should match the expected value."); -} - -TEST_CASE("[Rect2i] Basic getters") { - const Rect2i rect = Rect2i(0, 100, 1280, 720); - CHECK_MESSAGE( - rect.get_position() == Vector2i(0, 100), - "get_position() should return the expected value."); - CHECK_MESSAGE( - rect.get_size() == Vector2i(1280, 720), - "get_size() should return the expected value."); - CHECK_MESSAGE( - rect.get_end() == Vector2i(1280, 820), - "get_end() should return the expected value."); - CHECK_MESSAGE( - rect.get_center() == Vector2i(640, 460), - "get_center() should return the expected value."); - CHECK_MESSAGE( - Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460), - "get_center() should return the expected value."); -} - -TEST_CASE("[Rect2i] Basic setters") { - Rect2i rect = Rect2i(0, 100, 1280, 720); - rect.set_end(Vector2i(4000, 4000)); - CHECK_MESSAGE( - rect == Rect2i(0, 100, 4000, 3900), - "set_end() should result in the expected Rect2i."); - - rect = Rect2i(0, 100, 1280, 720); - rect.set_position(Vector2i(4000, 4000)); - CHECK_MESSAGE( - rect == Rect2i(4000, 4000, 1280, 720), - "set_position() should result in the expected Rect2i."); - - rect = Rect2i(0, 100, 1280, 720); - rect.set_size(Vector2i(4000, 4000)); - CHECK_MESSAGE( - rect == Rect2i(0, 100, 4000, 4000), - "set_size() should result in the expected Rect2i."); -} - -TEST_CASE("[Rect2i] Area getters") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).get_area() == 921'600, - "get_area() should return the expected value."); - CHECK_MESSAGE( - Rect2i(0, 100, -1280, -720).get_area() == 921'600, - "get_area() should return the expected value."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, -720).get_area() == -921'600, - "get_area() should return the expected value."); - CHECK_MESSAGE( - Rect2i(0, 100, -1280, 720).get_area() == -921'600, - "get_area() should return the expected value."); - CHECK_MESSAGE( - Rect2i(0, 100, 0, 720).get_area() == 0, - "get_area() should return the expected value."); - - CHECK_MESSAGE( - !Rect2i(0, 100, 1280, 720).has_no_area(), - "has_no_area() should return the expected value on Rect2i with an area."); - CHECK_MESSAGE( - Rect2i(0, 100, 0, 500).has_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); - CHECK_MESSAGE( - Rect2i(0, 100, 500, 0).has_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); - CHECK_MESSAGE( - Rect2i(0, 100, 0, 0).has_no_area(), - "has_no_area() should return the expected value on Rect2i with no area."); -} - -TEST_CASE("[Rect2i] Absolute coordinates") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720), - "abs() should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720), - "abs() should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720), - "abs() should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720), - "abs() should return the expected Rect2i."); -} - -TEST_CASE("[Rect2i] Intersection") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100), - "intersection() with fully enclosed Rect2i should return the expected result."); - // The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels. - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100), - "intersection() with partially enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(), - "intersection() with non-enclosed Rect2i should return the expected result."); -} - -TEST_CASE("[Rect2i] Enclosing") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)), - "encloses() with fully contained Rect2i should return the expected result."); - CHECK_MESSAGE( - !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)), - "encloses() with partially contained Rect2i should return the expected result."); - CHECK_MESSAGE( - !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)), - "encloses() with non-contained Rect2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 100, 1280, 720)), - "encloses() with identical Rect2i should return the expected result."); -} - -TEST_CASE("[Rect2i] Expanding") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720), - "expand() with contained Vector2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820), - "expand() with non-contained Vector2i should return the expected result."); -} - -TEST_CASE("[Rect2i] Growing") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920), - "grow() with positive value should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520), - "grow() with negative value should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280), - "grow() with large negative value should return the expected Rect2i."); - - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320), - "grow_individual() with positive values should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520), - "grow_individual() with positive and negative values should return the expected Rect2i."); - - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220), - "grow_side() with positive value should return the expected Rect2i."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220), - "grow_side() with negative value should return the expected Rect2i."); -} - -TEST_CASE("[Rect2i] Has point") { - Rect2i rect = Rect2i(0, 100, 1280, 720); - CHECK_MESSAGE( - rect.has_point(Vector2i(500, 600)), - "has_point() with contained Vector2i should return the expected result."); - CHECK_MESSAGE( - !rect.has_point(Vector2i(0, 0)), - "has_point() with non-contained Vector2i should return the expected result."); - - CHECK_MESSAGE( - rect.has_point(rect.position), - "has_point() with positive size should include `position`."); - CHECK_MESSAGE( - rect.has_point(rect.position + Vector2i(1, 1)), - "has_point() with positive size should include `position + (1, 1)`."); - CHECK_MESSAGE( - !rect.has_point(rect.position + Vector2i(1, -1)), - "has_point() with positive size should not include `position + (1, -1)`."); - CHECK_MESSAGE( - !rect.has_point(rect.position + rect.size), - "has_point() with positive size should not include `position + size`."); - CHECK_MESSAGE( - !rect.has_point(rect.position + rect.size + Vector2i(1, 1)), - "has_point() with positive size should not include `position + size + (1, 1)`."); - CHECK_MESSAGE( - rect.has_point(rect.position + rect.size + Vector2i(-1, -1)), - "has_point() with positive size should include `position + size + (-1, -1)`."); - CHECK_MESSAGE( - !rect.has_point(rect.position + rect.size + Vector2i(-1, 1)), - "has_point() with positive size should not include `position + size + (-1, 1)`."); - - CHECK_MESSAGE( - rect.has_point(rect.position + Vector2i(0, 10)), - "has_point() with point located on left edge should return true."); - CHECK_MESSAGE( - !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), - "has_point() with point located on right edge should return false."); - CHECK_MESSAGE( - rect.has_point(rect.position + Vector2i(10, 0)), - "has_point() with point located on top edge should return true."); - CHECK_MESSAGE( - !rect.has_point(rect.position + Vector2i(10, rect.size.y)), - "has_point() with point located on bottom edge should return false."); - - /* - // FIXME: Disabled for now until GH-37617 is fixed one way or another. - // More tests should then be written like for the positive size case. - rect = Rect2i(0, 100, -1280, -720); - CHECK_MESSAGE( - rect.has_point(rect.position), - "has_point() with negative size should include `position`."); - CHECK_MESSAGE( - !rect.has_point(rect.position + rect.size), - "has_point() with negative size should not include `position + size`."); - */ - - rect = Rect2i(-4000, -200, 1280, 720); - CHECK_MESSAGE( - rect.has_point(rect.position + Vector2i(0, 10)), - "has_point() with negative position and point located on left edge should return true."); - CHECK_MESSAGE( - !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), - "has_point() with negative position and point located on right edge should return false."); - CHECK_MESSAGE( - rect.has_point(rect.position + Vector2i(10, 0)), - "has_point() with negative position and point located on top edge should return true."); - CHECK_MESSAGE( - !rect.has_point(rect.position + Vector2i(10, rect.size.y)), - "has_point() with negative position and point located on bottom edge should return false."); -} - -TEST_CASE("[Rect2i] Intersection") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)), - "intersects() with fully enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)), - "intersects() with partially enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - !Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)), - "intersects() with non-enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - !Rect2i(0, 0, 2, 2).intersects(Rect2i(2, 2, 2, 2)), - "intersects() with adjacent Rect2i should return the expected result."); -} - -TEST_CASE("[Rect2i] Merging") { - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).merge(Rect2i(0, 300, 100, 100)) == Rect2i(0, 100, 1280, 720), - "merge() with fully enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).merge(Rect2i(1200, 700, 100, 100)) == Rect2i(0, 100, 1300, 720), - "merge() with partially enclosed Rect2i should return the expected result."); - CHECK_MESSAGE( - Rect2i(0, 100, 1280, 720).merge(Rect2i(-4000, -4000, 100, 100)) == Rect2i(-4000, -4000, 5280, 4820), - "merge() with non-enclosed Rect2i should return the expected result."); -} } // namespace TestRect2 #endif // TEST_RECT2_H diff --git a/tests/core/math/test_rect2i.h b/tests/core/math/test_rect2i.h new file mode 100644 index 0000000000..0d1a088a66 --- /dev/null +++ b/tests/core/math/test_rect2i.h @@ -0,0 +1,311 @@ +/*************************************************************************/ +/* test_rect2i.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 TEST_RECT2I_H +#define TEST_RECT2I_H + +#include "core/math/rect2.h" +#include "core/math/rect2i.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestRect2i { +TEST_CASE("[Rect2i] Constructor methods") { + Rect2i recti = Rect2i(0, 100, 1280, 720); + Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720)); + Rect2i recti_copy_recti = Rect2i(recti); + Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720)); + + CHECK_MESSAGE( + recti == recti_vector, + "Rect2is created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + recti == recti_copy_recti, + "Rect2is created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + recti == recti_copy_rect, + "Rect2is created with the same dimensions but by different methods should be equal."); +} + +TEST_CASE("[Rect2i] String conversion") { + // Note: This also depends on the Vector2 string representation. + CHECK_MESSAGE( + String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]", + "The string representation should match the expected value."); +} + +TEST_CASE("[Rect2i] Basic getters") { + const Rect2i rect = Rect2i(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.get_position() == Vector2i(0, 100), + "get_position() should return the expected value."); + CHECK_MESSAGE( + rect.get_size() == Vector2i(1280, 720), + "get_size() should return the expected value."); + CHECK_MESSAGE( + rect.get_end() == Vector2i(1280, 820), + "get_end() should return the expected value."); + CHECK_MESSAGE( + rect.get_center() == Vector2i(640, 460), + "get_center() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460), + "get_center() should return the expected value."); +} + +TEST_CASE("[Rect2i] Basic setters") { + Rect2i rect = Rect2i(0, 100, 1280, 720); + rect.set_end(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(0, 100, 4000, 3900), + "set_end() should result in the expected Rect2i."); + + rect = Rect2i(0, 100, 1280, 720); + rect.set_position(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(4000, 4000, 1280, 720), + "set_position() should result in the expected Rect2i."); + + rect = Rect2i(0, 100, 1280, 720); + rect.set_size(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(0, 100, 4000, 4000), + "set_size() should result in the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Area getters") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).get_area() == 921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, -720).get_area() == 921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, -720).get_area() == -921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, 720).get_area() == -921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 720).get_area() == 0, + "get_area() should return the expected value."); + + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).has_no_area(), + "has_no_area() should return the expected value on Rect2i with an area."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 500).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); + CHECK_MESSAGE( + Rect2i(0, 100, 500, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); +} + +TEST_CASE("[Rect2i] Absolute coordinates") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720), + "abs() should return the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Intersection") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100), + "intersection() with fully enclosed Rect2i should return the expected result."); + // The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels. + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100), + "intersection() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(), + "intersection() with non-enclosed Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Enclosing") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)), + "encloses() with fully contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)), + "encloses() with partially contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)), + "encloses() with non-contained Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 100, 1280, 720)), + "encloses() with identical Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Expanding") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720), + "expand() with contained Vector2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820), + "expand() with non-contained Vector2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Growing") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920), + "grow() with positive value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520), + "grow() with negative value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280), + "grow() with large negative value should return the expected Rect2i."); + + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320), + "grow_individual() with positive values should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520), + "grow_individual() with positive and negative values should return the expected Rect2i."); + + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220), + "grow_side() with positive value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220), + "grow_side() with negative value should return the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Has point") { + Rect2i rect = Rect2i(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.has_point(Vector2i(500, 600)), + "has_point() with contained Vector2i should return the expected result."); + CHECK_MESSAGE( + !rect.has_point(Vector2i(0, 0)), + "has_point() with non-contained Vector2i should return the expected result."); + + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with positive size should include `position`."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(1, 1)), + "has_point() with positive size should include `position + (1, 1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(1, -1)), + "has_point() with positive size should not include `position + (1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with positive size should not include `position + size`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2i(1, 1)), + "has_point() with positive size should not include `position + size + (1, 1)`."); + CHECK_MESSAGE( + rect.has_point(rect.position + rect.size + Vector2i(-1, -1)), + "has_point() with positive size should include `position + size + (-1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2i(-1, 1)), + "has_point() with positive size should not include `position + size + (-1, 1)`."); + + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(0, 10)), + "has_point() with point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), + "has_point() with point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(10, 0)), + "has_point() with point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(10, rect.size.y)), + "has_point() with point located on bottom edge should return false."); + + /* + // FIXME: Disabled for now until GH-37617 is fixed one way or another. + // More tests should then be written like for the positive size case. + rect = Rect2i(0, 100, -1280, -720); + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with negative size should include `position`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with negative size should not include `position + size`."); + */ + + rect = Rect2i(-4000, -200, 1280, 720); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(0, 10)), + "has_point() with negative position and point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), + "has_point() with negative position and point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(10, 0)), + "has_point() with negative position and point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(10, rect.size.y)), + "has_point() with negative position and point located on bottom edge should return false."); +} + +TEST_CASE("[Rect2i] Intersection") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)), + "intersects() with fully enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)), + "intersects() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)), + "intersects() with non-enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 0, 2, 2).intersects(Rect2i(2, 2, 2, 2)), + "intersects() with adjacent Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Merging") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(0, 300, 100, 100)) == Rect2i(0, 100, 1280, 720), + "merge() with fully enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(1200, 700, 100, 100)) == Rect2i(0, 100, 1300, 720), + "merge() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(-4000, -4000, 100, 100)) == Rect2i(-4000, -4000, 5280, 4820), + "merge() with non-enclosed Rect2i should return the expected result."); +} +} // namespace TestRect2i + +#endif // TEST_RECT2I_H diff --git a/tests/core/math/test_vector2.h b/tests/core/math/test_vector2.h index cb447acd17..ff60467bf4 100644 --- a/tests/core/math/test_vector2.h +++ b/tests/core/math/test_vector2.h @@ -32,6 +32,7 @@ #define TEST_VECTOR2_H #include "core/math/vector2.h" +#include "core/math/vector2i.h" #include "tests/test_macros.h" namespace TestVector2 { diff --git a/tests/core/math/test_vector2i.h b/tests/core/math/test_vector2i.h index 86e254654d..841bb793a4 100644 --- a/tests/core/math/test_vector2i.h +++ b/tests/core/math/test_vector2i.h @@ -32,6 +32,7 @@ #define TEST_VECTOR2I_H #include "core/math/vector2.h" +#include "core/math/vector2i.h" #include "tests/test_macros.h" namespace TestVector2i { diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 0190fa5184..3b51a6d805 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -48,6 +48,7 @@ #include "tests/core/math/test_math.h" #include "tests/core/math/test_random_number_generator.h" #include "tests/core/math/test_rect2.h" +#include "tests/core/math/test_rect2i.h" #include "tests/core/math/test_vector2.h" #include "tests/core/math/test_vector2i.h" #include "tests/core/math/test_vector3.h" diff --git a/thirdparty/misc/patches/polypartition-godot-types.patch b/thirdparty/misc/patches/polypartition-godot-types.patch index 782f02e8dc..61737f9fd2 100644 --- a/thirdparty/misc/patches/polypartition-godot-types.patch +++ b/thirdparty/misc/patches/polypartition-godot-types.patch @@ -1,19 +1,16 @@ diff --git a/thirdparty/misc/polypartition.cpp b/thirdparty/misc/polypartition.cpp -index 3a8a6efa83..5e94793b79 100644 +index 3a8a6efa83..8c5409bf24 100644 --- a/thirdparty/misc/polypartition.cpp +++ b/thirdparty/misc/polypartition.cpp -@@ -23,10 +23,7 @@ - - #include "polypartition.h" - --#include <math.h> --#include <string.h> +@@ -26,7 +26,6 @@ + #include <math.h> + #include <string.h> #include <algorithm> -#include <vector> TPPLPoly::TPPLPoly() { hole = false; -@@ -186,7 +183,7 @@ int TPPLPartition::Intersects(TPPLPoint &p11, TPPLPoint &p12, TPPLPoint &p21, TP +@@ -186,7 +185,7 @@ int TPPLPartition::Intersects(TPPLPoint &p11, TPPLPoint &p12, TPPLPoint &p21, TP // Removes holes from inpolys by merging them with non-holes. int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { TPPLPolyList polys; @@ -22,7 +19,7 @@ index 3a8a6efa83..5e94793b79 100644 long i, i2, holepointindex, polypointindex; TPPLPoint holepoint, polypoint, bestpolypoint; TPPLPoint linep1, linep2; -@@ -198,15 +195,15 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -198,15 +197,15 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { // Check for the trivial case of no holes. hasholes = false; @@ -42,7 +39,7 @@ index 3a8a6efa83..5e94793b79 100644 } return 1; } -@@ -216,8 +213,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -216,8 +215,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { while (1) { // Find the hole point with the largest x. hasholes = false; @@ -53,7 +50,7 @@ index 3a8a6efa83..5e94793b79 100644 continue; } -@@ -227,8 +224,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -227,8 +226,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { holepointindex = 0; } @@ -64,7 +61,7 @@ index 3a8a6efa83..5e94793b79 100644 holeiter = iter; holepointindex = i; } -@@ -237,24 +234,24 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -237,24 +236,24 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { if (!hasholes) { break; } @@ -98,7 +95,7 @@ index 3a8a6efa83..5e94793b79 100644 if (pointfound) { v1 = Normalize(polypoint - holepoint); v2 = Normalize(bestpolypoint - holepoint); -@@ -263,13 +260,13 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -263,13 +262,13 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { } } pointvisible = true; @@ -117,7 +114,7 @@ index 3a8a6efa83..5e94793b79 100644 if (Intersects(holepoint, polypoint, linep1, linep2)) { pointvisible = false; break; -@@ -292,18 +289,18 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -292,18 +291,18 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { return 0; } @@ -142,7 +139,7 @@ index 3a8a6efa83..5e94793b79 100644 i2++; } -@@ -312,8 +309,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { +@@ -312,8 +311,8 @@ int TPPLPartition::RemoveHoles(TPPLPolyList *inpolys, TPPLPolyList *outpolys) { polys.push_back(newpoly); } @@ -153,7 +150,7 @@ index 3a8a6efa83..5e94793b79 100644 } return 1; -@@ -524,13 +521,13 @@ int TPPLPartition::Triangulate_EC(TPPLPoly *poly, TPPLPolyList *triangles) { +@@ -524,13 +523,13 @@ int TPPLPartition::Triangulate_EC(TPPLPoly *poly, TPPLPolyList *triangles) { int TPPLPartition::Triangulate_EC(TPPLPolyList *inpolys, TPPLPolyList *triangles) { TPPLPolyList outpolys; @@ -170,7 +167,7 @@ index 3a8a6efa83..5e94793b79 100644 return 0; } } -@@ -543,7 +540,7 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -543,7 +542,7 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { } TPPLPolyList triangles; @@ -179,7 +176,7 @@ index 3a8a6efa83..5e94793b79 100644 TPPLPoly *poly1 = NULL, *poly2 = NULL; TPPLPoly newpoly; TPPLPoint d1, d2, p1, p2, p3; -@@ -578,19 +575,19 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -578,19 +577,19 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { return 0; } @@ -203,7 +200,7 @@ index 3a8a6efa83..5e94793b79 100644 for (i21 = 0; i21 < poly2->GetNumPoints(); i21++) { if ((d2.x != poly2->GetPoint(i21).x) || (d2.y != poly2->GetPoint(i21).y)) { -@@ -660,16 +657,16 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -660,16 +659,16 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { } triangles.erase(iter2); @@ -224,7 +221,7 @@ index 3a8a6efa83..5e94793b79 100644 } return 1; -@@ -677,13 +674,13 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -677,13 +676,13 @@ int TPPLPartition::ConvexPartition_HM(TPPLPoly *poly, TPPLPolyList *parts) { int TPPLPartition::ConvexPartition_HM(TPPLPolyList *inpolys, TPPLPolyList *parts) { TPPLPolyList outpolys; @@ -241,7 +238,7 @@ index 3a8a6efa83..5e94793b79 100644 return 0; } } -@@ -824,8 +821,8 @@ int TPPLPartition::Triangulate_OPT(TPPLPoly *poly, TPPLPolyList *triangles) { +@@ -824,8 +823,8 @@ int TPPLPartition::Triangulate_OPT(TPPLPoly *poly, TPPLPolyList *triangles) { newdiagonal.index1 = 0; newdiagonal.index2 = n - 1; diagonals.push_back(newdiagonal); @@ -252,7 +249,7 @@ index 3a8a6efa83..5e94793b79 100644 diagonals.pop_front(); bestvertex = dpstates[diagonal.index2][diagonal.index1].bestvertex; if (bestvertex == -1) { -@@ -873,10 +870,10 @@ void TPPLPartition::UpdateState(long a, long b, long w, long i, long j, DPState2 +@@ -873,10 +872,10 @@ void TPPLPartition::UpdateState(long a, long b, long w, long i, long j, DPState2 pairs->push_front(newdiagonal); dpstates[a][b].weight = w; } else { @@ -265,7 +262,7 @@ index 3a8a6efa83..5e94793b79 100644 pairs->pop_front(); } pairs->push_front(newdiagonal); -@@ -885,7 +882,7 @@ void TPPLPartition::UpdateState(long a, long b, long w, long i, long j, DPState2 +@@ -885,7 +884,7 @@ void TPPLPartition::UpdateState(long a, long b, long w, long i, long j, DPState2 void TPPLPartition::TypeA(long i, long j, long k, PartitionVertex *vertices, DPState2 **dpstates) { DiagonalList *pairs = NULL; @@ -274,7 +271,7 @@ index 3a8a6efa83..5e94793b79 100644 long top; long w; -@@ -902,23 +899,23 @@ void TPPLPartition::TypeA(long i, long j, long k, PartitionVertex *vertices, DPS +@@ -902,23 +901,23 @@ void TPPLPartition::TypeA(long i, long j, long k, PartitionVertex *vertices, DPS } if (j - i > 1) { pairs = &(dpstates[i][j].pairs); @@ -305,7 +302,7 @@ index 3a8a6efa83..5e94793b79 100644 } } } -@@ -927,7 +924,7 @@ void TPPLPartition::TypeA(long i, long j, long k, PartitionVertex *vertices, DPS +@@ -927,7 +926,7 @@ void TPPLPartition::TypeA(long i, long j, long k, PartitionVertex *vertices, DPS void TPPLPartition::TypeB(long i, long j, long k, PartitionVertex *vertices, DPState2 **dpstates) { DiagonalList *pairs = NULL; @@ -314,7 +311,7 @@ index 3a8a6efa83..5e94793b79 100644 long top; long w; -@@ -946,21 +943,21 @@ void TPPLPartition::TypeB(long i, long j, long k, PartitionVertex *vertices, DPS +@@ -946,21 +945,21 @@ void TPPLPartition::TypeB(long i, long j, long k, PartitionVertex *vertices, DPS if (k - j > 1) { pairs = &(dpstates[j][k].pairs); @@ -343,7 +340,7 @@ index 3a8a6efa83..5e94793b79 100644 } } else { w++; -@@ -981,11 +978,11 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -981,11 +980,11 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { DiagonalList diagonals, diagonals2; Diagonal diagonal, newdiagonal; DiagonalList *pairs = NULL, *pairs2 = NULL; @@ -358,7 +355,7 @@ index 3a8a6efa83..5e94793b79 100644 bool ijreal, jkreal; n = poly->GetNumPoints(); -@@ -1110,35 +1107,35 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1110,35 +1109,35 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { newdiagonal.index1 = 0; newdiagonal.index2 = n - 1; diagonals.push_front(newdiagonal); @@ -403,7 +400,7 @@ index 3a8a6efa83..5e94793b79 100644 pairs2->pop_back(); } else { break; -@@ -1153,21 +1150,21 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1153,21 +1152,21 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { diagonals.push_front(newdiagonal); } } else { @@ -431,7 +428,7 @@ index 3a8a6efa83..5e94793b79 100644 pairs2->pop_front(); } else { break; -@@ -1197,8 +1194,8 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1197,8 +1196,8 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { newdiagonal.index1 = 0; newdiagonal.index2 = n - 1; diagonals.push_front(newdiagonal); @@ -442,7 +439,7 @@ index 3a8a6efa83..5e94793b79 100644 diagonals.pop_front(); if ((diagonal.index2 - diagonal.index1) <= 1) { continue; -@@ -1210,8 +1207,8 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1210,8 +1209,8 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { indices.push_back(diagonal.index2); diagonals2.push_front(diagonal); @@ -453,7 +450,7 @@ index 3a8a6efa83..5e94793b79 100644 diagonals2.pop_front(); if ((diagonal.index2 - diagonal.index1) <= 1) { continue; -@@ -1220,16 +1217,16 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1220,16 +1219,16 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { jkreal = true; pairs = &(dpstates[diagonal.index1][diagonal.index2].pairs); if (!vertices[diagonal.index1].isConvex) { @@ -476,7 +473,7 @@ index 3a8a6efa83..5e94793b79 100644 jkreal = false; } } -@@ -1253,11 +1250,12 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1253,11 +1252,12 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { indices.push_back(j); } @@ -492,7 +489,7 @@ index 3a8a6efa83..5e94793b79 100644 k++; } parts->push_back(newpoly); -@@ -1281,7 +1279,7 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { +@@ -1281,7 +1281,7 @@ int TPPLPartition::ConvexPartition_OPT(TPPLPoly *poly, TPPLPolyList *parts) { // "Computational Geometry: Algorithms and Applications" // by Mark de Berg, Otfried Cheong, Marc van Kreveld, and Mark Overmars. int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monotonePolys) { @@ -501,7 +498,7 @@ index 3a8a6efa83..5e94793b79 100644 MonotoneVertex *vertices = NULL; long i, numvertices, vindex, vindex2, newnumvertices, maxnumvertices; long polystartindex, polyendindex; -@@ -1291,11 +1289,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1291,11 +1291,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto bool error = false; numvertices = 0; @@ -515,7 +512,7 @@ index 3a8a6efa83..5e94793b79 100644 } maxnumvertices = numvertices * 3; -@@ -1303,8 +1298,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1303,8 +1300,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto newnumvertices = numvertices; polystartindex = 0; @@ -526,7 +523,7 @@ index 3a8a6efa83..5e94793b79 100644 polyendindex = polystartindex + poly->GetNumPoints() - 1; for (i = 0; i < poly->GetNumPoints(); i++) { vertices[i + polystartindex].p = poly->GetPoint(i); -@@ -1360,14 +1355,14 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1360,14 +1357,14 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto // Note that while set doesn't actually have to be implemented as // a tree, complexity requirements for operations are the same as // for the balanced binary search tree. @@ -546,7 +543,7 @@ index 3a8a6efa83..5e94793b79 100644 } // For each vertex. -@@ -1387,13 +1382,14 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1387,13 +1384,14 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto newedge.p1 = v->p; newedge.p2 = vertices[v->next].p; newedge.index = vindex; @@ -564,7 +561,7 @@ index 3a8a6efa83..5e94793b79 100644 error = true; break; } -@@ -1412,29 +1408,30 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1412,29 +1410,30 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto newedge.p1 = v->p; newedge.p2 = v->p; edgeIter = edgeTree.lower_bound(newedge); @@ -601,7 +598,7 @@ index 3a8a6efa83..5e94793b79 100644 error = true; break; } -@@ -1452,25 +1449,25 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1452,25 +1451,25 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto newedge.p1 = v->p; newedge.p2 = v->p; edgeIter = edgeTree.lower_bound(newedge); @@ -632,7 +629,7 @@ index 3a8a6efa83..5e94793b79 100644 error = true; break; } -@@ -1488,27 +1485,28 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1488,27 +1487,28 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto newedge.p1 = v2->p; newedge.p2 = vertices[v2->next].p; newedge.index = vindex2; @@ -668,7 +665,7 @@ index 3a8a6efa83..5e94793b79 100644 } break; } -@@ -1569,8 +1567,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto +@@ -1569,8 +1569,8 @@ int TPPLPartition::MonotonePartition(TPPLPolyList *inpolys, TPPLPolyList *monoto // Adds a diagonal to the doubly-connected list of vertices. void TPPLPartition::AddDiagonal(MonotoneVertex *vertices, long *numvertices, long index1, long index2, @@ -679,7 +676,7 @@ index 3a8a6efa83..5e94793b79 100644 long newindex1, newindex2; newindex1 = *numvertices; -@@ -1597,14 +1595,14 @@ void TPPLPartition::AddDiagonal(MonotoneVertex *vertices, long *numvertices, lon +@@ -1597,14 +1597,14 @@ void TPPLPartition::AddDiagonal(MonotoneVertex *vertices, long *numvertices, lon vertextypes[newindex1] = vertextypes[index1]; edgeTreeIterators[newindex1] = edgeTreeIterators[index1]; helpers[newindex1] = helpers[index1]; @@ -698,7 +695,7 @@ index 3a8a6efa83..5e94793b79 100644 } } -@@ -1830,13 +1828,13 @@ int TPPLPartition::TriangulateMonotone(TPPLPoly *inPoly, TPPLPolyList *triangles +@@ -1830,13 +1830,13 @@ int TPPLPartition::TriangulateMonotone(TPPLPoly *inPoly, TPPLPolyList *triangles int TPPLPartition::Triangulate_MONO(TPPLPolyList *inpolys, TPPLPolyList *triangles) { TPPLPolyList monotone; diff --git a/thirdparty/misc/polypartition.cpp b/thirdparty/misc/polypartition.cpp index 5e94793b79..8c5409bf24 100644 --- a/thirdparty/misc/polypartition.cpp +++ b/thirdparty/misc/polypartition.cpp @@ -23,6 +23,8 @@ #include "polypartition.h" +#include <math.h> +#include <string.h> #include <algorithm> TPPLPoly::TPPLPoly() { |