diff --git a/core/math/a_star.cpp b/core/math/a_star.cpp index 8c4825eb33d..7663a386e97 100644 --- a/core/math/a_star.cpp +++ b/core/math/a_star.cpp @@ -321,11 +321,11 @@ Vector3 AStar3D::get_closest_position_in_segment(const Vector3 &p_point) const { return closest_point; } -bool AStar3D::_solve(Point *begin_point, Point *end_point) { +bool AStar3D::_solve(Point *begin_point, Point *end_point, bool p_allow_partial_path) { last_closest_point = nullptr; pass++; - if (!end_point->enabled) { + if (!end_point->enabled && !p_allow_partial_path) { return false; } @@ -445,7 +445,7 @@ Vector AStar3D::get_point_path(int64_t p_from_id, int64_t p_to_id, bool Point *begin_point = a; Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || last_closest_point == nullptr) { return Vector(); @@ -499,7 +499,7 @@ Vector AStar3D::get_id_path(int64_t p_from_id, int64_t p_to_id, bool p_ Point *begin_point = a; Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || last_closest_point == nullptr) { return Vector(); @@ -728,7 +728,7 @@ Vector AStar2D::get_point_path(int64_t p_from_id, int64_t p_to_id, bool AStar3D::Point *begin_point = a; AStar3D::Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || astar.last_closest_point == nullptr) { return Vector(); @@ -782,7 +782,7 @@ Vector AStar2D::get_id_path(int64_t p_from_id, int64_t p_to_id, bool p_ AStar3D::Point *begin_point = a; AStar3D::Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || astar.last_closest_point == nullptr) { return Vector(); @@ -818,11 +818,11 @@ Vector AStar2D::get_id_path(int64_t p_from_id, int64_t p_to_id, bool p_ return path; } -bool AStar2D::_solve(AStar3D::Point *begin_point, AStar3D::Point *end_point) { +bool AStar2D::_solve(AStar3D::Point *begin_point, AStar3D::Point *end_point, bool p_allow_partial_path) { astar.last_closest_point = nullptr; astar.pass++; - if (!end_point->enabled) { + if (!end_point->enabled && !p_allow_partial_path) { return false; } diff --git a/core/math/a_star.h b/core/math/a_star.h index 03e9b0f4cdd..351c4357dee 100644 --- a/core/math/a_star.h +++ b/core/math/a_star.h @@ -117,7 +117,7 @@ class AStar3D : public RefCounted { HashSet segments; Point *last_closest_point = nullptr; - bool _solve(Point *begin_point, Point *end_point); + bool _solve(Point *begin_point, Point *end_point, bool p_allow_partial_path); protected: static void _bind_methods(); @@ -173,7 +173,7 @@ class AStar2D : public RefCounted { GDCLASS(AStar2D, RefCounted); AStar3D astar; - bool _solve(AStar3D::Point *begin_point, AStar3D::Point *end_point); + bool _solve(AStar3D::Point *begin_point, AStar3D::Point *end_point, bool p_allow_partial_path); protected: static void _bind_methods(); diff --git a/core/math/a_star_grid_2d.cpp b/core/math/a_star_grid_2d.cpp index 06f30614e7c..c7e14a63344 100644 --- a/core/math/a_star_grid_2d.cpp +++ b/core/math/a_star_grid_2d.cpp @@ -129,13 +129,18 @@ void AStarGrid2D::update() { } points.clear(); + solid_mask.clear(); const int32_t end_x = region.get_end().x; const int32_t end_y = region.get_end().y; const Vector2 half_cell_size = cell_size / 2; + for (int32_t x = region.position.x; x < end_x + 2; x++) { + solid_mask.push_back(true); + } for (int32_t y = region.position.y; y < end_y; y++) { LocalVector line; + solid_mask.push_back(true); for (int32_t x = region.position.x; x < end_x; x++) { Vector2 v = offset; switch (cell_shape) { @@ -152,10 +157,16 @@ void AStarGrid2D::update() { break; } line.push_back(Point(Vector2i(x, y), v)); + solid_mask.push_back(false); } + solid_mask.push_back(true); points.push_back(line); } + for (int32_t x = region.position.x; x < end_x + 2; x++) { + solid_mask.push_back(true); + } + dirty = false; } @@ -217,13 +228,13 @@ AStarGrid2D::Heuristic AStarGrid2D::get_default_estimate_heuristic() const { void AStarGrid2D::set_point_solid(const Vector2i &p_id, bool p_solid) { ERR_FAIL_COND_MSG(dirty, "Grid is not initialized. Call the update method."); ERR_FAIL_COND_MSG(!is_in_boundsv(p_id), vformat("Can't set if point is disabled. Point %s out of bounds %s.", p_id, region)); - _get_point_unchecked(p_id)->solid = p_solid; + _set_solid_unchecked(p_id, p_solid); } bool AStarGrid2D::is_point_solid(const Vector2i &p_id) const { ERR_FAIL_COND_V_MSG(dirty, false, "Grid is not initialized. Call the update method."); ERR_FAIL_COND_V_MSG(!is_in_boundsv(p_id), false, vformat("Can't get if point is disabled. Point %s out of bounds %s.", p_id, region)); - return _get_point_unchecked(p_id)->solid; + return _get_solid_unchecked(p_id); } void AStarGrid2D::set_point_weight_scale(const Vector2i &p_id, real_t p_weight_scale) { @@ -248,7 +259,7 @@ void AStarGrid2D::fill_solid_region(const Rect2i &p_region, bool p_solid) { for (int32_t y = safe_region.position.y; y < end_y; y++) { for (int32_t x = safe_region.position.x; x < end_x; x++) { - _get_point_unchecked(x, y)->solid = p_solid; + _set_solid_unchecked(x, y, p_solid); } } } @@ -269,13 +280,6 @@ void AStarGrid2D::fill_weight_scale_region(const Rect2i &p_region, real_t p_weig } AStarGrid2D::Point *AStarGrid2D::_jump(Point *p_from, Point *p_to) { - if (!p_to || p_to->solid) { - return nullptr; - } - if (p_to == end) { - return p_to; - } - int32_t from_x = p_from->id.x; int32_t from_y = p_from->id.y; @@ -286,72 +290,109 @@ AStarGrid2D::Point *AStarGrid2D::_jump(Point *p_from, Point *p_to) { int32_t dy = to_y - from_y; if (diagonal_mode == DIAGONAL_MODE_ALWAYS || diagonal_mode == DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE) { - if (dx != 0 && dy != 0) { - if ((_is_walkable(to_x - dx, to_y + dy) && !_is_walkable(to_x - dx, to_y)) || (_is_walkable(to_x + dx, to_y - dy) && !_is_walkable(to_x, to_y - dy))) { - return p_to; - } - if (_jump(p_to, _get_point(to_x + dx, to_y)) != nullptr) { - return p_to; + if (dx == 0 || dy == 0) { + return _forced_successor(to_x, to_y, dx, dy); + } + + while (_is_walkable(to_x, to_y) && (diagonal_mode == DIAGONAL_MODE_ALWAYS || _is_walkable(to_x, to_y - dy) || _is_walkable(to_x - dx, to_y))) { + if (end->id.x == to_x && end->id.y == to_y) { + return end; } - if (_jump(p_to, _get_point(to_x, to_y + dy)) != nullptr) { - return p_to; + + if ((_is_walkable(to_x - dx, to_y + dy) && !_is_walkable(to_x - dx, to_y)) || (_is_walkable(to_x + dx, to_y - dy) && !_is_walkable(to_x, to_y - dy))) { + return _get_point_unchecked(to_x, to_y); } - } else { - if (dx != 0) { - if ((_is_walkable(to_x + dx, to_y + 1) && !_is_walkable(to_x, to_y + 1)) || (_is_walkable(to_x + dx, to_y - 1) && !_is_walkable(to_x, to_y - 1))) { - return p_to; - } - } else { - if ((_is_walkable(to_x + 1, to_y + dy) && !_is_walkable(to_x + 1, to_y)) || (_is_walkable(to_x - 1, to_y + dy) && !_is_walkable(to_x - 1, to_y))) { - return p_to; - } + + if (_forced_successor(to_x + dx, to_y, dx, 0) != nullptr || _forced_successor(to_x, to_y + dy, 0, dy) != nullptr) { + return _get_point_unchecked(to_x, to_y); } + + to_x += dx; + to_y += dy; } - if (_is_walkable(to_x + dx, to_y + dy) && (diagonal_mode == DIAGONAL_MODE_ALWAYS || (_is_walkable(to_x + dx, to_y) || _is_walkable(to_x, to_y + dy)))) { - return _jump(p_to, _get_point(to_x + dx, to_y + dy)); - } + } else if (diagonal_mode == DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES) { - if (dx != 0 && dy != 0) { - if ((_is_walkable(to_x + dx, to_y + dy) && !_is_walkable(to_x, to_y + dy)) || !_is_walkable(to_x + dx, to_y)) { - return p_to; - } - if (_jump(p_to, _get_point(to_x + dx, to_y)) != nullptr) { - return p_to; + if (dx == 0 || dy == 0) { + return _forced_successor(from_x, from_y, dx, dy, true); + } + + while (_is_walkable(to_x, to_y) && _is_walkable(to_x, to_y - dy) && _is_walkable(to_x - dx, to_y)) { + if (end->id.x == to_x && end->id.y == to_y) { + return end; } - if (_jump(p_to, _get_point(to_x, to_y + dy)) != nullptr) { - return p_to; + + if ((_is_walkable(to_x + dx, to_y + dy) && !_is_walkable(to_x, to_y + dy)) || !_is_walkable(to_x + dx, to_y)) { + return _get_point_unchecked(to_x, to_y); } - } else { - if (dx != 0) { - if ((_is_walkable(to_x, to_y + 1) && !_is_walkable(to_x - dx, to_y + 1)) || (_is_walkable(to_x, to_y - 1) && !_is_walkable(to_x - dx, to_y - 1))) { - return p_to; - } - } else { - if ((_is_walkable(to_x + 1, to_y) && !_is_walkable(to_x + 1, to_y - dy)) || (_is_walkable(to_x - 1, to_y) && !_is_walkable(to_x - 1, to_y - dy))) { - return p_to; - } + + if (_forced_successor(to_x, to_y, dx, 0) != nullptr || _forced_successor(to_x, to_y, 0, dy) != nullptr) { + return _get_point_unchecked(to_x, to_y); } + + to_x += dx; + to_y += dy; } - if (_is_walkable(to_x + dx, to_y + dy) && _is_walkable(to_x + dx, to_y) && _is_walkable(to_x, to_y + dy)) { - return _jump(p_to, _get_point(to_x + dx, to_y + dy)); - } + } else { // DIAGONAL_MODE_NEVER - if (dx != 0) { - if ((_is_walkable(to_x, to_y - 1) && !_is_walkable(to_x - dx, to_y - 1)) || (_is_walkable(to_x, to_y + 1) && !_is_walkable(to_x - dx, to_y + 1))) { - return p_to; + if (dy == 0) { + return _forced_successor(from_x, from_y, dx, 0, true); + } + + while (_is_walkable(to_x, to_y)) { + if (end->id.x == to_x && end->id.y == to_y) { + return end; } - } else if (dy != 0) { + if ((_is_walkable(to_x - 1, to_y) && !_is_walkable(to_x - 1, to_y - dy)) || (_is_walkable(to_x + 1, to_y) && !_is_walkable(to_x + 1, to_y - dy))) { - return p_to; - } - if (_jump(p_to, _get_point(to_x + 1, to_y)) != nullptr) { - return p_to; + return _get_point_unchecked(to_x, to_y); } - if (_jump(p_to, _get_point(to_x - 1, to_y)) != nullptr) { - return p_to; + + if (_forced_successor(to_x, to_y, 1, 0, true) != nullptr || _forced_successor(to_x, to_y, -1, 0, true) != nullptr) { + return _get_point_unchecked(to_x, to_y); } + + to_y += dy; } - return _jump(p_to, _get_point(to_x + dx, to_y + dy)); + } + + return nullptr; +} + +AStarGrid2D::Point *AStarGrid2D::_forced_successor(int32_t p_x, int32_t p_y, int32_t p_dx, int32_t p_dy, bool p_inclusive) { + // Remembering previous results can improve performance. + bool l_prev = false, r_prev = false, l = false, r = false; + + int32_t o_x = p_x, o_y = p_y; + if (p_inclusive) { + o_x += p_dx; + o_y += p_dy; + } + + int32_t l_x = p_x - p_dy, l_y = p_y - p_dx; + int32_t r_x = p_x + p_dy, r_y = p_y + p_dx; + + while (_is_walkable(o_x, o_y)) { + if (end->id.x == o_x && end->id.y == o_y) { + return end; + } + + l_prev = l || _is_walkable(l_x, l_y); + r_prev = r || _is_walkable(r_x, r_y); + + l_x += p_dx; + l_y += p_dy; + r_x += p_dx; + r_y += p_dy; + + l = _is_walkable(l_x, l_y); + r = _is_walkable(r_x, r_y); + + if ((l && !l_prev) || (r && !r_prev)) { + return _get_point_unchecked(o_x, o_y); + } + + o_x += p_dx; + o_y += p_dy; } return nullptr; } @@ -404,19 +445,19 @@ void AStarGrid2D::_get_nbors(Point *p_point, LocalVector &r_nbors) { } } - if (top && !top->solid) { + if (top && !_get_solid_unchecked(top->id)) { r_nbors.push_back(top); ts0 = true; } - if (right && !right->solid) { + if (right && !_get_solid_unchecked(right->id)) { r_nbors.push_back(right); ts1 = true; } - if (bottom && !bottom->solid) { + if (bottom && !_get_solid_unchecked(bottom->id)) { r_nbors.push_back(bottom); ts2 = true; } - if (left && !left->solid) { + if (left && !_get_solid_unchecked(left->id)) { r_nbors.push_back(left); ts3 = true; } @@ -446,25 +487,25 @@ void AStarGrid2D::_get_nbors(Point *p_point, LocalVector &r_nbors) { break; } - if (td0 && (top_left && !top_left->solid)) { + if (td0 && (top_left && !_get_solid_unchecked(top_left->id))) { r_nbors.push_back(top_left); } - if (td1 && (top_right && !top_right->solid)) { + if (td1 && (top_right && !_get_solid_unchecked(top_right->id))) { r_nbors.push_back(top_right); } - if (td2 && (bottom_right && !bottom_right->solid)) { + if (td2 && (bottom_right && !_get_solid_unchecked(bottom_right->id))) { r_nbors.push_back(bottom_right); } - if (td3 && (bottom_left && !bottom_left->solid)) { + if (td3 && (bottom_left && !_get_solid_unchecked(bottom_left->id))) { r_nbors.push_back(bottom_left); } } -bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point) { +bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point, bool p_allow_partial_path) { last_closest_point = nullptr; pass++; - if (p_end_point->solid) { + if (_get_solid_unchecked(p_end_point->id) && !p_allow_partial_path) { return false; } @@ -519,7 +560,7 @@ bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point) { continue; } } else { - if (e->solid || e->closed_pass == pass) { + if (_get_solid_unchecked(e->id) || e->closed_pass == pass) { continue; } weight_scale = e->weight_scale; @@ -581,6 +622,33 @@ Vector2 AStarGrid2D::get_point_position(const Vector2i &p_id) const { return _get_point_unchecked(p_id)->pos; } +TypedArray AStarGrid2D::get_point_data_in_region(const Rect2i &p_region) const { + ERR_FAIL_COND_V_MSG(dirty, TypedArray(), "Grid is not initialized. Call the update method."); + const Rect2i inter_region = region.intersection(p_region); + + const int32_t start_x = inter_region.position.x - region.position.x; + const int32_t start_y = inter_region.position.y - region.position.y; + const int32_t end_x = inter_region.get_end().x - region.position.x; + const int32_t end_y = inter_region.get_end().y - region.position.y; + + TypedArray data; + + for (int32_t y = start_y; y < end_y; y++) { + for (int32_t x = start_x; x < end_x; x++) { + const Point &p = points[y][x]; + + Dictionary dict; + dict["id"] = p.id; + dict["position"] = p.pos; + dict["solid"] = _get_solid_unchecked(p.id); + dict["weight_scale"] = p.weight_scale; + data.push_back(dict); + } + } + + return data; +} + Vector AStarGrid2D::get_point_path(const Vector2i &p_from_id, const Vector2i &p_to_id, bool p_allow_partial_path) { ERR_FAIL_COND_V_MSG(dirty, Vector(), "Grid is not initialized. Call the update method."); ERR_FAIL_COND_V_MSG(!is_in_boundsv(p_from_id), Vector(), vformat("Can't get id path. Point %s out of bounds %s.", p_from_id, region)); @@ -598,7 +666,7 @@ Vector AStarGrid2D::get_point_path(const Vector2i &p_from_id, const Vec Point *begin_point = a; Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || last_closest_point == nullptr) { return Vector(); @@ -651,7 +719,7 @@ TypedArray AStarGrid2D::get_id_path(const Vector2i &p_from_id, const V Point *begin_point = a; Point *end_point = b; - bool found_route = _solve(begin_point, end_point); + bool found_route = _solve(begin_point, end_point, p_allow_partial_path); if (!found_route) { if (!p_allow_partial_path || last_closest_point == nullptr) { return TypedArray(); @@ -719,6 +787,7 @@ void AStarGrid2D::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &AStarGrid2D::clear); ClassDB::bind_method(D_METHOD("get_point_position", "id"), &AStarGrid2D::get_point_position); + ClassDB::bind_method(D_METHOD("get_point_data_in_region", "region"), &AStarGrid2D::get_point_data_in_region); ClassDB::bind_method(D_METHOD("get_point_path", "from_id", "to_id", "allow_partial_path"), &AStarGrid2D::get_point_path, DEFVAL(false)); ClassDB::bind_method(D_METHOD("get_id_path", "from_id", "to_id", "allow_partial_path"), &AStarGrid2D::get_id_path, DEFVAL(false)); diff --git a/core/math/a_star_grid_2d.h b/core/math/a_star_grid_2d.h index 1650d45de5f..08cb0bc6fb6 100644 --- a/core/math/a_star_grid_2d.h +++ b/core/math/a_star_grid_2d.h @@ -81,7 +81,6 @@ class AStarGrid2D : public RefCounted { struct Point { Vector2i id; - bool solid = false; Vector2 pos; real_t weight_scale = 1.0; @@ -114,6 +113,7 @@ class AStarGrid2D : public RefCounted { } }; + LocalVector solid_mask; LocalVector> points; Point *end = nullptr; Point *last_closest_point = nullptr; @@ -121,11 +121,12 @@ class AStarGrid2D : public RefCounted { uint64_t pass = 1; private: // Internal routines. + _FORCE_INLINE_ size_t _to_mask_index(int32_t p_x, int32_t p_y) const { + return ((p_y - region.position.y + 1) * (region.size.x + 2)) + p_x - region.position.x + 1; + } + _FORCE_INLINE_ bool _is_walkable(int32_t p_x, int32_t p_y) const { - if (region.has_point(Vector2i(p_x, p_y))) { - return !points[p_y - region.position.y][p_x - region.position.x].solid; - } - return false; + return !solid_mask[_to_mask_index(p_x, p_y)]; } _FORCE_INLINE_ Point *_get_point(int32_t p_x, int32_t p_y) { @@ -135,6 +136,18 @@ class AStarGrid2D : public RefCounted { return nullptr; } + _FORCE_INLINE_ void _set_solid_unchecked(int32_t p_x, int32_t p_y, bool p_solid) { + solid_mask[_to_mask_index(p_x, p_y)] = p_solid; + } + + _FORCE_INLINE_ void _set_solid_unchecked(const Vector2i &p_id, bool p_solid) { + solid_mask[_to_mask_index(p_id.x, p_id.y)] = p_solid; + } + + _FORCE_INLINE_ bool _get_solid_unchecked(const Vector2i &p_id) const { + return solid_mask[_to_mask_index(p_id.x, p_id.y)]; + } + _FORCE_INLINE_ Point *_get_point_unchecked(int32_t p_x, int32_t p_y) { return &points[p_y - region.position.y][p_x - region.position.x]; } @@ -149,7 +162,8 @@ class AStarGrid2D : public RefCounted { void _get_nbors(Point *p_point, LocalVector &r_nbors); Point *_jump(Point *p_from, Point *p_to); - bool _solve(Point *p_begin_point, Point *p_end_point); + bool _solve(Point *p_begin_point, Point *p_end_point, bool p_allow_partial_path); + Point *_forced_successor(int32_t p_x, int32_t p_y, int32_t p_dx, int32_t p_dy, bool p_inclusive = false); protected: static void _bind_methods(); @@ -215,6 +229,7 @@ class AStarGrid2D : public RefCounted { void clear(); Vector2 get_point_position(const Vector2i &p_id) const; + TypedArray get_point_data_in_region(const Rect2i &p_region) const; Vector get_point_path(const Vector2i &p_from, const Vector2i &p_to, bool p_allow_partial_path = false); TypedArray get_id_path(const Vector2i &p_from, const Vector2i &p_to, bool p_allow_partial_path = false); }; diff --git a/doc/classes/AStar2D.xml b/doc/classes/AStar2D.xml index be29d36d05e..a97c3486c24 100644 --- a/doc/classes/AStar2D.xml +++ b/doc/classes/AStar2D.xml @@ -143,6 +143,7 @@ Returns an array with the IDs of the points that form the path found by AStar2D between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. + [b]Note:[/b] When [param allow_partial_path] is [code]true[/code] and [param to_id] is disabled the search may take an unusually long time to finish. [codeblocks] [gdscript] var astar = AStar2D.new() @@ -235,6 +236,7 @@ Returns an array with the points that are in the path found by AStar2D between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. [b]Note:[/b] This method is not thread-safe. If called from a [Thread], it will return an empty array and will print an error message. + Additionally, when [param allow_partial_path] is [code]true[/code] and [param to_id] is disabled the search may take an unusually long time to finish. diff --git a/doc/classes/AStar3D.xml b/doc/classes/AStar3D.xml index 98839db0abf..576e28e4611 100644 --- a/doc/classes/AStar3D.xml +++ b/doc/classes/AStar3D.xml @@ -185,6 +185,7 @@ Returns an array with the IDs of the points that form the path found by AStar3D between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. + [b]Note:[/b] When [param allow_partial_path] is [code]true[/code] and [param to_id] is disabled the search may take an unusually long time to finish. [codeblocks] [gdscript] var astar = AStar3D.new() @@ -275,6 +276,7 @@ Returns an array with the points that are in the path found by AStar3D between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. [b]Note:[/b] This method is not thread-safe. If called from a [Thread], it will return an empty array and will print an error message. + Additionally, when [param allow_partial_path] is [code]true[/code] and [param to_id] is disabled the search may take an unusually long time to finish. diff --git a/doc/classes/AStarGrid2D.xml b/doc/classes/AStarGrid2D.xml index b15bc8269ca..56b4b56c903 100644 --- a/doc/classes/AStarGrid2D.xml +++ b/doc/classes/AStarGrid2D.xml @@ -79,6 +79,14 @@ Returns an array with the IDs of the points that form the path found by AStar2D between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. + [b]Note:[/b] When [param allow_partial_path] is [code]true[/code] and [param to_id] is solid the search may take an unusually long time to finish. + + + + + + + Returns an array of dictionaries with point data ([code]id[/code]: [Vector2i], [code]position[/code]: [Vector2], [code]solid[/code]: [bool], [code]weight_scale[/code]: [float]) within a [param region]. @@ -90,6 +98,7 @@ Returns an array with the points that are in the path found by [AStarGrid2D] between the given points. The array is ordered from the starting point to the ending point of the path. If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. [b]Note:[/b] This method is not thread-safe. If called from a [Thread], it will return an empty array and will print an error message. + Additionally, when [param allow_partial_path] is [code]true[/code] and [param to_id] is solid the search may take an unusually long time to finish. diff --git a/doc/classes/JavaClass.xml b/doc/classes/JavaClass.xml index ecfcaa87815..9a6c30df10e 100644 --- a/doc/classes/JavaClass.xml +++ b/doc/classes/JavaClass.xml @@ -1,13 +1,33 @@ - Represents an object from the Java Native Interface. + Represents a class from the Java Native Interface. - Represents an object from the Java Native Interface. It is returned from [method JavaClassWrapper.wrap]. - [b]Note:[/b] This class only works on Android. For any other build, this class does nothing. + Represents a class from the Java Native Interface. It is returned from [method JavaClassWrapper.wrap]. + [b]Note:[/b] This class only works on Android. On any other platform, this class does nothing. [b]Note:[/b] This class is not to be confused with [JavaScriptObject]. + + + + + Returns the Java class name. + + + + + + Returns the object's Java methods and their signatures as an [Array] of dictionaries, in the same format as [method Object.get_method_list]. + + + + + + Returns a [JavaClass] representing the Java parent class of this class. + + + diff --git a/doc/classes/JavaClassWrapper.xml b/doc/classes/JavaClassWrapper.xml index 4b411eefa8c..1f1eb879918 100644 --- a/doc/classes/JavaClassWrapper.xml +++ b/doc/classes/JavaClassWrapper.xml @@ -6,6 +6,15 @@ The JavaClassWrapper singleton provides a way for the Redot application to send and receive data through the [url=https://developer.android.com/training/articles/perf-jni]Java Native Interface[/url] (JNI). [b]Note:[/b] This singleton is only available in Android builds. + [codeblock] + var LocalDateTime = JavaClassWrapper.wrap("java.time.LocalDateTime") + var DateTimeFormatter = JavaClassWrapper.wrap("java.time.format.DateTimeFormatter") + + var datetime = LocalDateTime.now() + var formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss") + + print(datetime.format(formatter)) + [/codeblock] diff --git a/doc/classes/JavaObject.xml b/doc/classes/JavaObject.xml new file mode 100644 index 00000000000..f38070e7d9d --- /dev/null +++ b/doc/classes/JavaObject.xml @@ -0,0 +1,21 @@ + + + + Represents an object from the Java Native Interface. + + + Represents an object from the Java Native Interface. It can be returned from Java methods called on [JavaClass] or other [JavaObject]s. See [JavaClassWrapper] for an example. + [b]Note:[/b] This class only works on Android. On any other platform, this class does nothing. + [b]Note:[/b] This class is not to be confused with [JavaScriptObject]. + + + + + + + + Returns the [JavaClass] that this object is an instance of. + + + + diff --git a/drivers/SCsub b/drivers/SCsub index e77b96cc87d..25011206a42 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -18,7 +18,9 @@ if env["platform"] == "windows": SConscript("backtrace/SCsub") if env["xaudio2"]: SConscript("xaudio2/SCsub") - +# Shared Apple platform drivers +if env["platform"] in ["macos", "ios"]: + SConscript("apple/SCsub") # Midi drivers SConscript("alsamidi/SCsub") SConscript("coremidi/SCsub") diff --git a/drivers/apple/SCsub b/drivers/apple/SCsub new file mode 100644 index 00000000000..e39fa22a59a --- /dev/null +++ b/drivers/apple/SCsub @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +Import("env") + +# Driver source files +env.add_source_files(env.drivers_sources, "*.mm") diff --git a/platform/ios/joypad_ios.h b/drivers/apple/joypad_apple.h similarity index 66% rename from platform/ios/joypad_ios.h rename to drivers/apple/joypad_apple.h index 8bd4bd78f47..53c9cc9b5bd 100644 --- a/platform/ios/joypad_ios.h +++ b/drivers/apple/joypad_apple.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* joypad_ios.h */ +/* joypad_apple.h */ /**************************************************************************/ /* This file is part of: */ /* REDOT ENGINE */ @@ -30,23 +30,44 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ +#include "core/input/input.h" + +#define Key _QKey #import +#undef Key -@interface JoypadIOSObserver : NSObject +@class GCController; +class RumbleContext; -- (void)startObserving; -- (void)startProcessing; -- (void)finishObserving; +struct GameController { + int joy_id; + GCController *controller; + RumbleContext *rumble_context API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) = nil; + NSInteger ff_effect_timestamp = 0; + bool force_feedback = false; -@end + GameController(int p_joy_id, GCController *p_controller); + ~GameController(); +}; -class JoypadIOS { +class JoypadApple { private: - JoypadIOSObserver *observer; + id connect_observer = nil; + id disconnect_observer = nil; + HashMap joypads; + HashMap controller_to_joy_id; + + GCControllerPlayerIndex get_free_player_index(); + + void add_joypad(GCController *p_controller); + void remove_joypad(GCController *p_controller); public: - JoypadIOS(); - ~JoypadIOS(); + JoypadApple(); + ~JoypadApple(); + + void joypad_vibration_start(GameController &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)); + void joypad_vibration_stop(GameController &p_joypad, uint64_t p_timestamp) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)); - void start_processing(); + void process_joypads(); }; diff --git a/drivers/apple/joypad_apple.mm b/drivers/apple/joypad_apple.mm new file mode 100644 index 00000000000..47edb1ed6ff --- /dev/null +++ b/drivers/apple/joypad_apple.mm @@ -0,0 +1,429 @@ +/**************************************************************************/ +/* joypad_apple.mm */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#import "joypad_apple.h" + +#include +#import + +#include "core/config/project_settings.h" +#include "main/main.h" + +class API_AVAILABLE(macos(11), ios(14.0), tvos(14.0)) RumbleMotor { + CHHapticEngine *engine; + id player; + bool is_started; + + RumbleMotor(GCController *p_controller, GCHapticsLocality p_locality) { + engine = [p_controller.haptics createEngineWithLocality:p_locality]; + engine.autoShutdownEnabled = YES; + } + +public: + static RumbleMotor *create(GCController *p_controller, GCHapticsLocality p_locality) { + if ([p_controller.haptics.supportedLocalities containsObject:p_locality]) { + return memnew(RumbleMotor(p_controller, p_locality)); + } + return nullptr; + } + + _ALWAYS_INLINE_ bool has_active_player() { + return player != nil; + } + + void execute_pattern(CHHapticPattern *p_pattern) { + NSError *error; + if (!is_started) { + ERR_FAIL_COND_MSG(![engine startAndReturnError:&error], "Couldn't start controller haptic engine: " + String::utf8(error.localizedDescription.UTF8String)); + is_started = YES; + } + + player = [engine createPlayerWithPattern:p_pattern error:&error]; + ERR_FAIL_COND_MSG(error, "Couldn't create controller haptic pattern player: " + String::utf8(error.localizedDescription.UTF8String)); + ERR_FAIL_COND_MSG(![player startAtTime:CHHapticTimeImmediate error:&error], "Couldn't execute controller haptic pattern: " + String::utf8(error.localizedDescription.UTF8String)); + } + + void stop() { + id old_player = player; + player = nil; + + NSError *error; + ERR_FAIL_COND_MSG(![old_player stopAtTime:CHHapticTimeImmediate error:&error], "Couldn't stop controller haptic pattern: " + String::utf8(error.localizedDescription.UTF8String)); + } +}; + +class API_AVAILABLE(macos(11), ios(14.0), tvos(14.0)) RumbleContext { + RumbleMotor *weak_motor; + RumbleMotor *strong_motor; + +public: + RumbleContext(GCController *p_controller) { + weak_motor = RumbleMotor::create(p_controller, GCHapticsLocalityRightHandle); + strong_motor = RumbleMotor::create(p_controller, GCHapticsLocalityLeftHandle); + } + + ~RumbleContext() { + if (weak_motor) { + memdelete(weak_motor); + } + if (strong_motor) { + memdelete(strong_motor); + } + } + + _ALWAYS_INLINE_ bool has_motors() { + return weak_motor != nullptr && strong_motor != nullptr; + } + + _ALWAYS_INLINE_ bool has_active_players() { + if (!has_motors()) { + return false; + } + return (weak_motor && weak_motor->has_active_player()) || (strong_motor && strong_motor->has_active_player()); + } + + void stop() { + if (weak_motor) { + weak_motor->stop(); + } + if (strong_motor) { + strong_motor->stop(); + } + } + + void play_weak_pattern(CHHapticPattern *p_pattern) { + if (weak_motor) { + weak_motor->execute_pattern(p_pattern); + } + } + + void play_strong_pattern(CHHapticPattern *p_pattern) { + if (strong_motor) { + strong_motor->execute_pattern(p_pattern); + } + } +}; + +GameController::GameController(int p_joy_id, GCController *p_controller) : + joy_id(p_joy_id), controller(p_controller) { + force_feedback = NO; + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + if (controller.haptics != nil) { + // Create a rumble context for the controller. + rumble_context = memnew(RumbleContext(p_controller)); + + // If the rumble motors aren't available, disable force feedback. + force_feedback = rumble_context->has_motors(); + } + } + + int l_joy_id = joy_id; + + auto BUTTON = [l_joy_id](JoyButton p_button) { + return ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_button(l_joy_id, p_button, pressed); + }; + }; + + if (controller.extendedGamepad != nil) { + GCExtendedGamepad *gamepad = controller.extendedGamepad; + + gamepad.buttonA.pressedChangedHandler = BUTTON(JoyButton::A); + gamepad.buttonB.pressedChangedHandler = BUTTON(JoyButton::B); + gamepad.buttonX.pressedChangedHandler = BUTTON(JoyButton::X); + gamepad.buttonY.pressedChangedHandler = BUTTON(JoyButton::Y); + gamepad.leftShoulder.pressedChangedHandler = BUTTON(JoyButton::LEFT_SHOULDER); + gamepad.rightShoulder.pressedChangedHandler = BUTTON(JoyButton::RIGHT_SHOULDER); + gamepad.dpad.up.pressedChangedHandler = BUTTON(JoyButton::DPAD_UP); + gamepad.dpad.down.pressedChangedHandler = BUTTON(JoyButton::DPAD_DOWN); + gamepad.dpad.left.pressedChangedHandler = BUTTON(JoyButton::DPAD_LEFT); + gamepad.dpad.right.pressedChangedHandler = BUTTON(JoyButton::DPAD_RIGHT); + + gamepad.leftThumbstick.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::LEFT_X, xValue); + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::LEFT_Y, -yValue); + }; + + gamepad.rightThumbstick.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::RIGHT_X, xValue); + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::RIGHT_Y, -yValue); + }; + gamepad.leftTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::TRIGGER_LEFT, value); + }; + gamepad.rightTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::TRIGGER_RIGHT, value); + }; + + if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) { + gamepad.leftThumbstickButton.pressedChangedHandler = BUTTON(JoyButton::LEFT_STICK); + gamepad.rightThumbstickButton.pressedChangedHandler = BUTTON(JoyButton::RIGHT_STICK); + } + + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + gamepad.buttonOptions.pressedChangedHandler = BUTTON(JoyButton::BACK); + gamepad.buttonMenu.pressedChangedHandler = BUTTON(JoyButton::START); + } + + if (@available(macOS 11, iOS 14.0, tvOS 14.0, *)) { + gamepad.buttonHome.pressedChangedHandler = BUTTON(JoyButton::GUIDE); + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + xboxGamepad.paddleButton1.pressedChangedHandler = BUTTON(JoyButton::PADDLE1); + xboxGamepad.paddleButton2.pressedChangedHandler = BUTTON(JoyButton::PADDLE2); + xboxGamepad.paddleButton3.pressedChangedHandler = BUTTON(JoyButton::PADDLE3); + xboxGamepad.paddleButton4.pressedChangedHandler = BUTTON(JoyButton::PADDLE4); + } + } + + if (@available(macOS 12, iOS 15.0, tvOS 15.0, *)) { + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + xboxGamepad.buttonShare.pressedChangedHandler = BUTTON(JoyButton::MISC1); + } + } + } else if (controller.microGamepad != nil) { + GCMicroGamepad *gamepad = controller.microGamepad; + + gamepad.buttonA.pressedChangedHandler = BUTTON(JoyButton::A); + gamepad.buttonX.pressedChangedHandler = BUTTON(JoyButton::X); + gamepad.dpad.up.pressedChangedHandler = BUTTON(JoyButton::DPAD_UP); + gamepad.dpad.down.pressedChangedHandler = BUTTON(JoyButton::DPAD_DOWN); + gamepad.dpad.left.pressedChangedHandler = BUTTON(JoyButton::DPAD_LEFT); + gamepad.dpad.right.pressedChangedHandler = BUTTON(JoyButton::DPAD_RIGHT); + } + + // TODO: Need to add support for controller.motion which gives us access to + // the orientation of the device (if supported). +} + +GameController::~GameController() { + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + if (rumble_context) { + memdelete(rumble_context); + } + } +} + +JoypadApple::JoypadApple() { + connect_observer = [NSNotificationCenter.defaultCenter + addObserverForName:GCControllerDidConnectNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + GCController *controller = notification.object; + if (!controller) { + return; + } + add_joypad(controller); + }]; + + disconnect_observer = [NSNotificationCenter.defaultCenter + addObserverForName:GCControllerDidDisconnectNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + GCController *controller = notification.object; + if (!controller) { + return; + } + remove_joypad(controller); + }]; + + if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) { + GCController.shouldMonitorBackgroundEvents = YES; + } +} + +JoypadApple::~JoypadApple() { + for (KeyValue &E : joypads) { + memdelete(E.value); + E.value = nullptr; + } + + [NSNotificationCenter.defaultCenter removeObserver:connect_observer]; + [NSNotificationCenter.defaultCenter removeObserver:disconnect_observer]; +} + +// Finds the rightmost set bit in a number, n. +// variation of https://www.geeksforgeeks.org/position-of-rightmost-set-bit/ +int rightmost_one(int n) { + return __builtin_ctz(n & -n) + 1; +} + +GCControllerPlayerIndex JoypadApple::get_free_player_index() { + // player_set will be a bitfield where each bit represents a player index. + __block uint32_t player_set = 0; + for (const KeyValue &E : controller_to_joy_id) { + player_set |= 1U << E.key.playerIndex; + } + + // invert, as we want to find the first unset player index. + int n = rightmost_one((int)(~player_set)); + if (n >= 5) { + return GCControllerPlayerIndexUnset; + } + + return (GCControllerPlayerIndex)(n - 1); +} + +void JoypadApple::add_joypad(GCController *p_controller) { + if (controller_to_joy_id.has(p_controller)) { + return; + } + + // Get a new id for our controller. + int joy_id = Input::get_singleton()->get_unused_joy_id(); + + if (joy_id == -1) { + print_verbose("Couldn't retrieve new joy ID."); + return; + } + + // Assign our player index. + if (p_controller.playerIndex == GCControllerPlayerIndexUnset) { + p_controller.playerIndex = get_free_player_index(); + } + + // Tell Godot about our new controller. + Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8(p_controller.vendorName.UTF8String)); + + // Assign our player index. + joypads.insert(joy_id, memnew(GameController(joy_id, p_controller))); + controller_to_joy_id.insert(p_controller, joy_id); +} + +void JoypadApple::remove_joypad(GCController *p_controller) { + if (!controller_to_joy_id.has(p_controller)) { + return; + } + + int joy_id = controller_to_joy_id[p_controller]; + controller_to_joy_id.erase(p_controller); + + // Tell Godot this joystick is no longer there. + Input::get_singleton()->joy_connection_changed(joy_id, false, ""); + + // And remove it from our dictionary. + GameController **old = joypads.getptr(joy_id); + memdelete(*old); + *old = nullptr; + joypads.erase(joy_id); +} + +API_AVAILABLE(macos(10.15), ios(13.0), tvos(14.0)) +CHHapticPattern *get_vibration_pattern(float p_magnitude, float p_duration) { + // Creates a vibration pattern with an intensity and duration. + NSDictionary *hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{ + CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration], + + CHHapticPatternKeyEventParameters : @[ + @{ + CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity, + CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_magnitude] + }, + ], + }, + }, + ], + }; + NSError *error; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; + return pattern; +} + +void JoypadApple::joypad_vibration_start(GameController &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { + if (!p_joypad.force_feedback || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { + return; + } + + // If there is active vibration players, stop them. + if (p_joypad.rumble_context->has_active_players()) { + joypad_vibration_stop(p_joypad, p_timestamp); + } + + // Gets the default vibration pattern and creates a player for each motor. + CHHapticPattern *weak_pattern = get_vibration_pattern(p_weak_magnitude, p_duration); + CHHapticPattern *strong_pattern = get_vibration_pattern(p_strong_magnitude, p_duration); + + p_joypad.rumble_context->play_weak_pattern(weak_pattern); + p_joypad.rumble_context->play_strong_pattern(strong_pattern); + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +void JoypadApple::joypad_vibration_stop(GameController &p_joypad, uint64_t p_timestamp) { + if (!p_joypad.force_feedback) { + return; + } + // If there is no active vibration players, exit. + if (!p_joypad.rumble_context->has_active_players()) { + return; + } + + p_joypad.rumble_context->stop(); + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +void JoypadApple::process_joypads() { + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + for (KeyValue &E : joypads) { + int id = E.key; + GameController &joypad = *E.value; + + if (joypad.force_feedback) { + Input *input = Input::get_singleton(); + uint64_t timestamp = input->get_joy_vibration_timestamp(id); + + if (timestamp > (unsigned)joypad.ff_effect_timestamp) { + Vector2 strength = input->get_joy_vibration_strength(id); + float duration = input->get_joy_vibration_duration(id); + if (duration == 0) { + duration = GCHapticDurationInfinite; + } + + if (strength.x == 0 && strength.y == 0) { + joypad_vibration_stop(joypad, timestamp); + } else { + joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); + } + } + } + } + } +} diff --git a/editor/import/resource_importer_layered_texture.cpp b/editor/import/resource_importer_layered_texture.cpp index a510951d297..9edff32e54d 100644 --- a/editor/import/resource_importer_layered_texture.cpp +++ b/editor/import/resource_importer_layered_texture.cpp @@ -278,6 +278,10 @@ void ResourceImporterLayeredTexture::_save_tex(Vector> p_images, cons f->store_32(0); f->store_32(0); + if ((p_compress_mode == COMPRESS_LOSSLESS || p_compress_mode == COMPRESS_LOSSY) && p_images[0]->get_format() >= Image::FORMAT_RF) { + p_compress_mode = COMPRESS_VRAM_UNCOMPRESSED; // These can't go as lossy. + } + for (int i = 0; i < p_images.size(); i++) { ResourceImporterTexture::save_to_ctex_format(f, p_images[i], ResourceImporterTexture::CompressMode(p_compress_mode), used_channels, p_vram_compression, p_lossy); } diff --git a/editor/import/resource_importer_texture.cpp b/editor/import/resource_importer_texture.cpp index 8a6952c100d..37cf8d5f98f 100644 --- a/editor/import/resource_importer_texture.cpp +++ b/editor/import/resource_importer_texture.cpp @@ -382,7 +382,7 @@ void ResourceImporterTexture::_save_ctex(const Ref &p_image, const String f->store_32(0); f->store_32(0); - if ((p_compress_mode == COMPRESS_LOSSLESS || p_compress_mode == COMPRESS_LOSSY) && p_image->get_format() > Image::FORMAT_RGBA8) { + if ((p_compress_mode == COMPRESS_LOSSLESS || p_compress_mode == COMPRESS_LOSSY) && p_image->get_format() >= Image::FORMAT_RF) { p_compress_mode = COMPRESS_VRAM_UNCOMPRESSED; //these can't go as lossy } diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html index 4e1d4f3277e..66d60a4a43d 100644 --- a/misc/dist/html/full-size.html +++ b/misc/dist/html/full-size.html @@ -164,10 +164,11 @@ new Promise((resolve) => { setTimeout(() => resolve(), 2000); }), - ]).catch((err) => { - console.error('Error while registering service worker:', err); - }).then(() => { + ]).then(() => { + // Reload if there was no error. window.location.reload(); + }).catch((err) => { + console.error('Error while registering service worker:', err); }); } else { // Display the message as usual diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 7465fbbb4b1..e43f941d3a8 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -603,12 +603,8 @@ Error GLTFDocument::_parse_nodes(Ref p_state) { if (n.has("scale")) { node->set_scale(_arr_to_vec3(n["scale"])); } - - Transform3D godot_rest_transform; - godot_rest_transform.basis.set_quaternion_scale(node->transform.basis.get_rotation_quaternion(), node->transform.basis.get_scale()); - godot_rest_transform.origin = node->transform.origin; - node->set_additional_data("GODOT_rest_transform", godot_rest_transform); } + node->set_additional_data("GODOT_rest_transform", node->transform); if (n.has("extensions")) { Dictionary extensions = n["extensions"]; @@ -3035,6 +3031,7 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { } } array[Mesh::ARRAY_WEIGHTS] = weights; + flags |= Mesh::ARRAY_FLAG_USE_8_BONE_WEIGHTS; } if (!indices.is_empty()) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index 86e069c5bbe..d1626a73a78 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -422,25 +422,10 @@ MarshalType marshalType if (exportAttr != null && propertySymbol != null) { - if (propertySymbol.GetMethod == null) + if (propertySymbol.GetMethod == null || propertySymbol.SetMethod == null || propertySymbol.SetMethod.IsInitOnly) { - // This should never happen, as we filtered WriteOnly properties, but just in case. - context.ReportDiagnostic(Diagnostic.Create( - Common.ExportedPropertyIsWriteOnlyRule, - propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(), - propertySymbol.ToDisplayString() - )); - return null; - } - - if (propertySymbol.SetMethod == null || propertySymbol.SetMethod.IsInitOnly) - { - // This should never happen, as we filtered ReadOnly properties, but just in case. - context.ReportDiagnostic(Diagnostic.Create( - Common.ExportedMemberIsReadOnlyRule, - propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(), - propertySymbol.ToDisplayString() - )); + // Exports can be neither read-only nor write-only but the diagnostic errors for properties are already + // reported by ScriptPropertyDefValGenerator.cs so just quit early here. return null; } } diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj index f23f2b9a8cf..208e6d8f411 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -5,7 +5,6 @@ net6.0-windows 10 enable - win-x86 False LatestMajor diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index d3899c809a2..e84b4e92c72 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -259,11 +259,12 @@ public Error OpenInExternalEditor(Script script, int line, int col) var args = new List { + Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.dll"), GodotSharpDirs.ProjectSlnPath, line >= 0 ? $"{scriptPath};{line + 1};{col + 1}" : scriptPath }; - string command = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.exe"); + string command = DotNetFinder.FindDotNetExe() ?? "dotnet"; try { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 901700067df..94417665b3a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -505,16 +505,29 @@ internal static unsafe godot_bool AddScriptBridge(IntPtr scriptPtr, godot_string private static unsafe bool AddScriptBridgeCore(IntPtr scriptPtr, string scriptPath) { - lock (_scriptTypeBiMap.ReadWriteLock) + _scriptTypeBiMap.ReadWriteLock.EnterUpgradeableReadLock(); + try { if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) { if (!_pathTypeBiMap.TryGetScriptType(scriptPath, out Type? scriptType)) return false; - _scriptTypeBiMap.Add(scriptPtr, scriptType); + _scriptTypeBiMap.ReadWriteLock.EnterWriteLock(); + try + { + _scriptTypeBiMap.Add(scriptPtr, scriptType); + } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitWriteLock(); + } } } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitUpgradeableReadLock(); + } return true; } @@ -537,7 +550,8 @@ internal static unsafe void GetOrCreateScriptBridgeForPath(godot_string* scriptP private static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) { - lock (_scriptTypeBiMap.ReadWriteLock) + _scriptTypeBiMap.ReadWriteLock.EnterUpgradeableReadLock(); + try { if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) { @@ -549,6 +563,10 @@ private static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot // This path is slower, but it's only executed for the first instantiation of the type CreateScriptBridgeForType(scriptType, outScript); } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitUpgradeableReadLock(); + } } internal static unsafe void GetOrLoadOrCreateScriptForType(Type scriptType, godot_ref* outScript) @@ -556,7 +574,8 @@ internal static unsafe void GetOrLoadOrCreateScriptForType(Type scriptType, godo static bool GetPathOtherwiseGetOrCreateScript(Type scriptType, godot_ref* outScript, [MaybeNullWhen(false)] out string scriptPath) { - lock (_scriptTypeBiMap.ReadWriteLock) + _scriptTypeBiMap.ReadWriteLock.EnterUpgradeableReadLock(); + try { if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) { @@ -584,6 +603,10 @@ static bool GetPathOtherwiseGetOrCreateScript(Type scriptType, godot_ref* outScr scriptPath = null; return false; } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitUpgradeableReadLock(); + } } static string GetVirtualConstructedGenericTypeScriptPath(Type scriptType, string scriptPath) @@ -613,7 +636,16 @@ static string GetVirtualConstructedGenericTypeScriptPath(Type scriptType, string // IMPORTANT: The virtual path must be added to _pathTypeBiMap before the first // load of the script, otherwise the loaded script won't be added to _scriptTypeBiMap. scriptPath = GetVirtualConstructedGenericTypeScriptPath(scriptType, scriptPath); - _pathTypeBiMap.Add(scriptPath, scriptType); + + _scriptTypeBiMap.ReadWriteLock.EnterWriteLock(); + try + { + _pathTypeBiMap.Add(scriptPath, scriptType); + } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitWriteLock(); + } } // This must be done outside the read-write lock, as the script resource loading can lock it @@ -643,89 +675,108 @@ private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* { Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}."); - NativeFuncs.godotsharp_internal_new_csharp_script(outScript); - IntPtr scriptPtr = outScript->Reference; + _scriptTypeBiMap.ReadWriteLock.EnterWriteLock(); + try + { + NativeFuncs.godotsharp_internal_new_csharp_script(outScript); + IntPtr scriptPtr = outScript->Reference; - // Caller takes care of locking - _scriptTypeBiMap.Add(scriptPtr, scriptType); + _scriptTypeBiMap.Add(scriptPtr, scriptType); + } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitWriteLock(); + } - NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + NativeFuncs.godotsharp_internal_reload_registered_script(outScript->Reference); } [UnmanagedCallersOnly] internal static void RemoveScriptBridge(IntPtr scriptPtr) { + _scriptTypeBiMap.ReadWriteLock.EnterWriteLock(); try { - lock (_scriptTypeBiMap.ReadWriteLock) - { - _scriptTypeBiMap.Remove(scriptPtr); - } + _scriptTypeBiMap.Remove(scriptPtr); } catch (Exception e) { ExceptionUtils.LogException(e); } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitWriteLock(); + } } [UnmanagedCallersOnly] internal static godot_bool TryReloadRegisteredScriptWithClass(IntPtr scriptPtr) { + _scriptTypeBiMap.ReadWriteLock.EnterUpgradeableReadLock(); try { - lock (_scriptTypeBiMap.ReadWriteLock) + if (_scriptTypeBiMap.TryGetScriptType(scriptPtr, out _)) { - if (_scriptTypeBiMap.TryGetScriptType(scriptPtr, out _)) - { - // NOTE: - // Currently, we reload all scripts, not only the ones from the unloaded ALC. - // As such, we need to handle this case instead of treating it as an error. - NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); - return godot_bool.True; - } + // NOTE: + // Currently, we reload all scripts, not only the ones from the unloaded ALC. + // As such, we need to handle this case instead of treating it as an error. + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + return godot_bool.True; + } - if (!_scriptDataForReload.TryGetValue(scriptPtr, out var dataForReload)) - { - GD.PushError("Missing class qualified name for reloading script"); - return godot_bool.False; - } + if (!_scriptDataForReload.TryGetValue(scriptPtr, out var dataForReload)) + { + GD.PushError("Missing class qualified name for reloading script"); + return godot_bool.False; + } - _ = _scriptDataForReload.TryRemove(scriptPtr, out _); + _ = _scriptDataForReload.TryRemove(scriptPtr, out _); - if (dataForReload.assemblyName == null) - { - GD.PushError( - $"Missing assembly name of class '{dataForReload.classFullName}' for reloading script"); - return godot_bool.False; - } + if (dataForReload.assemblyName == null) + { + GD.PushError( + $"Missing assembly name of class '{dataForReload.classFullName}' for reloading script"); + return godot_bool.False; + } - var scriptType = ReflectionUtils.FindTypeInLoadedAssemblies(dataForReload.assemblyName, - dataForReload.classFullName); + var scriptType = ReflectionUtils.FindTypeInLoadedAssemblies(dataForReload.assemblyName, + dataForReload.classFullName); - if (scriptType == null) - { - // The class was removed, can't reload - return godot_bool.False; - } + if (scriptType == null) + { + // The class was removed, can't reload + return godot_bool.False; + } - if (!typeof(GodotObject).IsAssignableFrom(scriptType)) - { - // The class no longer inherits GodotObject, can't reload - return godot_bool.False; - } + if (!typeof(GodotObject).IsAssignableFrom(scriptType)) + { + // The class no longer inherits GodotObject, can't reload + return godot_bool.False; + } + _scriptTypeBiMap.ReadWriteLock.EnterWriteLock(); + try + { _scriptTypeBiMap.Add(scriptPtr, scriptType); + } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitWriteLock(); + } - NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); - return godot_bool.True; - } + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); return godot_bool.False; } + finally + { + _scriptTypeBiMap.ReadWriteLock.ExitUpgradeableReadLock(); + } } private static unsafe void GetScriptTypeInfo(Type scriptType, godot_csharp_type_info* outTypeInfo) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs index 29fa13d6259..aca9595ecba 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; namespace Godot.Bridge; @@ -13,7 +14,7 @@ public static partial class ScriptManagerBridge { private class ScriptTypeBiMap { - public readonly object ReadWriteLock = new(); + public readonly ReaderWriterLockSlim ReadWriteLock = new(LockRecursionPolicy.SupportsRecursion); private System.Collections.Generic.Dictionary _scriptTypeMap = new(); private System.Collections.Generic.Dictionary _typeScriptMap = new(); diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 9dfcd1ca55e..4fcd4bc19ac 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -1984,8 +1984,9 @@ bool OpenXRAPI::poll_events() { if (local_floor_emulation.enabled) { local_floor_emulation.should_reset_floor_height = true; } - if (event->poseValid && xr_interface) { - xr_interface->on_pose_recentered(); + + if (xr_interface) { + xr_interface->on_reference_space_change_pending(); } } break; case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: { diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 7c8dda17788..eec705fe86d 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -1139,6 +1139,12 @@ void OpenXRInterface::process() { if (head.is_valid()) { head->set_pose("default", head_transform, head_linear_velocity, head_angular_velocity, head_confidence); } + + if (reference_stage_changing) { + // Now that we have updated tracking information in our updated reference space, trigger our pose recentered signal. + emit_signal(SNAME("pose_recentered")); + reference_stage_changing = false; + } } void OpenXRInterface::pre_render() { @@ -1320,8 +1326,8 @@ void OpenXRInterface::on_state_exiting() { emit_signal(SNAME("instance_exiting")); } -void OpenXRInterface::on_pose_recentered() { - emit_signal(SNAME("pose_recentered")); +void OpenXRInterface::on_reference_space_change_pending() { + reference_stage_changing = true; } void OpenXRInterface::on_refresh_rate_changes(float p_new_rate) { diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index 71af2f8176b..c7e516ee556 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -72,6 +72,7 @@ class OpenXRInterface : public XRInterface { private: OpenXRAPI *openxr_api = nullptr; bool initialized = false; + bool reference_stage_changing = false; XRInterface::TrackingStatus tracking_state; // At a minimum we need a tracker for our head @@ -209,7 +210,7 @@ class OpenXRInterface : public XRInterface { void on_state_stopping(); void on_state_loss_pending(); void on_state_exiting(); - void on_pose_recentered(); + void on_reference_space_change_pending(); void on_refresh_rate_changes(float p_new_rate); void tracker_profile_changed(RID p_tracker, RID p_interaction_profile); diff --git a/modules/openxr/scene/openxr_composition_layer.cpp b/modules/openxr/scene/openxr_composition_layer.cpp index c33d5ed57dd..5742c1350c6 100644 --- a/modules/openxr/scene/openxr_composition_layer.cpp +++ b/modules/openxr/scene/openxr_composition_layer.cpp @@ -53,6 +53,10 @@ OpenXRCompositionLayer::OpenXRCompositionLayer() { openxr_api = OpenXRAPI::get_singleton(); composition_layer_extension = OpenXRCompositionLayerExtension::get_singleton(); + if (openxr_api) { + openxr_session_running = openxr_api->is_running(); + } + Ref openxr_interface = XRServer::get_singleton()->find_interface("OpenXR"); if (openxr_interface.is_valid()) { openxr_interface->connect("session_begun", callable_mp(this, &OpenXRCompositionLayer::_on_openxr_session_begun)); @@ -340,7 +344,7 @@ void OpenXRCompositionLayer::_notification(int p_what) { } } break; case NOTIFICATION_VISIBILITY_CHANGED: { - if (!fallback && openxr_session_running && is_inside_tree()) { + if (is_natively_supported() && openxr_session_running && is_inside_tree()) { if (layer_viewport && is_visible()) { openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size()); } else { diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index f90650dc00b..c376d00ab27 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -51,6 +51,7 @@ void register_android_api() { #endif GDREGISTER_CLASS(JavaClass); + GDREGISTER_CLASS(JavaObject); GDREGISTER_CLASS(JavaClassWrapper); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaClassWrapper", JavaClassWrapper::get_singleton())); } @@ -61,6 +62,16 @@ void unregister_android_api() { #endif } +void JavaClass::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_java_class_name"), &JavaClass::get_java_class_name); + ClassDB::bind_method(D_METHOD("get_java_method_list"), &JavaClass::get_java_method_list); + ClassDB::bind_method(D_METHOD("get_java_parent_class"), &JavaClass::get_java_parent_class); +} + +void JavaObject::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_java_class"), &JavaObject::get_java_class); +} + void JavaClassWrapper::_bind_methods() { ClassDB::bind_method(D_METHOD("wrap", "name"), &JavaClassWrapper::wrap); } @@ -71,13 +82,32 @@ Variant JavaClass::callp(const StringName &, const Variant **, int, Callable::Ca return Variant(); } +String JavaClass::get_java_class_name() const { + return ""; +} + +TypedArray JavaClass::get_java_method_list() const { + return TypedArray(); +} + +Ref JavaClass::get_java_parent_class() const { + return Ref(); +} + JavaClass::JavaClass() { } +JavaClass::~JavaClass() { +} + Variant JavaObject::callp(const StringName &, const Variant **, int, Callable::CallError &) { return Variant(); } +Ref JavaObject::get_java_class() const { + return Ref(); +} + JavaClassWrapper *JavaClassWrapper::singleton = nullptr; Ref JavaClassWrapper::wrap(const String &) { diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index cbcb4bf49a1..0561904368c 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -34,6 +34,7 @@ #define JAVA_CLASS_WRAPPER_H #include "core/object/ref_counted.h" +#include "core/variant/typed_array.h" #ifdef ANDROID_ENABLED #include @@ -69,6 +70,7 @@ class JavaClass : public RefCounted { struct MethodInfo { bool _static = false; + bool _constructor = false; Vector param_types; Vector param_sigs; uint32_t return_type = 0; @@ -176,14 +178,29 @@ class JavaClass : public RefCounted { bool _call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret); friend class JavaClassWrapper; + friend class JavaObject; + String java_class_name; + String java_constructor_name; HashMap> methods; jclass _class; #endif +protected: + static void _bind_methods(); + public: virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + String get_java_class_name() const; + TypedArray get_java_method_list() const; + Ref get_java_parent_class() const; + +#ifdef ANDROID_ENABLED + virtual String to_string() override; +#endif + JavaClass(); + ~JavaClass(); }; class JavaObject : public RefCounted { @@ -193,14 +210,24 @@ class JavaObject : public RefCounted { Ref base_class; friend class JavaClass; - jobject instance; + jobject instance = nullptr; #endif +protected: + static void _bind_methods(); + public: virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + Ref get_java_class() const; + #ifdef ANDROID_ENABLED - JavaObject(const Ref &p_base, jobject *p_instance); + virtual String to_string() override; + + jobject get_instance() { return instance; } + + JavaObject(); + JavaObject(const Ref &p_base, jobject p_instance); ~JavaObject(); #endif }; @@ -211,13 +238,17 @@ class JavaClassWrapper : public Object { #ifdef ANDROID_ENABLED RBMap> class_cache; friend class JavaClass; - jmethodID getDeclaredMethods; - jmethodID getFields; - jmethodID getParameterTypes; - jmethodID getReturnType; - jmethodID getModifiers; - jmethodID getName; + jmethodID Class_getDeclaredConstructors; + jmethodID Class_getDeclaredMethods; + jmethodID Class_getFields; jmethodID Class_getName; + jmethodID Class_getSuperclass; + jmethodID Constructor_getParameterTypes; + jmethodID Constructor_getModifiers; + jmethodID Method_getParameterTypes; + jmethodID Method_getReturnType; + jmethodID Method_getModifiers; + jmethodID Method_getName; jmethodID Field_getName; jmethodID Field_getModifiers; jmethodID Field_get; @@ -244,6 +275,8 @@ class JavaClassWrapper : public Object { Ref wrap(const String &p_class); #ifdef ANDROID_ENABLED + Ref wrap_jclass(jclass p_class); + JavaClassWrapper(jobject p_activity = nullptr); #else JavaClassWrapper(); diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index f3efbe9fbe6..0e8b444f97c 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -182,6 +182,11 @@ class JNISingleton : public Object { env->DeleteLocalRef(obj); } break; + case Variant::OBJECT: { + jobject obj = env->CallObjectMethodA(instance, E->get().method, v); + ret = _jobject_to_variant(env, obj); + env->DeleteLocalRef(obj); + } break; default: { env->PopLocalFrame(nullptr); ERR_FAIL_V(Variant()); diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 12dedd4c0c3..5df65f2246b 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -46,7 +46,7 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, MethodInfo *method = nullptr; for (MethodInfo &E : M->value) { - if (!p_instance && !E._static) { + if (!p_instance && !E._static && !E._constructor) { r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; continue; } @@ -104,15 +104,19 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, } } break; case ARG_TYPE_CLASS: { - if (p_args[i]->get_type() != Variant::OBJECT) { + if (p_args[i]->get_type() != Variant::OBJECT && p_args[i]->get_type() != Variant::NIL) { arg_expected = Variant::OBJECT; } else { Ref ref = *p_args[i]; - if (!ref.is_null()) { + if (ref.is_valid()) { if (Object::cast_to(ref.ptr())) { Ref jo = ref; //could be faster - jclass c = env->FindClass(E.param_sigs[i].operator String().utf8().get_data()); + String cn = E.param_sigs[i].operator String(); + if (cn.begins_with("L") && cn.ends_with(";")) { + cn = cn.substr(1, cn.length() - 2); + } + jclass c = env->FindClass(cn.utf8().get_data()); if (!c || !env->IsInstanceOf(jo->instance, c)) { arg_expected = Variant::OBJECT; } else { @@ -460,7 +464,9 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, } break; default: { jobject obj; - if (method->_static) { + if (method->_constructor) { + obj = env->NewObject(_class, method->method, argv); + } else if (method->_static) { obj = env->CallStaticObjectMethodA(_class, method->method, argv); } else { obj = env->CallObjectMethodA(p_instance->instance, method->method, argv); @@ -489,7 +495,9 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, Variant JavaClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { Variant ret; - bool found = _call_method(nullptr, p_method, p_args, p_argcount, r_error, ret); + + String method = (p_method == java_constructor_name) ? "" : p_method; + bool found = _call_method(nullptr, method, p_args, p_argcount, r_error, ret); if (found) { return ret; } @@ -497,19 +505,156 @@ Variant JavaClass::callp(const StringName &p_method, const Variant **p_args, int return RefCounted::callp(p_method, p_args, p_argcount, r_error); } +String JavaClass::get_java_class_name() const { + return java_class_name; +} + +TypedArray JavaClass::get_java_method_list() const { + TypedArray method_list; + + for (const KeyValue> &item : methods) { + for (const MethodInfo &mi : item.value) { + Dictionary method; + + method["name"] = mi._constructor ? java_constructor_name : String(item.key); + method["id"] = (uint64_t)mi.method; + method["default_args"] = Array(); + method["flags"] = METHOD_FLAGS_DEFAULT & (mi._static || mi._constructor ? METHOD_FLAG_STATIC : METHOD_FLAG_NORMAL); + + { + Array a; + + for (uint32_t argtype : mi.param_types) { + Dictionary d; + + Variant::Type t = Variant::NIL; + float likelihood = 0.0; + _convert_to_variant_type(argtype, t, likelihood); + d["type"] = t; + if (t == Variant::OBJECT) { + d["hint"] = PROPERTY_HINT_RESOURCE_TYPE; + d["hint_string"] = "JavaObject"; + } else { + d["hint"] = 0; + d["hint_string"] = ""; + } + + a.push_back(d); + } + + method["args"] = a; + } + + { + Dictionary d; + + if (mi._constructor) { + d["type"] = Variant::OBJECT; + d["hint"] = PROPERTY_HINT_RESOURCE_TYPE; + d["hint_string"] = "JavaObject"; + } else { + Variant::Type t = Variant::NIL; + float likelihood = 0.0; + _convert_to_variant_type(mi.return_type, t, likelihood); + d["type"] = t; + if (t == Variant::OBJECT) { + d["hint"] = PROPERTY_HINT_RESOURCE_TYPE; + d["hint_string"] = "JavaObject"; + } else { + d["hint"] = 0; + d["hint_string"] = ""; + } + } + + method["return_type"] = d; + } + + method_list.push_back(method); + } + } + + return method_list; +} + +Ref JavaClass::get_java_parent_class() const { + ERR_FAIL_NULL_V(_class, Ref()); + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Ref()); + + jclass superclass = (jclass)env->CallObjectMethod(_class, JavaClassWrapper::singleton->Class_getSuperclass); + if (!superclass) { + return Ref(); + } + + Ref ret = JavaClassWrapper::singleton->wrap_jclass(superclass); + env->DeleteLocalRef(superclass); + return ret; +} + +String JavaClass::to_string() { + return ""; +} + JavaClass::JavaClass() { } +JavaClass::~JavaClass() { + if (_class) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(_class); + } +} + ///////////////////// Variant JavaObject::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - return Variant(); + if (instance) { + Ref c = base_class; + while (c.is_valid()) { + Variant ret; + bool found = c->_call_method(this, p_method, p_args, p_argcount, r_error, ret); + if (found) { + return ret; + } + c = c->get_java_parent_class(); + } + } + + return RefCounted::callp(p_method, p_args, p_argcount, r_error); +} + +Ref JavaObject::get_java_class() const { + return base_class; } -JavaObject::JavaObject(const Ref &p_base, jobject *p_instance) { +String JavaObject::to_string() { + if (base_class.is_valid() && instance) { + return "java_class_name + " \"" + (String)call("toString") + "\">"; + } + return RefCounted::to_string(); +} + +JavaObject::JavaObject() { +} + +JavaObject::JavaObject(const Ref &p_base, jobject p_instance) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + base_class = p_base; + instance = env->NewGlobalRef(p_instance); } JavaObject::~JavaObject() { + if (instance) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(instance); + } } //////////////////// @@ -651,6 +796,16 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va return true; } break; case ARG_TYPE_CLASS: { + jclass java_class = env->GetObjectClass(obj); + Ref java_class_wrapped = JavaClassWrapper::singleton->wrap_jclass(java_class); + env->DeleteLocalRef(java_class); + + if (java_class_wrapped.is_valid()) { + Ref ret = Ref(memnew(JavaObject(java_class_wrapped, obj))); + var = ret; + return true; + } + return false; } break; case ARG_ARRAY_BIT | ARG_TYPE_VOID: { @@ -968,43 +1123,67 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va } Ref JavaClassWrapper::wrap(const String &p_class) { - if (class_cache.has(p_class)) { - return class_cache[p_class]; + String class_name_dots = p_class.replace("/", "."); + if (class_cache.has(class_name_dots)) { + return class_cache[class_name_dots]; } JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, Ref()); - jclass bclass = env->FindClass(p_class.utf8().get_data()); + jclass bclass = env->FindClass(class_name_dots.replace(".", "/").utf8().get_data()); ERR_FAIL_NULL_V(bclass, Ref()); - jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, getDeclaredMethods); + jobjectArray constructors = (jobjectArray)env->CallObjectMethod(bclass, Class_getDeclaredConstructors); + ERR_FAIL_NULL_V(constructors, Ref()); + jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, Class_getDeclaredMethods); ERR_FAIL_NULL_V(methods, Ref()); Ref java_class = memnew(JavaClass); + java_class->java_class_name = class_name_dots; + Vector class_name_parts = class_name_dots.split("."); + java_class->java_constructor_name = class_name_parts[class_name_parts.size() - 1]; + java_class->_class = (jclass)env->NewGlobalRef(bclass); + class_cache[class_name_dots] = java_class; + + LocalVector methods_and_constructors; + int constructor_count = env->GetArrayLength(constructors); + int method_count = env->GetArrayLength(methods); + methods_and_constructors.resize(method_count + constructor_count); + for (int i = 0; i < constructor_count; i++) { + methods_and_constructors[i] = env->GetObjectArrayElement(constructors, i); + } + for (int i = 0; i < method_count; i++) { + methods_and_constructors[constructor_count + i] = env->GetObjectArrayElement(methods, i); + } - int count = env->GetArrayLength(methods); - - for (int i = 0; i < count; i++) { - jobject obj = env->GetObjectArrayElement(methods, i); + for (int i = 0; i < (int)methods_and_constructors.size(); i++) { + jobject obj = methods_and_constructors[i]; ERR_CONTINUE(!obj); - jstring name = (jstring)env->CallObjectMethod(obj, getName); - String str_method = jstring_to_string(name, env); - env->DeleteLocalRef(name); + bool is_constructor = i < constructor_count; + + String str_method; + if (is_constructor) { + str_method = ""; + } else { + jstring name = (jstring)env->CallObjectMethod(obj, Method_getName); + str_method = jstring_to_string(name, env); + env->DeleteLocalRef(name); + } Vector params; - jint mods = env->CallIntMethod(obj, getModifiers); + jint mods = env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers); if (!(mods & 0x0001)) { env->DeleteLocalRef(obj); continue; //not public bye } - jobjectArray param_types = (jobjectArray)env->CallObjectMethod(obj, getParameterTypes); - int count2 = env->GetArrayLength(param_types); + jobjectArray param_types = (jobjectArray)env->CallObjectMethod(obj, is_constructor ? Constructor_getParameterTypes : Method_getParameterTypes); + int count = env->GetArrayLength(param_types); if (!java_class->methods.has(str_method)) { java_class->methods[str_method] = List(); @@ -1012,10 +1191,11 @@ Ref JavaClassWrapper::wrap(const String &p_class) { JavaClass::MethodInfo mi; mi._static = (mods & 0x8) != 0; + mi._constructor = is_constructor; bool valid = true; String signature = "("; - for (int j = 0; j < count2; j++) { + for (int j = 0; j < count; j++) { jobject obj2 = env->GetObjectArrayElement(param_types, j); String strsig; uint32_t sig = 0; @@ -1031,7 +1211,7 @@ Ref JavaClassWrapper::wrap(const String &p_class) { } if (!valid) { - print_line("Method can't be bound (unsupported arguments): " + p_class + "::" + str_method); + print_line("Method can't be bound (unsupported arguments): " + class_name_dots + "::" + str_method); env->DeleteLocalRef(obj); env->DeleteLocalRef(param_types); continue; @@ -1039,21 +1219,28 @@ Ref JavaClassWrapper::wrap(const String &p_class) { signature += ")"; - jobject return_type = (jobject)env->CallObjectMethod(obj, getReturnType); + if (is_constructor) { + signature += "V"; + mi.return_type = JavaClass::ARG_TYPE_CLASS; + } else { + jobject return_type = (jobject)env->CallObjectMethod(obj, Method_getReturnType); + + String strsig; + uint32_t sig = 0; + if (!_get_type_sig(env, return_type, sig, strsig)) { + print_line("Method can't be bound (unsupported return type): " + class_name_dots + "::" + str_method); + env->DeleteLocalRef(obj); + env->DeleteLocalRef(param_types); + env->DeleteLocalRef(return_type); + continue; + } + + signature += strsig; + mi.return_type = sig; - String strsig; - uint32_t sig = 0; - if (!_get_type_sig(env, return_type, sig, strsig)) { - print_line("Method can't be bound (unsupported return type): " + p_class + "::" + str_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(param_types); env->DeleteLocalRef(return_type); - continue; } - signature += strsig; - mi.return_type = sig; - bool discard = false; for (List::Element *E = java_class->methods[str_method].front(); E; E = E->next()) { @@ -1105,14 +1292,14 @@ Ref JavaClassWrapper::wrap(const String &p_class) { env->DeleteLocalRef(obj); env->DeleteLocalRef(param_types); - env->DeleteLocalRef(return_type); } + env->DeleteLocalRef(constructors); env->DeleteLocalRef(methods); - jobjectArray fields = (jobjectArray)env->CallObjectMethod(bclass, getFields); + jobjectArray fields = (jobjectArray)env->CallObjectMethod(bclass, Class_getFields); - count = env->GetArrayLength(fields); + int count = env->GetArrayLength(fields); for (int i = 0; i < count; i++) { jobject obj = env->GetObjectArrayElement(fields, i); @@ -1148,7 +1335,18 @@ Ref JavaClassWrapper::wrap(const String &p_class) { env->DeleteLocalRef(fields); - return Ref(); + return java_class; +} + +Ref JavaClassWrapper::wrap_jclass(jclass p_class) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Ref()); + + jstring class_name = (jstring)env->CallObjectMethod(p_class, Class_getName); + String class_name_string = jstring_to_string(class_name, env); + env->DeleteLocalRef(class_name); + + return wrap(class_name_string); } JavaClassWrapper *JavaClassWrapper::singleton = nullptr; @@ -1160,16 +1358,23 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { ERR_FAIL_NULL(env); jclass bclass = env->FindClass("java/lang/Class"); - getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); - getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); + Class_getDeclaredConstructors = env->GetMethodID(bclass, "getDeclaredConstructors", "()[Ljava/lang/reflect/Constructor;"); + Class_getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); + Class_getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); + Class_getSuperclass = env->GetMethodID(bclass, "getSuperclass", "()Ljava/lang/Class;"); + env->DeleteLocalRef(bclass); + + bclass = env->FindClass("java/lang/reflect/Constructor"); + Constructor_getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); + Constructor_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/reflect/Method"); - getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); - getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;"); - getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); - getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); + Method_getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); + Method_getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;"); + Method_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); + Method_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/reflect/Field"); diff --git a/platform/android/jni_utils.cpp b/platform/android/jni_utils.cpp index 38f73cf9a70..19960799a35 100644 --- a/platform/android/jni_utils.cpp +++ b/platform/android/jni_utils.cpp @@ -32,6 +32,8 @@ #include "jni_utils.h" +#include "api/java_class_wrapper.h" + jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject) { jvalret v; @@ -187,6 +189,16 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a v.obj = arr; } break; + case Variant::OBJECT: { + Ref generic_object = *p_arg; + if (generic_object.is_valid()) { + jobject obj = env->NewLocalRef(generic_object->get_instance()); + v.val.l = obj; + v.obj = obj; + } else { + v.val.i = 0; + } + } break; default: { v.val.i = 0; @@ -360,9 +372,11 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj) { return ret; } + Ref generic_object(memnew(JavaObject(JavaClassWrapper::get_singleton()->wrap(name), obj))); + env->DeleteLocalRef(c); - return Variant(); + return generic_object; } Variant::Type get_jni_type(const String &p_type) { @@ -397,10 +411,10 @@ Variant::Type get_jni_type(const String &p_type) { idx++; } - return Variant::NIL; + return Variant::OBJECT; } -const char *get_jni_sig(const String &p_type) { +String get_jni_sig(const String &p_type) { static struct { const char *name; const char *sig; @@ -432,5 +446,5 @@ const char *get_jni_sig(const String &p_type) { idx++; } - return "Ljava/lang/Object;"; + return "L" + p_type.replace(".", "/") + ";"; } diff --git a/platform/android/jni_utils.h b/platform/android/jni_utils.h index 9f7c5845f37..93e65b59e7c 100644 --- a/platform/android/jni_utils.h +++ b/platform/android/jni_utils.h @@ -54,6 +54,6 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj); Variant::Type get_jni_type(const String &p_type); -const char *get_jni_sig(const String &p_type); +String get_jni_sig(const String &p_type); #endif // JNI_UTILS_H diff --git a/platform/ios/SCsub b/platform/ios/SCsub index cff7dcc1fd1..905d4384804 100644 --- a/platform/ios/SCsub +++ b/platform/ios/SCsub @@ -67,7 +67,6 @@ ios_lib = [ "ios.mm", "rendering_context_driver_vulkan_ios.mm", "display_server_ios.mm", - "joypad_ios.mm", "godot_view.mm", "tts_ios.mm", "display_layer.mm", diff --git a/platform/ios/joypad_ios.mm b/platform/ios/joypad_ios.mm deleted file mode 100644 index 55074bb2723..00000000000 --- a/platform/ios/joypad_ios.mm +++ /dev/null @@ -1,364 +0,0 @@ -/**************************************************************************/ -/* joypad_ios.mm */ -/**************************************************************************/ -/* This file is part of: */ -/* REDOT ENGINE */ -/* https://redotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2024-present Redot Engine contributors */ -/* (see REDOT_AUTHORS.md) */ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* 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. */ -/**************************************************************************/ - -#import "joypad_ios.h" - -#import "godot_view.h" -#import "os_ios.h" - -#include "core/config/project_settings.h" -#import "drivers/coreaudio/audio_driver_coreaudio.h" -#include "main/main.h" - -JoypadIOS::JoypadIOS() { - observer = [[JoypadIOSObserver alloc] init]; - [observer startObserving]; -} - -JoypadIOS::~JoypadIOS() { - if (observer) { - [observer finishObserving]; - observer = nil; - } -} - -void JoypadIOS::start_processing() { - if (observer) { - [observer startProcessing]; - } -} - -@interface JoypadIOSObserver () - -@property(assign, nonatomic) BOOL isObserving; -@property(assign, nonatomic) BOOL isProcessing; -@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; -@property(strong, nonatomic) NSMutableArray *joypadsQueue; - -@end - -@implementation JoypadIOSObserver - -- (instancetype)init { - self = [super init]; - - if (self) { - [self godot_commonInit]; - } - - return self; -} - -- (void)godot_commonInit { - self.isObserving = NO; - self.isProcessing = NO; -} - -- (void)startProcessing { - self.isProcessing = YES; - - for (GCController *controller in self.joypadsQueue) { - [self addiOSJoypad:controller]; - } - - [self.joypadsQueue removeAllObjects]; -} - -- (void)startObserving { - if (self.isObserving) { - return; - } - - self.isObserving = YES; - - self.connectedJoypads = [NSMutableDictionary dictionary]; - self.joypadsQueue = [NSMutableArray array]; - - // get told when controllers connect, this will be called right away for - // already connected controllers - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasConnected:) - name:GCControllerDidConnectNotification - object:nil]; - - // get told when controllers disconnect - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; -} - -- (void)finishObserving { - if (self.isObserving) { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - } - - self.isObserving = NO; - self.isProcessing = NO; - - self.connectedJoypads = nil; - self.joypadsQueue = nil; -} - -- (void)dealloc { - [self finishObserving]; -} - -- (int)getJoyIdForController:(GCController *)controller { - NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; - - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - } - - return -1; -} - -- (void)addiOSJoypad:(GCController *)controller { - // get a new id for our controller - int joy_id = Input::get_singleton()->get_unused_joy_id(); - - if (joy_id == -1) { - print_verbose("Couldn't retrieve new joy ID."); - return; - } - - // assign our player index - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [self getFreePlayerIndex]; - } - - // tell Godot about our new controller - Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); - - // add it to our dictionary, this will retain our controllers - [self.connectedJoypads setObject:controller forKey:[NSNumber numberWithInt:joy_id]]; - - // set our input handler - [self setControllerInputHandler:controller]; -} - -- (void)controllerWasConnected:(NSNotification *)notification { - // get our controller - GCController *controller = (GCController *)notification.object; - - if (!controller) { - print_verbose("Couldn't retrieve new controller."); - return; - } - - if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) { - print_verbose("Controller is already registered."); - } else if (!self.isProcessing) { - [self.joypadsQueue addObject:controller]; - } else { - [self addiOSJoypad:controller]; - } -} - -- (void)controllerWasDisconnected:(NSNotification *)notification { - // find our joystick, there should be only one in our dictionary - GCController *controller = (GCController *)notification.object; - - if (!controller) { - return; - } - - NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; - for (NSNumber *key in keys) { - // tell Godot this joystick is no longer there - int joy_id = [key intValue]; - Input::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // and remove it from our dictionary - [self.connectedJoypads removeObjectForKey:key]; - } -} - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (self.connectedJoypads == nil) { - NSArray *keys = [self.connectedJoypads allKeys]; - for (NSNumber *key in keys) { - GCController *controller = [self.connectedJoypads objectForKey:key]; - if (controller.playerIndex == GCControllerPlayerIndex1) { - have_player_1 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex2) { - have_player_2 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex3) { - have_player_3 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex4) { - have_player_4 = true; - } - } - } - - if (!have_player_1) { - return GCControllerPlayerIndex1; - } else if (!have_player_2) { - return GCControllerPlayerIndex2; - } else if (!have_player_3) { - return GCControllerPlayerIndex3; - } else if (!have_player_4) { - return GCControllerPlayerIndex4; - } else { - return GCControllerPlayerIndexUnset; - } -} - -- (void)setControllerInputHandler:(GCController *)controller { - // Hook in the callback handler for the correct gamepad profile. - // This is a bit of a weird design choice on Apples part. - // You need to select the most capable gamepad profile for the - // gamepad attached. - if (controller.extendedGamepad != nil) { - // The extended gamepad profile has all the input you could possibly find on - // a gamepad but will only be active if your gamepad actually has all of - // these... - _weakify(self); - _weakify(controller); - - controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - Input::get_singleton()->joy_button(joy_id, JoyButton::B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - Input::get_singleton()->joy_button(joy_id, JoyButton::Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - - if (element == gamepad.leftThumbstick) { - float value = gamepad.leftThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_X, value); - value = -gamepad.leftThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_Y, value); - } else if (element == gamepad.rightThumbstick) { - float value = gamepad.rightThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_X, value); - value = -gamepad.rightThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_Y, value); - } else if (element == gamepad.leftTrigger) { - float value = gamepad.leftTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_LEFT, value); - } else if (element == gamepad.rightTrigger) { - float value = gamepad.rightTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); - } - - if (@available(iOS 13, *)) { - // iOS uses 'buttonOptions' and 'buttonMenu' names for BACK and START joy buttons. - if (element == gamepad.buttonOptions) { - Input::get_singleton()->joy_button(joy_id, JoyButton::BACK, - gamepad.buttonOptions.isPressed); - } else if (element == gamepad.buttonMenu) { - Input::get_singleton()->joy_button(joy_id, JoyButton::START, - gamepad.buttonMenu.isPressed); - } - } - - if (@available(iOS 14, *)) { - // iOS uses 'buttonHome' for the GUIDE joy button. - if (element == gamepad.buttonHome) { - Input::get_singleton()->joy_button(joy_id, JoyButton::GUIDE, - gamepad.buttonHome.isPressed); - } - } - }; - } else if (controller.microGamepad != nil) { - // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad - _weakify(self); - _weakify(controller); - - controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, gamepad.dpad.right.isPressed); - } - }; - } - - ///@TODO need to add support for controller.motion which gives us access to - /// the orientation of the device (if supported) - - ///@TODO need to add support for controllerPausedHandler which should be a - /// toggle -} - -@end diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index 0bb11aa83e9..c29d48ea716 100644 --- a/platform/ios/os_ios.h +++ b/platform/ios/os_ios.h @@ -36,8 +36,8 @@ #ifdef IOS_ENABLED #import "ios.h" -#import "joypad_ios.h" +#import "drivers/apple/joypad_apple.h" #import "drivers/coreaudio/audio_driver_coreaudio.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" @@ -60,7 +60,7 @@ class OS_IOS : public OS_Unix { iOS *ios = nullptr; - JoypadIOS *joypad_ios = nullptr; + JoypadApple *joypad_apple = nullptr; MainLoop *main_loop = nullptr; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index c9062db23d4..346b2c559bc 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -132,12 +132,12 @@ void register_dynamic_symbol(char *name, void *address) { ios = memnew(iOS); Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); - joypad_ios = memnew(JoypadIOS); + joypad_apple = memnew(JoypadApple); } void OS_IOS::deinitialize_modules() { - if (joypad_ios) { - memdelete(joypad_ios); + if (joypad_apple) { + memdelete(joypad_apple); } if (ios) { @@ -171,6 +171,8 @@ void register_dynamic_symbol(char *name, void *address) { DisplayServer::get_singleton()->process_events(); } + joypad_apple->process_joypads(); + return Main::iteration(); } @@ -178,10 +180,6 @@ void register_dynamic_symbol(char *name, void *address) { if (Main::start() == EXIT_SUCCESS) { main_loop->initialize(); } - - if (joypad_ios) { - joypad_ios->start_processing(); - } } void OS_IOS::finalize() { diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index aba75f0e7c5..e7cc3bcfa6d 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -3000,7 +3000,11 @@ bool DisplayServerX11::window_is_focused(WindowID p_window) const { const WindowData &wd = windows[p_window]; - return wd.focused; + Window focused_window; + int focus_ret_state; + XGetInputFocus(x11_display, &focused_window, &focus_ret_state); + + return wd.x11_window == focused_window; } bool DisplayServerX11::window_can_draw(WindowID p_window) const { diff --git a/platform/macos/SCsub b/platform/macos/SCsub index 505e8b3cc20..7a1239018e0 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -120,7 +120,6 @@ files = [ "native_menu_macos.mm", "dir_access_macos.mm", "tts_macos.mm", - "joypad_macos.mm", "rendering_context_driver_vulkan_macos.mm", "gl_manager_macos_angle.mm", "gl_manager_macos_legacy.mm", diff --git a/platform/macos/joypad_macos.h b/platform/macos/joypad_macos.h deleted file mode 100644 index 4a2542c1be5..00000000000 --- a/platform/macos/joypad_macos.h +++ /dev/null @@ -1,91 +0,0 @@ -/**************************************************************************/ -/* joypad_macos.h */ -/**************************************************************************/ -/* This file is part of: */ -/* REDOT ENGINE */ -/* https://redotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2024-present Redot Engine contributors */ -/* (see REDOT_AUTHORS.md) */ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* 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/input/input.h" - -#define Key _QKey -#import -#import -#undef Key - -@interface JoypadMacOSObserver : NSObject - -- (void)startObserving; -- (void)startProcessing; -- (void)finishObserving; - -@end - -API_AVAILABLE(macosx(11)) -@interface RumbleMotor : NSObject -@property(strong, nonatomic) CHHapticEngine *engine; -@property(strong, nonatomic) id player; -@end - -API_AVAILABLE(macosx(11)) -@interface RumbleContext : NSObject -// High frequency motor, it's usually the right engine. -@property(strong, nonatomic) RumbleMotor *weak_motor; -// Low frequency motor, it's usually the left engine. -@property(strong, nonatomic) RumbleMotor *strong_motor; -@end - -// Controller support for macOS begins with macOS 10.9+, -// however haptics (vibrations) are only supported in macOS 11+. -@interface Joypad : NSObject - -@property(assign, nonatomic) BOOL force_feedback; -@property(assign, nonatomic) NSInteger ff_effect_timestamp; -@property(strong, nonatomic) GCController *controller; -@property(strong, nonatomic) RumbleContext *rumble_context API_AVAILABLE(macosx(11)); - -- (instancetype)init; -- (instancetype)init:(GCController *)controller; - -@end - -class JoypadMacOS { -private: - JoypadMacOSObserver *observer; - -public: - JoypadMacOS(); - ~JoypadMacOS(); - - API_AVAILABLE(macosx(11)) - void joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); - API_AVAILABLE(macosx(11)) - void joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp); - - void start_processing(); - void process_joypads(); -}; diff --git a/platform/macos/joypad_macos.mm b/platform/macos/joypad_macos.mm deleted file mode 100644 index d7bc2e77cda..00000000000 --- a/platform/macos/joypad_macos.mm +++ /dev/null @@ -1,613 +0,0 @@ -/**************************************************************************/ -/* joypad_macos.mm */ -/**************************************************************************/ -/* This file is part of: */ -/* REDOT ENGINE */ -/* https://redotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2024-present Redot Engine contributors */ -/* (see REDOT_AUTHORS.md) */ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* 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. */ -/**************************************************************************/ - -#import "joypad_macos.h" - -#include - -#import "os_macos.h" - -#include "core/config/project_settings.h" -#include "core/os/keyboard.h" -#include "core/string/ustring.h" -#include "main/main.h" - -@implementation RumbleMotor - -- (instancetype)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality { - self = [super init]; - self.engine = [controller.haptics createEngineWithLocality:locality]; - self.player = nil; - return self; -} - -- (void)execute_pattern:(CHHapticPattern *)pattern { - NSError *error; - id player = [self.engine createPlayerWithPattern:pattern error:&error]; - - // When all players have stopped for an engine, stop the engine. - [self.engine notifyWhenPlayersFinished:^CHHapticEngineFinishedAction(NSError *_Nullable error) { - return CHHapticEngineFinishedActionStopEngine; - }]; - - self.player = player; - - // Starts the engine and returns if an error was encountered. - if (![self.engine startAndReturnError:&error]) { - print_verbose("Couldn't start controller haptic engine"); - return; - } - if (![self.player startAtTime:0 error:&error]) { - print_verbose("Couldn't execute controller haptic pattern"); - } -} - -- (void)stop { - NSError *error; - [self.player stopAtTime:0 error:&error]; - self.player = nil; -} - -@end - -@implementation RumbleContext - -- (instancetype)init { - self = [super init]; - self.weak_motor = nil; - self.strong_motor = nil; - return self; -} - -- (bool)hasMotors { - return self.weak_motor != nil && self.strong_motor != nil; -} -- (bool)hasActivePlayers { - if (![self hasMotors]) { - return NO; - } - return self.weak_motor.player != nil && self.strong_motor.player != nil; -} - -@end - -@implementation Joypad - -- (instancetype)init { - self = [super init]; - return self; -} -- (instancetype)init:(GCController *)controller { - self = [super init]; - self.controller = controller; - - if (@available(macOS 11, *)) { - // Haptics within the controller is only available in macOS 11+. - self.rumble_context = [[RumbleContext alloc] init]; - - // Create Weak and Strong motors for controller. - self.rumble_context.weak_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle]; - self.rumble_context.strong_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle]; - - // If the rumble motors aren't available, disable force feedback. - if (![self.rumble_context hasMotors]) { - self.force_feedback = NO; - } else { - self.force_feedback = YES; - } - } else { - self.force_feedback = NO; - } - - self.ff_effect_timestamp = 0; - - return self; -} - -@end - -JoypadMacOS::JoypadMacOS() { - observer = [[JoypadMacOSObserver alloc] init]; - [observer startObserving]; - - if (@available(macOS 11.3, *)) { - GCController.shouldMonitorBackgroundEvents = YES; - } -} - -JoypadMacOS::~JoypadMacOS() { - if (observer) { - [observer finishObserving]; - observer = nil; - } -} - -void JoypadMacOS::start_processing() { - if (observer) { - [observer startProcessing]; - } - process_joypads(); -} - -API_AVAILABLE(macosx(10.15)) -CHHapticPattern *get_vibration_pattern(float p_magnitude, float p_duration) { - // Creates a vibration pattern with an intensity and duration. - NSDictionary *hapticDict = @{ - CHHapticPatternKeyPattern : @[ - @{ - CHHapticPatternKeyEvent : @{ - CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, - CHHapticPatternKeyTime : @(CHHapticTimeImmediate), - CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration], - - CHHapticPatternKeyEventParameters : @[ - @{ - CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity, - CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_magnitude] - }, - ], - }, - }, - ], - }; - NSError *error; - CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; - return pattern; -} - -void JoypadMacOS::joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { - if (!p_joypad.force_feedback || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { - return; - } - - // If there is active vibration players, stop them. - if ([p_joypad.rumble_context hasActivePlayers]) { - joypad_vibration_stop(p_joypad, p_timestamp); - } - - // Gets the default vibration pattern and creates a player for each motor. - CHHapticPattern *weak_pattern = get_vibration_pattern(p_weak_magnitude, p_duration); - CHHapticPattern *strong_pattern = get_vibration_pattern(p_strong_magnitude, p_duration); - - RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; - RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; - - [weak_motor execute_pattern:weak_pattern]; - [strong_motor execute_pattern:strong_pattern]; - - p_joypad.ff_effect_timestamp = p_timestamp; -} - -void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp) { - if (!p_joypad.force_feedback) { - return; - } - // If there is no active vibration players, exit. - if (![p_joypad.rumble_context hasActivePlayers]) { - return; - } - - RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; - RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; - - [weak_motor stop]; - [strong_motor stop]; - - p_joypad.ff_effect_timestamp = p_timestamp; -} - -@interface JoypadMacOSObserver () - -@property(assign, nonatomic) BOOL isObserving; -@property(assign, nonatomic) BOOL isProcessing; -@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; -@property(strong, nonatomic) NSMutableArray *joypadsQueue; - -@end - -@implementation JoypadMacOSObserver - -- (instancetype)init { - self = [super init]; - - if (self) { - [self godot_commonInit]; - } - - return self; -} - -- (void)godot_commonInit { - self.isObserving = NO; - self.isProcessing = NO; -} - -- (void)startProcessing { - self.isProcessing = YES; - - for (GCController *controller in self.joypadsQueue) { - [self addMacOSJoypad:controller]; - } - - [self.joypadsQueue removeAllObjects]; -} - -- (void)startObserving { - if (self.isObserving) { - return; - } - - self.isObserving = YES; - - self.connectedJoypads = [NSMutableDictionary dictionary]; - self.joypadsQueue = [NSMutableArray array]; - - // Get told when controllers connect, this will be called right away for - // already connected controllers. - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasConnected:) - name:GCControllerDidConnectNotification - object:nil]; - - // Get told when controllers disconnect. - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; -} - -- (void)finishObserving { - if (self.isObserving) { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - } - - self.isObserving = NO; - self.isProcessing = NO; - - self.connectedJoypads = nil; - self.joypadsQueue = nil; -} - -- (void)dealloc { - [self finishObserving]; -} - -- (NSArray *)getAllKeysForController:(GCController *)controller { - NSArray *keys = [self.connectedJoypads allKeys]; - NSMutableArray *final_keys = [NSMutableArray array]; - - for (NSNumber *key in keys) { - Joypad *joypad = [self.connectedJoypads objectForKey:key]; - if (joypad.controller == controller) { - [final_keys addObject:key]; - } - } - - return final_keys; -} - -- (int)getJoyIdForController:(GCController *)controller { - NSArray *keys = [self getAllKeysForController:controller]; - - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - } - - return -1; -} - -- (void)addMacOSJoypad:(GCController *)controller { - // Get a new id for our controller. - int joy_id = Input::get_singleton()->get_unused_joy_id(); - - if (joy_id == -1) { - print_verbose("Couldn't retrieve new joy ID."); - return; - } - - // Assign our player index. - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [self getFreePlayerIndex]; - } - - // Tell Godot about our new controller. - Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); - - Joypad *joypad = [[Joypad alloc] init:controller]; - - // Add it to our dictionary, this will retain our controllers. - [self.connectedJoypads setObject:joypad forKey:[NSNumber numberWithInt:joy_id]]; - - // Set our input handler. - [self setControllerInputHandler:controller]; -} - -- (void)controllerWasConnected:(NSNotification *)notification { - // Get our controller. - GCController *controller = (GCController *)notification.object; - - if (!controller) { - print_verbose("Couldn't retrieve new controller."); - return; - } - - if ([[self getAllKeysForController:controller] count] > 0) { - print_verbose("Controller is already registered."); - } else if (!self.isProcessing) { - [self.joypadsQueue addObject:controller]; - } else { - [self addMacOSJoypad:controller]; - } -} - -- (void)controllerWasDisconnected:(NSNotification *)notification { - // Find our joystick, there should be only one in our dictionary. - GCController *controller = (GCController *)notification.object; - - if (!controller) { - return; - } - - NSArray *keys = [self getAllKeysForController:controller]; - for (NSNumber *key in keys) { - // Tell Godot this joystick is no longer there. - int joy_id = [key intValue]; - Input::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // And remove it from our dictionary. - [self.connectedJoypads removeObjectForKey:key]; - } -} - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (self.connectedJoypads == nil) { - NSArray *keys = [self.connectedJoypads allKeys]; - for (NSNumber *key in keys) { - Joypad *joypad = [self.connectedJoypads objectForKey:key]; - GCController *controller = joypad.controller; - if (controller.playerIndex == GCControllerPlayerIndex1) { - have_player_1 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex2) { - have_player_2 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex3) { - have_player_3 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex4) { - have_player_4 = true; - } - } - } - - if (!have_player_1) { - return GCControllerPlayerIndex1; - } else if (!have_player_2) { - return GCControllerPlayerIndex2; - } else if (!have_player_3) { - return GCControllerPlayerIndex3; - } else if (!have_player_4) { - return GCControllerPlayerIndex4; - } else { - return GCControllerPlayerIndexUnset; - } -} - -- (void)setControllerInputHandler:(GCController *)controller { - // Hook in the callback handler for the correct gamepad profile. - // This is a bit of a weird design choice on Apples part. - // You need to select the most capable gamepad profile for the - // gamepad attached. - if (controller.extendedGamepad != nil) { - // The extended gamepad profile has all the input you could possibly find on - // a gamepad but will only be active if your gamepad actually has all of - // these... - _weakify(self); - _weakify(controller); - - controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - Input::get_singleton()->joy_button(joy_id, JoyButton::B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - Input::get_singleton()->joy_button(joy_id, JoyButton::Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - - if (element == gamepad.leftThumbstick) { - float value = gamepad.leftThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_X, value); - value = -gamepad.leftThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_Y, value); - } else if (element == gamepad.rightThumbstick) { - float value = gamepad.rightThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_X, value); - value = -gamepad.rightThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_Y, value); - } else if (element == gamepad.leftTrigger) { - float value = gamepad.leftTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_LEFT, value); - } else if (element == gamepad.rightTrigger) { - float value = gamepad.rightTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); - } - - if (@available(macOS 10.14.1, *)) { - if (element == gamepad.leftThumbstickButton) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_STICK, - gamepad.leftThumbstickButton.isPressed); - } else if (element == gamepad.rightThumbstickButton) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_STICK, - gamepad.rightThumbstickButton.isPressed); - } - } - - if (@available(macOS 10.15, *)) { - if (element == gamepad.buttonOptions) { - Input::get_singleton()->joy_button(joy_id, JoyButton::BACK, - gamepad.buttonOptions.isPressed); - } else if (element == gamepad.buttonMenu) { - Input::get_singleton()->joy_button(joy_id, JoyButton::START, - gamepad.buttonMenu.isPressed); - } - } - - if (@available(macOS 11, *)) { - if (element == gamepad.buttonHome) { - Input::get_singleton()->joy_button(joy_id, JoyButton::GUIDE, - gamepad.buttonHome.isPressed); - } - if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { - GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; - if (element == xboxGamepad.paddleButton1) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE1, - xboxGamepad.paddleButton1.isPressed); - } else if (element == xboxGamepad.paddleButton2) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE2, - xboxGamepad.paddleButton2.isPressed); - } else if (element == xboxGamepad.paddleButton3) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE3, - xboxGamepad.paddleButton3.isPressed); - } else if (element == xboxGamepad.paddleButton4) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE4, - xboxGamepad.paddleButton4.isPressed); - } - } - } - - if (@available(macOS 12, *)) { - if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { - GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; - if (element == xboxGamepad.buttonShare) { - Input::get_singleton()->joy_button(joy_id, JoyButton::MISC1, - xboxGamepad.buttonShare.isPressed); - } - } - } - }; - } else if (controller.microGamepad != nil) { - // Micro gamepads were added in macOS 10.11 and feature just 2 buttons and a d-pad. - _weakify(self); - _weakify(controller); - - controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - }; - } - - // TODO: Need to add support for controller.motion which gives us access to - // the orientation of the device (if supported). -} - -@end - -void JoypadMacOS::process_joypads() { - if (@available(macOS 11, *)) { - // Process vibrations in macOS 11+. - NSArray *keys = [observer.connectedJoypads allKeys]; - - for (NSNumber *key in keys) { - int id = key.intValue; - Joypad *joypad = [observer.connectedJoypads objectForKey:key]; - - if (joypad.force_feedback) { - Input *input = Input::get_singleton(); - uint64_t timestamp = input->get_joy_vibration_timestamp(id); - - if (timestamp > (unsigned)joypad.ff_effect_timestamp) { - Vector2 strength = input->get_joy_vibration_strength(id); - float duration = input->get_joy_vibration_duration(id); - if (duration == 0) { - duration = GCHapticDurationInfinite; - } - - if (strength.x == 0 && strength.y == 0) { - joypad_vibration_stop(joypad, timestamp); - } else { - joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); - } - } - } - } - } -} diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index d7162b12a9e..017e54652a1 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -34,16 +34,16 @@ #define OS_MACOS_H #include "crash_handler_macos.h" -#import "joypad_macos.h" #include "core/input/input.h" +#import "drivers/apple/joypad_apple.h" #import "drivers/coreaudio/audio_driver_coreaudio.h" #import "drivers/coremidi/midi_driver_coremidi.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" class OS_MacOS : public OS_Unix { - JoypadMacOS *joypad_macos = nullptr; + JoypadApple *joypad_apple = nullptr; #ifdef COREAUDIO_ENABLED AudioDriverCoreAudio audio_driver; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index ab23e2755a8..d202f72d8b3 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -135,13 +135,13 @@ delete_main_loop(); - if (joypad_macos) { - memdelete(joypad_macos); + if (joypad_apple) { + memdelete(joypad_apple); } } void OS_MacOS::initialize_joypads() { - joypad_macos = memnew(JoypadMacOS()); + joypad_apple = memnew(JoypadApple()); } void OS_MacOS::set_main_loop(MainLoop *p_main_loop) { @@ -784,7 +784,7 @@ if (DisplayServer::get_singleton()) { DisplayServer::get_singleton()->process_events(); // Get rid of pending events. } - joypad_macos->start_processing(); + joypad_apple->process_joypads(); if (Main::iteration()) { quit = true; diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index 8aed2a18ef0..165acb24afe 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -217,6 +217,9 @@ Error EditorExportPlatformWeb::_add_manifest_icon(const String &p_path, const St } Error EditorExportPlatformWeb::_build_pwa(const Ref &p_preset, const String p_path, const Vector &p_shared_objects) { + List preset_features; + get_preset_features(p_preset, &preset_features); + String proj_name = GLOBAL_GET("application/config/name"); if (proj_name.is_empty()) { proj_name = "Redot Game"; diff --git a/platform/windows/console_wrapper_windows.cpp b/platform/windows/console_wrapper_windows.cpp index e458bb776ad..e09217d9c3f 100644 --- a/platform/windows/console_wrapper_windows.cpp +++ b/platform/windows/console_wrapper_windows.cpp @@ -67,7 +67,9 @@ int main(int argc, char *argv[]) { // Enable virtual terminal sequences processing. HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); - DWORD out_mode = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + DWORD out_mode = 0; + GetConsoleMode(stdout_handle, &out_mode); + out_mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(stdout_handle, out_mode); // Find main executable name and check if it exist. diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index c9116c04d6d..dd596308480 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -3161,6 +3161,10 @@ void DisplayServerWindows::process_events() { } _THREAD_SAFE_UNLOCK_ + if (tts) { + tts->process_events(); + } + if (!drop_events) { _process_key_events(); Input::get_singleton()->flush_buffered_events(); diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 18cdba6fd95..0620b87d8b7 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -71,6 +71,7 @@ extern "C" { __declspec(dllexport) DWORD NvOptimusEnablement = 1; __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +__declspec(dllexport) void NoHotPatch() {} // Disable Nahimic code injection. } // Workaround mingw-w64 < 4.0 bug @@ -1962,7 +1963,9 @@ OS_Windows::OS_Windows(HINSTANCE _hInstance) { // NOTE: The engine does not use ANSI escape codes to color error/warning messages; it uses Windows API calls instead. // Therefore, error/warning messages are still colored on Windows versions older than 10. HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - DWORD outMode = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + DWORD outMode = 0; + GetConsoleMode(stdoutHandle, &outMode); + outMode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(stdoutHandle, outMode)) { // Windows 8.1 or below, or Windows 10 prior to Anniversary Update. print_verbose("Can't set the ENABLE_VIRTUAL_TERMINAL_PROCESSING Windows console mode. `print_rich()` will not work as expected."); diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp index 82704a5d6e1..5d16f086b0a 100644 --- a/platform/windows/tts_windows.cpp +++ b/platform/windows/tts_windows.cpp @@ -45,7 +45,7 @@ void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) } else if (event.eEventId == SPEI_END_INPUT_STREAM) { DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[stream_num].id); tts->ids.erase(stream_num); - tts->_update_tts(); + tts->update_requested = true; } else if (event.eEventId == SPEI_WORD_BOUNDARY) { const Char16String &string = tts->ids[stream_num].string; int pos = 0; @@ -62,8 +62,8 @@ void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) } } -void TTS_Windows::_update_tts() { - if (!is_speaking() && !paused && queue.size() > 0) { +void TTS_Windows::process_events() { + if (update_requested && !paused && queue.size() > 0 && !is_speaking()) { DisplayServer::TTSUtterance &message = queue.front()->get(); String text; @@ -112,6 +112,8 @@ void TTS_Windows::_update_tts() { ids[(uint32_t)stream_number] = ut; queue.pop_front(); + + update_requested = false; } } @@ -209,7 +211,7 @@ void TTS_Windows::speak(const String &p_text, const String &p_voice, int p_volum if (is_paused()) { resume(); } else { - _update_tts(); + update_requested = true; } } diff --git a/platform/windows/tts_windows.h b/platform/windows/tts_windows.h index cf315b988dd..c8a879e93ab 100644 --- a/platform/windows/tts_windows.h +++ b/platform/windows/tts_windows.h @@ -57,9 +57,9 @@ class TTS_Windows { int id; }; HashMap ids; + bool update_requested = false; static void __stdcall speech_event_callback(WPARAM wParam, LPARAM lParam); - void _update_tts(); static TTS_Windows *singleton; @@ -75,6 +75,8 @@ class TTS_Windows { void resume(); void stop(); + void process_events(); + TTS_Windows(); ~TTS_Windows(); }; diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 3f3254ec889..c0b0729620f 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -693,6 +693,7 @@ void GPUParticles2D::_notification(int p_what) { RS::get_singleton()->particles_set_speed_scale(particles, 0); } set_process_internal(true); + set_physics_process_internal(true); previous_position = get_global_position(); } break; @@ -716,15 +717,6 @@ void GPUParticles2D::_notification(int p_what) { } break; case NOTIFICATION_INTERNAL_PROCESS: { - const Vector3 velocity = Vector3((get_global_position() - previous_position).x, (get_global_position() - previous_position).y, 0.0) / - get_process_delta_time(); - - if (velocity != previous_velocity) { - RS::get_singleton()->particles_set_emitter_velocity(particles, velocity); - previous_velocity = velocity; - } - previous_position = get_global_position(); - if (one_shot) { time += get_process_delta_time(); if (time > emission_time) { @@ -744,6 +736,19 @@ void GPUParticles2D::_notification(int p_what) { } } } break; + + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + // Update velocity in physics process, so that velocity calculations remain correct + // if the physics tick rate is lower than the rendered framerate (especially without physics interpolation). + const Vector3 velocity = Vector3((get_global_position() - previous_position).x, (get_global_position() - previous_position).y, 0.0) / + get_physics_process_delta_time(); + + if (velocity != previous_velocity) { + RS::get_singleton()->particles_set_emitter_velocity(particles, velocity); + previous_velocity = velocity; + } + previous_position = get_global_position(); + } break; } } diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp index fab14d503a6..da8ca0419b9 100644 --- a/scene/2d/path_2d.cpp +++ b/scene/2d/path_2d.cpp @@ -169,17 +169,16 @@ void Path2D::_curve_changed() { return; } - if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_paths_hint()) { - return; - } - - queue_redraw(); for (int i = 0; i < get_child_count(); i++) { PathFollow2D *follow = Object::cast_to(get_child(i)); if (follow) { follow->path_changed(); } } + + if (Engine::get_singleton()->is_editor_hint() || get_tree()->is_debugging_paths_hint()) { + queue_redraw(); + } } void Path2D::set_curve(const Ref &p_curve) { diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp index d63b61be32d..3ca061ef724 100644 --- a/scene/3d/camera_3d.cpp +++ b/scene/3d/camera_3d.cpp @@ -398,9 +398,12 @@ Vector3 Camera3D::project_position(const Point2 &p_point, real_t p_z_depth) cons } Size2 viewport_size = get_viewport()->get_visible_rect().size; - Projection cm = _get_camera_projection(p_z_depth); + Projection cm = _get_camera_projection(_near); - Vector2 vp_he = cm.get_viewport_half_extents(); + Plane z_slice(Vector3(0, 0, 1), -p_z_depth); + Vector3 res; + z_slice.intersect_3(cm.get_projection_plane(Projection::Planes::PLANE_RIGHT), cm.get_projection_plane(Projection::Planes::PLANE_TOP), &res); + Vector2 vp_he(res.x, res.y); Vector2 point; point.x = (p_point.x / viewport_size.x) * 2.0 - 1.0; diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index d4669acbbaf..f4b1338e16c 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -465,14 +465,6 @@ void GPUParticles3D::_notification(int p_what) { // Use internal process when emitting and one_shot is on so that when // the shot ends the editor can properly update. case NOTIFICATION_INTERNAL_PROCESS: { - const Vector3 velocity = (get_global_position() - previous_position) / get_process_delta_time(); - - if (velocity != previous_velocity) { - RS::get_singleton()->particles_set_emitter_velocity(particles, velocity); - previous_velocity = velocity; - } - previous_position = get_global_position(); - if (one_shot) { time += get_process_delta_time(); if (time > emission_time) { @@ -493,8 +485,21 @@ void GPUParticles3D::_notification(int p_what) { } } break; + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + // Update velocity in physics process, so that velocity calculations remain correct + // if the physics tick rate is lower than the rendered framerate (especially without physics interpolation). + const Vector3 velocity = (get_global_position() - previous_position) / get_physics_process_delta_time(); + + if (velocity != previous_velocity) { + RS::get_singleton()->particles_set_emitter_velocity(particles, velocity); + previous_velocity = velocity; + } + previous_position = get_global_position(); + } break; + case NOTIFICATION_ENTER_TREE: { set_process_internal(false); + set_physics_process_internal(false); if (sub_emitter != NodePath()) { _attach_sub_emitter(); } @@ -505,6 +510,7 @@ void GPUParticles3D::_notification(int p_what) { } previous_position = get_global_transform().origin; set_process_internal(true); + set_physics_process_internal(true); } break; case NOTIFICATION_EXIT_TREE: { diff --git a/scene/3d/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index cceda79f6b6..6c4ec5985e9 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -855,6 +855,10 @@ Ref Skeleton3D::register_skin(const Ref &p_skin) { return skin_ref; } +void Skeleton3D::force_update_deferred() { + _make_dirty(); +} + void Skeleton3D::force_update_all_dirty_bones() { if (!dirty) { return; diff --git a/scene/3d/skeleton_3d.h b/scene/3d/skeleton_3d.h index 937b11eb91f..6f2f86111c4 100644 --- a/scene/3d/skeleton_3d.h +++ b/scene/3d/skeleton_3d.h @@ -265,6 +265,7 @@ class Skeleton3D : public Node3D { void force_update_all_dirty_bones(); void force_update_all_bone_transforms(); void force_update_bone_children_transforms(int bone_idx); + void force_update_deferred(); void set_modifier_callback_mode_process(ModifierCallbackModeProcess p_mode); ModifierCallbackModeProcess get_modifier_callback_mode_process() const; diff --git a/scene/3d/skeleton_modifier_3d.cpp b/scene/3d/skeleton_modifier_3d.cpp index eb85639ead8..8ffa9296bb7 100644 --- a/scene/3d/skeleton_modifier_3d.cpp +++ b/scene/3d/skeleton_modifier_3d.cpp @@ -77,6 +77,17 @@ void SkeletonModifier3D::_skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) // } +void SkeletonModifier3D::_force_update_skeleton_skin() { + if (!is_inside_tree()) { + return; + } + Skeleton3D *skeleton = get_skeleton(); + if (!skeleton) { + return; + } + skeleton->force_update_deferred(); +} + /* Process */ void SkeletonModifier3D::set_active(bool p_active) { @@ -85,6 +96,7 @@ void SkeletonModifier3D::set_active(bool p_active) { } active = p_active; _set_active(active); + _force_update_skeleton_skin(); } bool SkeletonModifier3D::is_active() const { @@ -121,6 +133,10 @@ void SkeletonModifier3D::_notification(int p_what) { case NOTIFICATION_PARENTED: { _update_skeleton(); } break; + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_UNPARENTED: { + _force_update_skeleton_skin(); + } break; } } diff --git a/scene/3d/skeleton_modifier_3d.h b/scene/3d/skeleton_modifier_3d.h index 7e7ddf80994..7eb2fc6299e 100644 --- a/scene/3d/skeleton_modifier_3d.h +++ b/scene/3d/skeleton_modifier_3d.h @@ -52,6 +52,7 @@ class SkeletonModifier3D : public Node3D { void _update_skeleton(); void _update_skeleton_path(); + void _force_update_skeleton_skin(); virtual void _skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new); diff --git a/scene/animation/animation_blend_space_1d.cpp b/scene/animation/animation_blend_space_1d.cpp index 809d5139583..beb1a17b73f 100644 --- a/scene/animation/animation_blend_space_1d.cpp +++ b/scene/animation/animation_blend_space_1d.cpp @@ -376,21 +376,20 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(const AnimationM } if (new_closest != cur_closest && new_closest != -1) { - NodeTimeInfo from; if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { - //for ping-pong loop + NodeTimeInfo from; + // For ping-pong loop. Ref na_c = static_cast>(blend_points[cur_closest].node); Ref na_n = static_cast>(blend_points[new_closest].node); if (!na_c.is_null() && !na_n.is_null()) { na_n->set_backward(na_c->is_backward()); } - //see how much animation remains + // See how much animation remains. pi.seeked = false; pi.weight = 0; - from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); + from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, true); + pi.time = from.position; } - - pi.time = from.position; pi.seeked = true; pi.weight = 1.0; mind = blend_node(blend_points[new_closest].node, blend_points[new_closest].name, pi, FILTER_IGNORE, true, p_test_only); diff --git a/scene/animation/animation_blend_space_2d.cpp b/scene/animation/animation_blend_space_2d.cpp index 5dd63d74e37..43c7a6483de 100644 --- a/scene/animation/animation_blend_space_2d.cpp +++ b/scene/animation/animation_blend_space_2d.cpp @@ -553,21 +553,20 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(const AnimationM } if (new_closest != cur_closest && new_closest != -1) { - NodeTimeInfo from; if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { - //for ping-pong loop + NodeTimeInfo from; + // For ping-pong loop. Ref na_c = static_cast>(blend_points[cur_closest].node); Ref na_n = static_cast>(blend_points[new_closest].node); if (!na_c.is_null() && !na_n.is_null()) { na_n->set_backward(na_c->is_backward()); } - //see how much animation remains + // See how much animation remains. pi.seeked = false; pi.weight = 0; - from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); + from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, true); + pi.time = from.position; } - - pi.time = from.position; pi.seeked = true; pi.weight = 1.0; mind = blend_node(blend_points[new_closest].node, blend_points[new_closest].name, pi, FILTER_IGNORE, true, p_test_only); diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index 84295a59a9c..6fb4b2f0b70 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -46,6 +46,18 @@ Vector (*AnimationNodeAnimation::get_editable_animation_list)() = nullpt void AnimationNodeAnimation::get_parameter_list(List *r_list) const { AnimationNode::get_parameter_list(r_list); + r_list->push_back(PropertyInfo(Variant::BOOL, backward, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); +} + +Variant AnimationNodeAnimation::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + if (p_parameter == backward) { + return false; + } + return 0; } AnimationNode::NodeTimeInfo AnimationNodeAnimation::get_node_time_info() const { @@ -97,7 +109,7 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::process(const AnimationMixer if (p_playback_info.seeked) { pi.delta = get_node_time_info().position - p_playback_info.time; } else { - pi.time = get_node_time_info().position + (backward ? -p_playback_info.delta : p_playback_info.delta); + pi.time = get_node_time_info().position + (get_parameter(backward) ? -p_playback_info.delta : p_playback_info.delta); } NodeTimeInfo nti = _process(pi, p_test_only); @@ -130,6 +142,7 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe double cur_len = cur_nti.length; double cur_time = p_playback_info.time; double cur_delta = p_playback_info.delta; + bool cur_backward = get_parameter(backward); Animation::LoopMode cur_loop_mode = cur_nti.loop_mode; double prev_time = cur_nti.position; @@ -147,13 +160,13 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe if (!Math::is_zero_approx(cur_len)) { cur_time = Math::fposmod(cur_time, cur_len); } - backward = false; + cur_backward = false; } else { if (!Math::is_zero_approx(cur_len)) { if (Animation::is_greater_or_equal_approx(prev_time, 0) && Animation::is_less_approx(cur_time, 0)) { - backward = !backward; + cur_backward = !cur_backward; } else if (Animation::is_less_or_equal_approx(prev_time, cur_len) && Animation::is_greater_approx(cur_time, cur_len)) { - backward = !backward; + cur_backward = !cur_backward; } cur_time = Math::pingpong(cur_time, cur_len); } @@ -166,7 +179,7 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe cur_delta += cur_time - cur_len; cur_time = cur_len; } - backward = false; + cur_backward = false; // If ended, don't progress AnimationNode. So set delta to 0. if (!Math::is_zero_approx(cur_delta)) { if (play_mode == PLAY_MODE_FORWARD) { @@ -259,6 +272,8 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe blend_animation(animation, pi); } + set_parameter(backward, cur_backward); + return nti; } @@ -275,11 +290,11 @@ AnimationNodeAnimation::PlayMode AnimationNodeAnimation::get_play_mode() const { } void AnimationNodeAnimation::set_backward(bool p_backward) { - backward = p_backward; + set_parameter(backward, p_backward); } bool AnimationNodeAnimation::is_backward() const { - return backward; + return get_parameter(backward); } void AnimationNodeAnimation::set_use_custom_timeline(bool p_use_custom_timeline) { diff --git a/scene/animation/animation_blend_tree.h b/scene/animation/animation_blend_tree.h index 825185381ab..27987462faa 100644 --- a/scene/animation/animation_blend_tree.h +++ b/scene/animation/animation_blend_tree.h @@ -38,6 +38,8 @@ class AnimationNodeAnimation : public AnimationRootNode { GDCLASS(AnimationNodeAnimation, AnimationRootNode); + StringName backward = "backward"; // Only used by pingpong animation. + StringName animation; bool use_custom_timeline = false; @@ -56,6 +58,7 @@ class AnimationNodeAnimation : public AnimationRootNode { }; void get_parameter_list(List *r_list) const override; + virtual Variant get_parameter_default_value(const StringName &p_parameter) const override; virtual NodeTimeInfo get_node_time_info() const override; // Wrapper of get_parameter(). @@ -97,7 +100,6 @@ class AnimationNodeAnimation : public AnimationRootNode { private: PlayMode play_mode = PLAY_MODE_FORWARD; - bool backward = false; // Only used by pingpong animation. }; VARIANT_ENUM_CAST(AnimationNodeAnimation::PlayMode) diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index d3a4a2cd768..7124f6ec016 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -2905,7 +2905,7 @@ void TextEdit::_close_ime_window() { void TextEdit::_update_ime_window_position() { DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; - if (wid == DisplayServer::INVALID_WINDOW_ID || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { + if (wid == DisplayServer::INVALID_WINDOW_ID || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME) || !DisplayServer::get_singleton()->window_is_focused(wid)) { return; } DisplayServer::get_singleton()->window_set_ime_active(true, wid); diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp index 129a04845aa..99f31157874 100644 --- a/scene/resources/2d/tile_set.cpp +++ b/scene/resources/2d/tile_set.cpp @@ -6427,9 +6427,9 @@ int TileData::get_terrain_set() const { } void TileData::set_terrain(int p_terrain) { - ERR_FAIL_COND(terrain_set < 0); ERR_FAIL_COND(p_terrain < -1); - if (tile_set) { + ERR_FAIL_COND(terrain_set < 0 && p_terrain != -1); + if (tile_set && terrain_set >= 0) { ERR_FAIL_COND(p_terrain >= tile_set->get_terrains_count(terrain_set)); } terrain = p_terrain; @@ -6442,9 +6442,9 @@ int TileData::get_terrain() const { void TileData::set_terrain_peering_bit(TileSet::CellNeighbor p_peering_bit, int p_terrain_index) { ERR_FAIL_INDEX(p_peering_bit, TileSet::CellNeighbor::CELL_NEIGHBOR_MAX); - ERR_FAIL_COND(terrain_set < 0); ERR_FAIL_COND(p_terrain_index < -1); - if (tile_set) { + ERR_FAIL_COND(terrain_set < 0 && p_terrain_index != -1); + if (tile_set && terrain_set >= 0) { ERR_FAIL_COND(p_terrain_index >= tile_set->get_terrains_count(terrain_set)); ERR_FAIL_COND(!is_valid_terrain_peering_bit(p_peering_bit)); } diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index 97b78167c73..cc2404d5e34 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -4810,9 +4810,9 @@ void Animation::compress(uint32_t p_page_size, uint32_t p_fps, float p_split_tol continue; // This track is exhausted (all keys were added already), don't consider. } } - - uint32_t key_frame = double(track_get_key_time(uncomp_track, time_tracks[i].key_index)) / frame_len; - + double key_time = track_get_key_time(uncomp_track, time_tracks[i].key_index); + double result = key_time / frame_len; + uint32_t key_frame = Math::fast_ftoi(result); if (time_tracks[i].needs_start_frame && key_frame > base_page_frame) { start_frame = true; best_frame = base_page_frame; diff --git a/servers/physics_3d/godot_space_3d.cpp b/servers/physics_3d/godot_space_3d.cpp index e896141ba00..ee37940789b 100644 --- a/servers/physics_3d/godot_space_3d.cpp +++ b/servers/physics_3d/godot_space_3d.cpp @@ -972,6 +972,7 @@ bool GodotSpace3D::test_body_motion(GodotBody3D *p_body, const PhysicsServer3D:: rcd.object = col_obj; rcd.shape = shape_idx; + rcd.local_shape = j; bool sc = GodotCollisionSolver3D::solve_static(body_shape, body_shape_xform, col_obj->get_shape(shape_idx), col_obj->get_transform() * col_obj->get_shape_transform(shape_idx), _rest_cbk_result, &rcd, nullptr, margin); if (!sc) { continue; diff --git a/tests/scene/test_camera_3d.h b/tests/scene/test_camera_3d.h index 549383b17f6..040bc1072bb 100644 --- a/tests/scene/test_camera_3d.h +++ b/tests/scene/test_camera_3d.h @@ -233,10 +233,13 @@ TEST_CASE("[SceneTree][Camera3D] Project/Unproject position") { test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); // Center. CHECK(test_camera->project_position(Vector2(200, 100), 0.5f).is_equal_approx(Vector3(0, 0, -0.5f))); + CHECK(test_camera->project_position(Vector2(200, 100), test_camera->get_far()).is_equal_approx(Vector3(0, 0, -test_camera->get_far()))); // Top left. CHECK(test_camera->project_position(Vector2(0, 0), 1.5f).is_equal_approx(Vector3(-5.0f, 2.5f, -1.5f))); + CHECK(test_camera->project_position(Vector2(0, 0), test_camera->get_near()).is_equal_approx(Vector3(-5.0f, 2.5f, -test_camera->get_near()))); // Bottom right. CHECK(test_camera->project_position(Vector2(400, 200), 5.0f).is_equal_approx(Vector3(5.0f, -2.5f, -5.0f))); + CHECK(test_camera->project_position(Vector2(400, 200), test_camera->get_far()).is_equal_approx(Vector3(5.0f, -2.5f, -test_camera->get_far()))); } SUBCASE("Perspective projection") { @@ -244,12 +247,15 @@ TEST_CASE("[SceneTree][Camera3D] Project/Unproject position") { // Center. CHECK(test_camera->project_position(Vector2(200, 100), 0.5f).is_equal_approx(Vector3(0, 0, -0.5f))); CHECK(test_camera->project_position(Vector2(200, 100), 100.0f).is_equal_approx(Vector3(0, 0, -100.0f))); + CHECK(test_camera->project_position(Vector2(200, 100), test_camera->get_far()).is_equal_approx(Vector3(0, 0, -1.0f) * test_camera->get_far())); // 3/4th way to Top left. CHECK(test_camera->project_position(Vector2(100, 50), 0.5f).is_equal_approx(Vector3(-SQRT3 * 0.5f, SQRT3 * 0.25f, -0.5f))); CHECK(test_camera->project_position(Vector2(100, 50), 1.0f).is_equal_approx(Vector3(-SQRT3, SQRT3 * 0.5f, -1.0f))); + CHECK(test_camera->project_position(Vector2(100, 50), test_camera->get_near()).is_equal_approx(Vector3(-SQRT3, SQRT3 * 0.5f, -1.0f) * test_camera->get_near())); // 3/4th way to Bottom right. CHECK(test_camera->project_position(Vector2(300, 150), 0.5f).is_equal_approx(Vector3(SQRT3 * 0.5f, -SQRT3 * 0.25f, -0.5f))); CHECK(test_camera->project_position(Vector2(300, 150), 1.0f).is_equal_approx(Vector3(SQRT3, -SQRT3 * 0.5f, -1.0f))); + CHECK(test_camera->project_position(Vector2(300, 150), test_camera->get_far()).is_equal_approx(Vector3(SQRT3, -SQRT3 * 0.5f, -1.0f) * test_camera->get_far())); } } diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 9e59a406669..a12f390ecf3 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -344,6 +344,9 @@ struct GodotTestCaseListener : public doctest::IReporter { #ifdef TOOLS_ENABLED if (EditorSettings::get_singleton()) { EditorSettings::destroy(); + + // Instantiating the EditorSettings singleton sets the locale to the editor's language. + TranslationServer::get_singleton()->set_locale("en"); } #endif // TOOLS_ENABLED diff --git a/thirdparty/README.md b/thirdparty/README.md index b8785ca5e99..b8451890b24 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -537,7 +537,7 @@ in the MSVC debugger. ## mbedtls - Upstream: https://github.com/Mbed-TLS/mbedtls -- Version: 3.6.1 (71c569d44bf3a8bd53d874c81ee8ac644dd6e9e3, 2024) +- Version: 3.6.2 (107ea89daaefb9867ea9121002fbbdf926780e98, 2024) - License: Apache 2.0 File extracted from upstream release tarball: diff --git a/thirdparty/mbedtls/include/mbedtls/build_info.h b/thirdparty/mbedtls/include/mbedtls/build_info.h index 8242ec68281..d91d2964b6a 100644 --- a/thirdparty/mbedtls/include/mbedtls/build_info.h +++ b/thirdparty/mbedtls/include/mbedtls/build_info.h @@ -26,16 +26,16 @@ */ #define MBEDTLS_VERSION_MAJOR 3 #define MBEDTLS_VERSION_MINOR 6 -#define MBEDTLS_VERSION_PATCH 1 +#define MBEDTLS_VERSION_PATCH 2 /** * The single version number has the following structure: * MMNNPP00 * Major version | Minor version | Patch version */ -#define MBEDTLS_VERSION_NUMBER 0x03060100 -#define MBEDTLS_VERSION_STRING "3.6.1" -#define MBEDTLS_VERSION_STRING_FULL "Mbed TLS 3.6.1" +#define MBEDTLS_VERSION_NUMBER 0x03060200 +#define MBEDTLS_VERSION_STRING "3.6.2" +#define MBEDTLS_VERSION_STRING_FULL "Mbed TLS 3.6.2" /* Macros for build-time platform detection */ diff --git a/thirdparty/mbedtls/library/pkwrite.c b/thirdparty/mbedtls/library/pkwrite.c index 5e009c565ea..2a698448bee 100644 --- a/thirdparty/mbedtls/library/pkwrite.c +++ b/thirdparty/mbedtls/library/pkwrite.c @@ -65,17 +65,21 @@ static int pk_write_rsa_der(unsigned char **p, unsigned char *buf, #if defined(MBEDTLS_USE_PSA_CRYPTO) if (mbedtls_pk_get_type(pk) == MBEDTLS_PK_OPAQUE) { uint8_t tmp[PSA_EXPORT_KEY_PAIR_MAX_SIZE]; - size_t len = 0, tmp_len = 0; + size_t tmp_len = 0; if (psa_export_key(pk->priv_id, tmp, sizeof(tmp), &tmp_len) != PSA_SUCCESS) { return MBEDTLS_ERR_PK_BAD_INPUT_DATA; } + /* Ensure there's enough space in the provided buffer before copying data into it. */ + if (tmp_len > (size_t) (*p - buf)) { + mbedtls_platform_zeroize(tmp, sizeof(tmp)); + return MBEDTLS_ERR_ASN1_BUF_TOO_SMALL; + } *p -= tmp_len; memcpy(*p, tmp, tmp_len); - len += tmp_len; mbedtls_platform_zeroize(tmp, sizeof(tmp)); - return (int) len; + return (int) tmp_len; } #endif /* MBEDTLS_USE_PSA_CRYPTO */ return mbedtls_rsa_write_key(mbedtls_pk_rsa(*pk), buf, p); @@ -125,6 +129,10 @@ static int pk_write_ec_pubkey(unsigned char **p, unsigned char *start, if (psa_export_public_key(pk->priv_id, buf, sizeof(buf), &len) != PSA_SUCCESS) { return MBEDTLS_ERR_PK_BAD_INPUT_DATA; } + /* Ensure there's enough space in the provided buffer before copying data into it. */ + if (len > (size_t) (*p - start)) { + return MBEDTLS_ERR_ASN1_BUF_TOO_SMALL; + } *p -= len; memcpy(*p, buf, len); return (int) len;