diff options
124 files changed, 3962 insertions, 1509 deletions
diff --git a/AUTHORS.md b/AUTHORS.md index 3d7a6adf60..a5ef24e825 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -117,7 +117,8 @@ name is available. m4nu3lf marynate mrezai - rraallvv + robfram romulox-x + rraallvv sersoong yg2f (SuperUserNameMan) @@ -30,9 +30,11 @@ generous deed immortalized in the next stable release of Godot Engine. codetrotter E Hewert Hein-Pieter van Braam + Igors Vaitkus Jamal Alyafei Jay Sistar Matthieu Huvé + Mike King Nathan Warden Neal Gompa (Conan Kudo) Pascal Julien @@ -50,6 +52,7 @@ generous deed immortalized in the next stable release of Godot Engine. Asdf cheese65536 Jake Bo + Kris Michael Manuele Finocchiaro Officine Pixel S.n.c. Rémi Verschelde @@ -60,11 +63,15 @@ generous deed immortalized in the next stable release of Godot Engine. Austen McRae Benjamin Botwin Bernhard Liebl + Catalin Moldovan + DeepSquid + Florian Breisch + Gary Oberbrunner Johannes Wuensch Josep G. Camarasa - Kris Michael + Joshua Lesperance Libre-Dépanne - Mike King + Matthew Bennett Ranoller Rob Messick Svenne Krap @@ -81,12 +88,14 @@ generous deed immortalized in the next stable release of Godot Engine. Garrett Dockins Guilherme Felipe de C. G. da Silva Harman Bains - Henrique Alves + John + Justo Delgado Baudà Karsten Bock Laurence Bannister Rami Robert Willes Robin Arys + Rufus Sasparilla ScottMakesGames Testus Maximus Thomas Bjarnelöf @@ -94,9 +103,9 @@ generous deed immortalized in the next stable release of Godot Engine. Xavier Tan Zaq Poi + Alessandra Pereyra Alexey Dyadchenko Amanda Haldy - Arnaud Verstuyf Chris Brown Chris Wilson Cody Parker @@ -111,23 +120,26 @@ generous deed immortalized in the next stable release of Godot Engine. Jeppe Zapp Jeremi Biernacki joe513 + John O'Mahoney Jordan M Lucas Juraj Móza Justin Arnold - Justo Delgado Baudà Leandro Voltolino Lisandro Lorea + Marco Andrew Cafolla Markus Wiesner - Marty Plumbo Marvin Nick Nikitin Pablo Cholaky Patrick Schnorbus Pete Goodwin Phyronnaz + Simon De Greve + Sofox Ted Travis Womack Trent McPheron + Vladimir ## Silver donors @@ -142,13 +154,13 @@ generous deed immortalized in the next stable release of Godot Engine. Anthony Bongiovanni Arda Erol Arthur S. Muszynski + Aubrey Falconer Avencherus Bastian Böhm Benedikt Benjamin Beshara Ben Vercammen Blair Allen - Bryan Crow Bryanna M Bryan Stevenson Carwyn Edwards @@ -161,6 +173,7 @@ generous deed immortalized in the next stable release of Godot Engine. Daniel Kaplan Daniel Langegger Daniel Mircea + David David Cravens David May Dominik Wetzel @@ -168,21 +181,23 @@ generous deed immortalized in the next stable release of Godot Engine. Fabian Becker fengjiongmax Francesco Lisi + Frédéric Alix G3Dev sà rl Geequlim Gerrit Großkopf Gerrit Procee Gilberto K. Otubo Guldoman - HeartBeast Heribert Hirth + hubert jenkins Hunter Jones ialex32x + Ivan Vodopiviz Jaime Ruiz-Borau Vizárraga - Jed Rose + Jed Jeff Hungerford Joel Fivat - Johannes du Randt + Johan Lindberg Jonas Yamazaki Jonathan Martin Jonathan Nieto @@ -199,9 +214,9 @@ generous deed immortalized in the next stable release of Godot Engine. Klavdij Voncina Lars pfeffer Linus Lind Lundgren + Macil magodev Martin Eigel - Martin Novák Matthew Fitzpatrick Matthias Hölzl Max R.R. Collada @@ -229,15 +244,14 @@ generous deed immortalized in the next stable release of Godot Engine. Patric Vormstein Paul Mason Paweł Kowal - Pedro Luz Pierre-Igor Berthet Pietro Vertechi Piotr Kaczmarski Richman Stewart - Rodolfo Baeza Roger Burgess Roger Smith Roman Tinkov + Ryan Whited Sasori Olkof Scott D. Yelich Sootstone @@ -245,10 +259,14 @@ generous deed immortalized in the next stable release of Godot Engine. Thibault Barbaroux Thomas Bell Thomas Herzog & Xananax + Thomas Kurz Tom Larrow Tyler Stafos UltyX + Victor Gonzalez Fernandez Victor Holt + Viktor Ferenczi + werner mendizabal Wout Standaert Yu He diff --git a/core/array.cpp b/core/array.cpp index 0ddac1662c..9e3250fd47 100644 --- a/core/array.cpp +++ b/core/array.cpp @@ -35,8 +35,8 @@ #include "variant.h" #include "vector.h" -struct ArrayPrivate { - +class ArrayPrivate { +public: SafeRefCount refcount; Vector<Variant> array; }; @@ -211,13 +211,13 @@ const Variant &Array::get(int p_idx) const { return operator[](p_idx); } -Array Array::duplicate() const { +Array Array::duplicate(bool p_deep) const { Array new_arr; int element_count = size(); new_arr.resize(element_count); for (int i = 0; i < element_count; i++) { - new_arr[i] = get(i); + new_arr[i] = p_deep ? get(i).duplicate(p_deep) : get(i); } return new_arr; diff --git a/core/array.h b/core/array.h index 684a8e265d..e549a886e6 100644 --- a/core/array.h +++ b/core/array.h @@ -88,7 +88,7 @@ public: Variant pop_back(); Variant pop_front(); - Array duplicate() const; + Array duplicate(bool p_deep = false) const; Array(const Array &p_from); Array(); diff --git a/core/dictionary.cpp b/core/dictionary.cpp index e3f4aa5f28..ba0de95861 100644 --- a/core/dictionary.cpp +++ b/core/dictionary.cpp @@ -211,7 +211,7 @@ const Variant *Dictionary::next(const Variant *p_key) const { return NULL; } -Dictionary Dictionary::duplicate() const { +Dictionary Dictionary::duplicate(bool p_deep) const { Dictionary n; @@ -219,7 +219,7 @@ Dictionary Dictionary::duplicate() const { get_key_list(&keys); for (List<Variant>::Element *E = keys.front(); E; E = E->next()) { - n[E->get()] = operator[](E->get()); + n[E->get()] = p_deep ? operator[](E->get()).duplicate(p_deep) : operator[](E->get()); } return n; diff --git a/core/dictionary.h b/core/dictionary.h index f001f2d5e1..9eef265d5b 100644 --- a/core/dictionary.h +++ b/core/dictionary.h @@ -75,7 +75,7 @@ public: Array keys() const; Array values() const; - Dictionary duplicate() const; + Dictionary duplicate(bool p_deep = false) const; Dictionary(const Dictionary &p_from); Dictionary(); diff --git a/core/image.cpp b/core/image.cpp index 07e705265d..2ac8ffea56 100644 --- a/core/image.cpp +++ b/core/image.cpp @@ -1077,61 +1077,29 @@ Error Image::generate_mipmaps() { PoolVector<uint8_t>::Write wp = data.write(); - if (next_power_of_2(width) == uint32_t(width) && next_power_of_2(height) == uint32_t(height)) { - //use fast code for powers of 2 - int prev_ofs = 0; - int prev_h = height; - int prev_w = width; + int prev_ofs = 0; + int prev_h = height; + int prev_w = width; - for (int i = 1; i < mmcount; i++) { + for (int i = 1; i < mmcount; i++) { - int ofs, w, h; - _get_mipmap_offset_and_size(i, ofs, w, h); + int ofs, w, h; + _get_mipmap_offset_and_size(i, ofs, w, h); - switch (format) { + switch (format) { - case FORMAT_L8: - case FORMAT_R8: _generate_po2_mipmap<1>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; - case FORMAT_LA8: - case FORMAT_RG8: _generate_po2_mipmap<2>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; - case FORMAT_RGB8: _generate_po2_mipmap<3>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; - case FORMAT_RGBA8: _generate_po2_mipmap<4>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; - default: {} - } - - prev_ofs = ofs; - prev_w = w; - prev_h = h; + case FORMAT_L8: + case FORMAT_R8: _generate_po2_mipmap<1>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; + case FORMAT_LA8: + case FORMAT_RG8: _generate_po2_mipmap<2>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; + case FORMAT_RGB8: _generate_po2_mipmap<3>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; + case FORMAT_RGBA8: _generate_po2_mipmap<4>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h); break; + default: {} } - } else { - //use slow code.. - - //use bilinear filtered code for non powers of 2 - int prev_ofs = 0; - int prev_h = height; - int prev_w = width; - - for (int i = 1; i < mmcount; i++) { - - int ofs, w, h; - _get_mipmap_offset_and_size(i, ofs, w, h); - - switch (format) { - - case FORMAT_L8: - case FORMAT_R8: _scale_bilinear<1>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h, w, h); break; - case FORMAT_LA8: - case FORMAT_RG8: _scale_bilinear<2>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h, w, h); break; - case FORMAT_RGB8: _scale_bilinear<3>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h, w, h); break; - case FORMAT_RGBA8: _scale_bilinear<4>(&wp[prev_ofs], &wp[ofs], prev_w, prev_h, w, h); break; - default: {} - } - - prev_ofs = ofs; - prev_w = w; - prev_h = h; - } + prev_ofs = ofs; + prev_w = w; + prev_h = h; } mipmaps = true; @@ -2271,6 +2239,7 @@ void Image::_bind_methods() { ClassDB::bind_method(D_METHOD("premultiply_alpha"), &Image::premultiply_alpha); ClassDB::bind_method(D_METHOD("srgb_to_linear"), &Image::srgb_to_linear); ClassDB::bind_method(D_METHOD("normalmap_to_xy"), &Image::normalmap_to_xy); + ClassDB::bind_method(D_METHOD("bumpmap_to_normalmap", "bump_scale"), &Image::bumpmap_to_normalmap, DEFVAL(1.0)); ClassDB::bind_method(D_METHOD("blit_rect", "src", "src_rect", "dst"), &Image::blit_rect); ClassDB::bind_method(D_METHOD("blit_rect_mask", "src", "mask", "src_rect", "dst"), &Image::blit_rect_mask); @@ -2379,6 +2348,47 @@ void Image::normalmap_to_xy() { convert(Image::FORMAT_LA8); } +void Image::bumpmap_to_normalmap(float bump_scale) { + ERR_FAIL_COND(!_can_modify(format)); + convert(Image::FORMAT_RF); + + PoolVector<uint8_t> result_image; //rgba output + result_image.resize(width * height * 4); + + { + PoolVector<uint8_t>::Read rp = data.read(); + PoolVector<uint8_t>::Write wp = result_image.write(); + + unsigned char *write_ptr = wp.ptr(); + float *read_ptr = (float *)rp.ptr(); + + for (int ty = 0; ty < height; ty++) { + int py = ty + 1; + if (py >= height) py -= height; + + for (int tx = 0; tx < width; tx++) { + int px = tx + 1; + if (px >= width) px -= width; + float here = read_ptr[ty * width + tx]; + float to_right = read_ptr[ty * width + px]; + float above = read_ptr[py * width + tx]; + Vector3 up = Vector3(0, 1, (here - above) * bump_scale); + Vector3 across = Vector3(1, 0, (to_right - here) * bump_scale); + + Vector3 normal = across.cross(up); + normal.normalize(); + + write_ptr[((ty * width + tx) << 2) + 0] = (127.5 + normal.x * 127.5); + write_ptr[((ty * width + tx) << 2) + 1] = (127.5 + normal.y * 127.5); + write_ptr[((ty * width + tx) << 2) + 2] = (127.5 + normal.z * 127.5); + write_ptr[((ty * width + tx) << 2) + 3] = 255; + } + } + } + format = FORMAT_RGBA8; + data = result_image; +} + void Image::srgb_to_linear() { if (data.size() == 0) diff --git a/core/image.h b/core/image.h index e962787ae9..17477d88ea 100644 --- a/core/image.h +++ b/core/image.h @@ -284,6 +284,7 @@ public: void premultiply_alpha(); void srgb_to_linear(); void normalmap_to_xy(); + void bumpmap_to_normalmap(float bump_scale = 1.0); void blit_rect(const Ref<Image> &p_src, const Rect2 &p_src_rect, const Point2 &p_dest); void blit_rect_mask(const Ref<Image> &p_src, const Ref<Image> &p_mask, const Rect2 &p_src_rect, const Point2 &p_dest); diff --git a/core/input_map.cpp b/core/input_map.cpp index bd03d61196..ea724d2595 100644 --- a/core/input_map.cpp +++ b/core/input_map.cpp @@ -35,6 +35,8 @@ InputMap *InputMap::singleton = NULL; +int InputMap::ALL_DEVICES = -1; + void InputMap::_bind_methods() { ClassDB::bind_method(D_METHOD("has_action", "action"), &InputMap::has_action); @@ -103,10 +105,10 @@ List<Ref<InputEvent> >::Element *InputMap::_find_event(List<Ref<InputEvent> > &p //if (e.type != Ref<InputEvent>::KEY && e.device != p_event.device) -- unsure about the KEY comparison, why is this here? // continue; - if (e->get_device() != p_event->get_device()) - continue; - if (e->action_match(p_event)) - return E; + int device = e->get_device(); + if (device == ALL_DEVICES || device == p_event->get_device()) + if (e->action_match(p_event)) + return E; } return NULL; diff --git a/core/input_map.h b/core/input_map.h index 84d90f6f2a..9f3c13c2cf 100644 --- a/core/input_map.h +++ b/core/input_map.h @@ -39,6 +39,11 @@ class InputMap : public Object { GDCLASS(InputMap, Object); public: + /** + * A special value used to signify that a given Action can be triggered by any device + */ + static int ALL_DEVICES; + struct Action { int id; List<Ref<InputEvent> > inputs; diff --git a/core/io/http_client.cpp b/core/io/http_client.cpp index 4d72f744e1..c787b7ec4c 100644 --- a/core/io/http_client.cpp +++ b/core/io/http_client.cpp @@ -618,7 +618,27 @@ String HTTPClient::query_string_from_dict(const Dictionary &p_dict) { String query = ""; Array keys = p_dict.keys(); for (int i = 0; i < keys.size(); ++i) { - query += "&" + String(keys[i]).http_escape() + "=" + String(p_dict[keys[i]]).http_escape(); + String encoded_key = String(keys[i]).percent_encode(); + Variant value = p_dict[keys[i]]; + switch (value.get_type()) { + case Variant::ARRAY: { + // Repeat the key with every values + Array values = value; + for (int j = 0; j < values.size(); ++j) { + query += "&" + encoded_key + "=" + String(values[j]).percent_encode(); + } + break; + } + case Variant::NIL: { + // Add the key with no value + query += "&" + encoded_key; + break; + } + default: { + // Add the key-value pair + query += "&" + encoded_key + "=" + String(value).percent_encode(); + } + } } query.erase(0, 1); return query; diff --git a/core/io/multiplayer_api.cpp b/core/io/multiplayer_api.cpp new file mode 100644 index 0000000000..cbe7f87d92 --- /dev/null +++ b/core/io/multiplayer_api.cpp @@ -0,0 +1,722 @@ +#include "core/io/multiplayer_api.h" +#include "core/io/marshalls.h" +#include "scene/main/node.h" + +void MultiplayerAPI::poll() { + + if (!network_peer.is_valid() || network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED) + return; + + network_peer->poll(); + + if (!network_peer.is_valid()) //it's possible that polling might have resulted in a disconnection, so check here + return; + + while (network_peer->get_available_packet_count()) { + + int sender = network_peer->get_packet_peer(); + const uint8_t *packet; + int len; + + Error err = network_peer->get_packet(&packet, len); + if (err != OK) { + ERR_PRINT("Error getting packet!"); + } + + rpc_sender_id = sender; + _process_packet(sender, packet, len); + rpc_sender_id = 0; + + if (!network_peer.is_valid()) { + break; //it's also possible that a packet or RPC caused a disconnection, so also check here + } + } +} + +void MultiplayerAPI::clear() { + connected_peers.clear(); + path_get_cache.clear(); + path_send_cache.clear(); + last_send_cache_id = 1; +} + +void MultiplayerAPI::set_root_node(Node *p_node) { + root_node = p_node; +} + +void MultiplayerAPI::set_network_peer(const Ref<NetworkedMultiplayerPeer> &p_peer) { + + if (network_peer.is_valid()) { + network_peer->disconnect("peer_connected", this, "add_peer"); + network_peer->disconnect("peer_disconnected", this, "del_peer"); + network_peer->disconnect("connection_succeeded", this, "connected_to_server"); + network_peer->disconnect("connection_failed", this, "connection_failed"); + network_peer->disconnect("server_disconnected", this, "server_disconnected"); + clear(); + } + + network_peer = p_peer; + + ERR_EXPLAIN("Supplied NetworkedNetworkPeer must be connecting or connected."); + ERR_FAIL_COND(p_peer.is_valid() && p_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED); + + if (network_peer.is_valid()) { + network_peer->connect("peer_connected", this, "add_peer"); + network_peer->connect("peer_disconnected", this, "del_peer"); + network_peer->connect("connection_succeeded", this, "connected_to_server"); + network_peer->connect("connection_failed", this, "connection_failed"); + network_peer->connect("server_disconnected", this, "server_disconnected"); + } +} + +Ref<NetworkedMultiplayerPeer> MultiplayerAPI::get_network_peer() const { + return network_peer; +} + +void MultiplayerAPI::_process_packet(int p_from, const uint8_t *p_packet, int p_packet_len) { + + ERR_FAIL_COND(root_node == NULL); + ERR_FAIL_COND(p_packet_len < 5); + + uint8_t packet_type = p_packet[0]; + + switch (packet_type) { + + case NETWORK_COMMAND_SIMPLIFY_PATH: { + + _process_simplify_path(p_from, p_packet, p_packet_len); + } break; + + case NETWORK_COMMAND_CONFIRM_PATH: { + + _process_confirm_path(p_from, p_packet, p_packet_len); + } break; + + case NETWORK_COMMAND_REMOTE_CALL: + case NETWORK_COMMAND_REMOTE_SET: { + + ERR_FAIL_COND(p_packet_len < 6); + + Node *node = _process_get_node(p_from, p_packet, p_packet_len); + + ERR_FAIL_COND(node == NULL); + + //detect cstring end + int len_end = 5; + for (; len_end < p_packet_len; len_end++) { + if (p_packet[len_end] == 0) { + break; + } + } + + ERR_FAIL_COND(len_end >= p_packet_len); + + StringName name = String::utf8((const char *)&p_packet[5]); + + if (packet_type == NETWORK_COMMAND_REMOTE_CALL) { + + _process_rpc(node, name, p_from, p_packet, p_packet_len, len_end + 1); + + } else { + + _process_rset(node, name, p_from, p_packet, p_packet_len, len_end + 1); + } + + } break; + } +} + +Node *MultiplayerAPI::_process_get_node(int p_from, const uint8_t *p_packet, int p_packet_len) { + + uint32_t target = decode_uint32(&p_packet[1]); + Node *node = NULL; + + if (target & 0x80000000) { + //use full path (not cached yet) + + int ofs = target & 0x7FFFFFFF; + ERR_FAIL_COND_V(ofs >= p_packet_len, NULL); + + String paths; + paths.parse_utf8((const char *)&p_packet[ofs], p_packet_len - ofs); + + NodePath np = paths; + + node = root_node->get_node(np); + + if (!node) + ERR_PRINTS("Failed to get path from RPC: " + String(np)); + } else { + //use cached path + int id = target; + + Map<int, PathGetCache>::Element *E = path_get_cache.find(p_from); + ERR_FAIL_COND_V(!E, NULL); + + Map<int, PathGetCache::NodeInfo>::Element *F = E->get().nodes.find(id); + ERR_FAIL_COND_V(!F, NULL); + + PathGetCache::NodeInfo *ni = &F->get(); + //do proper caching later + + node = root_node->get_node(ni->path); + if (!node) + ERR_PRINTS("Failed to get cached path from RPC: " + String(ni->path)); + } + return node; +} + +void MultiplayerAPI::_process_rpc(Node *p_node, const StringName &p_name, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset) { + if (!p_node->can_call_rpc(p_name, p_from)) + return; + + ERR_FAIL_COND(p_offset >= p_packet_len); + + int argc = p_packet[p_offset]; + Vector<Variant> args; + Vector<const Variant *> argp; + args.resize(argc); + argp.resize(argc); + + p_offset++; + + for (int i = 0; i < argc; i++) { + + ERR_FAIL_COND(p_offset >= p_packet_len); + int vlen; + Error err = decode_variant(args[i], &p_packet[p_offset], p_packet_len - p_offset, &vlen); + ERR_FAIL_COND(err != OK); + //args[i]=p_packet[3+i]; + argp[i] = &args[i]; + p_offset += vlen; + } + + Variant::CallError ce; + + p_node->call(p_name, (const Variant **)argp.ptr(), argc, ce); + if (ce.error != Variant::CallError::CALL_OK) { + String error = Variant::get_call_error_text(p_node, p_name, (const Variant **)argp.ptr(), argc, ce); + error = "RPC - " + error; + ERR_PRINTS(error); + } +} + +void MultiplayerAPI::_process_rset(Node *p_node, const StringName &p_name, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset) { + + if (!p_node->can_call_rset(p_name, p_from)) + return; + + ERR_FAIL_COND(p_offset >= p_packet_len); + + Variant value; + decode_variant(value, &p_packet[p_offset], p_packet_len - p_offset); + + bool valid; + + p_node->set(p_name, value, &valid); + if (!valid) { + String error = "Error setting remote property '" + String(p_name) + "', not found in object of type " + p_node->get_class(); + ERR_PRINTS(error); + } +} + +void MultiplayerAPI::_process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len) { + + ERR_FAIL_COND(p_packet_len < 5); + int id = decode_uint32(&p_packet[1]); + + String paths; + paths.parse_utf8((const char *)&p_packet[5], p_packet_len - 5); + + NodePath path = paths; + + if (!path_get_cache.has(p_from)) { + path_get_cache[p_from] = PathGetCache(); + } + + PathGetCache::NodeInfo ni; + ni.path = path; + ni.instance = 0; + + path_get_cache[p_from].nodes[id] = ni; + + //send ack + + //encode path + CharString pname = String(path).utf8(); + int len = encode_cstring(pname.get_data(), NULL); + + Vector<uint8_t> packet; + + packet.resize(1 + len); + packet[0] = NETWORK_COMMAND_CONFIRM_PATH; + encode_cstring(pname.get_data(), &packet[1]); + + network_peer->set_transfer_mode(NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); + network_peer->set_target_peer(p_from); + network_peer->put_packet(packet.ptr(), packet.size()); +} + +void MultiplayerAPI::_process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len) { + + String paths; + paths.parse_utf8((const char *)&p_packet[1], p_packet_len - 1); + + NodePath path = paths; + + PathSentCache *psc = path_send_cache.getptr(path); + ERR_FAIL_COND(!psc); + + Map<int, bool>::Element *E = psc->confirmed_peers.find(p_from); + ERR_FAIL_COND(!E); + E->get() = true; +} + +bool MultiplayerAPI::_send_confirm_path(NodePath p_path, PathSentCache *psc, int p_target) { + bool has_all_peers = true; + List<int> peers_to_add; //if one is missing, take note to add it + + for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { + + if (p_target < 0 && E->get() == -p_target) + continue; //continue, excluded + + if (p_target > 0 && E->get() != p_target) + continue; //continue, not for this peer + + Map<int, bool>::Element *F = psc->confirmed_peers.find(E->get()); + + if (!F || F->get() == false) { + //path was not cached, or was cached but is unconfirmed + if (!F) { + //not cached at all, take note + peers_to_add.push_back(E->get()); + } + + has_all_peers = false; + } + } + + //those that need to be added, send a message for this + + for (List<int>::Element *E = peers_to_add.front(); E; E = E->next()) { + + //encode function name + CharString pname = String(p_path).utf8(); + int len = encode_cstring(pname.get_data(), NULL); + + Vector<uint8_t> packet; + + packet.resize(1 + 4 + len); + packet[0] = NETWORK_COMMAND_SIMPLIFY_PATH; + encode_uint32(psc->id, &packet[1]); + encode_cstring(pname.get_data(), &packet[5]); + + network_peer->set_target_peer(E->get()); //to all of you + network_peer->set_transfer_mode(NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); + network_peer->put_packet(packet.ptr(), packet.size()); + + psc->confirmed_peers.insert(E->get(), false); //insert into confirmed, but as false since it was not confirmed + } + + return has_all_peers; +} + +void MultiplayerAPI::_send_rpc(Node *p_from, int p_to, bool p_unreliable, bool p_set, const StringName &p_name, const Variant **p_arg, int p_argcount) { + + if (network_peer.is_null()) { + ERR_EXPLAIN("Attempt to remote call/set when networking is not active in SceneTree."); + ERR_FAIL(); + } + + if (network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_CONNECTING) { + ERR_EXPLAIN("Attempt to remote call/set when networking is not connected yet in SceneTree."); + ERR_FAIL(); + } + + if (network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED) { + ERR_EXPLAIN("Attempt to remote call/set when networking is disconnected."); + ERR_FAIL(); + } + + if (p_argcount > 255) { + ERR_EXPLAIN("Too many arguments >255."); + ERR_FAIL(); + } + + if (p_to != 0 && !connected_peers.has(ABS(p_to))) { + if (p_to == network_peer->get_unique_id()) { + ERR_EXPLAIN("Attempt to remote call/set yourself! unique ID: " + itos(network_peer->get_unique_id())); + } else { + ERR_EXPLAIN("Attempt to remote call unexisting ID: " + itos(p_to)); + } + + ERR_FAIL(); + } + + NodePath from_path = (root_node->get_path()).rel_path_to(p_from->get_path()); + ERR_FAIL_COND(from_path.is_empty()); + + //see if the path is cached + PathSentCache *psc = path_send_cache.getptr(from_path); + if (!psc) { + //path is not cached, create + path_send_cache[from_path] = PathSentCache(); + psc = path_send_cache.getptr(from_path); + psc->id = last_send_cache_id++; + } + + //create base packet, lots of hardcode because it must be tight + + int ofs = 0; + +#define MAKE_ROOM(m_amount) \ + if (packet_cache.size() < m_amount) packet_cache.resize(m_amount); + + //encode type + MAKE_ROOM(1); + packet_cache[0] = p_set ? NETWORK_COMMAND_REMOTE_SET : NETWORK_COMMAND_REMOTE_CALL; + ofs += 1; + + //encode ID + MAKE_ROOM(ofs + 4); + encode_uint32(psc->id, &(packet_cache[ofs])); + ofs += 4; + + //encode function name + CharString name = String(p_name).utf8(); + int len = encode_cstring(name.get_data(), NULL); + MAKE_ROOM(ofs + len); + encode_cstring(name.get_data(), &(packet_cache[ofs])); + ofs += len; + + if (p_set) { + //set argument + Error err = encode_variant(*p_arg[0], NULL, len); + ERR_FAIL_COND(err != OK); + MAKE_ROOM(ofs + len); + encode_variant(*p_arg[0], &(packet_cache[ofs]), len); + ofs += len; + + } else { + //call arguments + MAKE_ROOM(ofs + 1); + packet_cache[ofs] = p_argcount; + ofs += 1; + for (int i = 0; i < p_argcount; i++) { + Error err = encode_variant(*p_arg[i], NULL, len); + ERR_FAIL_COND(err != OK); + MAKE_ROOM(ofs + len); + encode_variant(*p_arg[i], &(packet_cache[ofs]), len); + ofs += len; + } + } + + //see if all peers have cached path (is so, call can be fast) + bool has_all_peers = _send_confirm_path(from_path, psc, p_to); + + //take chance and set transfer mode, since all send methods will use it + network_peer->set_transfer_mode(p_unreliable ? NetworkedMultiplayerPeer::TRANSFER_MODE_UNRELIABLE : NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); + + if (has_all_peers) { + + //they all have verified paths, so send fast + network_peer->set_target_peer(p_to); //to all of you + network_peer->put_packet(packet_cache.ptr(), ofs); //a message with love + } else { + //not all verified path, so send one by one + + //apend path at the end, since we will need it for some packets + CharString pname = String(from_path).utf8(); + int path_len = encode_cstring(pname.get_data(), NULL); + MAKE_ROOM(ofs + path_len); + encode_cstring(pname.get_data(), &(packet_cache[ofs])); + + for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { + + if (p_to < 0 && E->get() == -p_to) + continue; //continue, excluded + + if (p_to > 0 && E->get() != p_to) + continue; //continue, not for this peer + + Map<int, bool>::Element *F = psc->confirmed_peers.find(E->get()); + ERR_CONTINUE(!F); //should never happen + + network_peer->set_target_peer(E->get()); //to this one specifically + + if (F->get() == true) { + //this one confirmed path, so use id + encode_uint32(psc->id, &(packet_cache[1])); + network_peer->put_packet(packet_cache.ptr(), ofs); + } else { + //this one did not confirm path yet, so use entire path (sorry!) + encode_uint32(0x80000000 | ofs, &(packet_cache[1])); //offset to path and flag + network_peer->put_packet(packet_cache.ptr(), ofs + path_len); + } + } + } +} + +void MultiplayerAPI::add_peer(int p_id) { + connected_peers.insert(p_id); + path_get_cache.insert(p_id, PathGetCache()); + emit_signal("network_peer_connected", p_id); +} + +void MultiplayerAPI::del_peer(int p_id) { + connected_peers.erase(p_id); + path_get_cache.erase(p_id); //I no longer need your cache, sorry + emit_signal("network_peer_disconnected", p_id); +} + +void MultiplayerAPI::connected_to_server() { + + emit_signal("connected_to_server"); +} + +void MultiplayerAPI::connection_failed() { + + emit_signal("connection_failed"); +} + +void MultiplayerAPI::server_disconnected() { + + emit_signal("server_disconnected"); +} + +bool _should_call_native(Node::RPCMode mode, bool is_master, bool &r_skip_rpc) { + + switch (mode) { + + case Node::RPC_MODE_DISABLED: { + //do nothing + } break; + case Node::RPC_MODE_REMOTE: { + //do nothing also, no need to call local + } break; + case Node::RPC_MODE_SYNC: { + //call it, sync always results in call + return true; + } break; + case Node::RPC_MODE_MASTER: { + if (is_master) + r_skip_rpc = true; //no other master so.. + return is_master; + } break; + case Node::RPC_MODE_SLAVE: { + return !is_master; + } break; + } + return false; +} + +bool _should_call_script(ScriptInstance::RPCMode mode, bool is_master, bool &r_skip_rpc) { + switch (mode) { + + case ScriptInstance::RPC_MODE_DISABLED: { + //do nothing + } break; + case ScriptInstance::RPC_MODE_REMOTE: { + //do nothing also, no need to call local + } break; + case ScriptInstance::RPC_MODE_SYNC: { + //call it, sync always results in call + return true; + } break; + case ScriptInstance::RPC_MODE_MASTER: { + if (is_master) + r_skip_rpc = true; //no other master so.. + return is_master; + } break; + case ScriptInstance::RPC_MODE_SLAVE: { + return !is_master; + } break; + } + return false; +} + +void MultiplayerAPI::rpcp(Node *p_node, int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount) { + + ERR_FAIL_COND(!p_node->is_inside_tree()); + ERR_FAIL_COND(!network_peer.is_valid()); + + int node_id = network_peer->get_unique_id(); + bool skip_rpc = false; + bool call_local_native = false; + bool call_local_script = false; + bool is_master = p_node->is_network_master(); + + if (p_peer_id == 0 || p_peer_id == node_id || (p_peer_id < 0 && p_peer_id != -node_id)) { + //check that send mode can use local call + + const Map<StringName, Node::RPCMode>::Element *E = p_node->get_node_rpc_mode(p_method); + if (E) { + call_local_native = _should_call_native(E->get(), is_master, skip_rpc); + } + + if (call_local_native) { + // done below + } else if (p_node->get_script_instance()) { + //attempt with script + ScriptInstance::RPCMode rpc_mode = p_node->get_script_instance()->get_rpc_mode(p_method); + call_local_script = _should_call_script(rpc_mode, is_master, skip_rpc); + } + } + + if (!skip_rpc) { + _send_rpc(p_node, p_peer_id, p_unreliable, false, p_method, p_arg, p_argcount); + } + + if (call_local_native) { + Variant::CallError ce; + p_node->call(p_method, p_arg, p_argcount, ce); + if (ce.error != Variant::CallError::CALL_OK) { + String error = Variant::get_call_error_text(p_node, p_method, p_arg, p_argcount, ce); + error = "rpc() aborted in local call: - " + error; + ERR_PRINTS(error); + return; + } + } + + if (call_local_script) { + Variant::CallError ce; + ce.error = Variant::CallError::CALL_OK; + p_node->get_script_instance()->call(p_method, p_arg, p_argcount, ce); + if (ce.error != Variant::CallError::CALL_OK) { + String error = Variant::get_call_error_text(p_node, p_method, p_arg, p_argcount, ce); + error = "rpc() aborted in script local call: - " + error; + ERR_PRINTS(error); + return; + } + } +} + +void MultiplayerAPI::rsetp(Node *p_node, int p_peer_id, bool p_unreliable, const StringName &p_property, const Variant &p_value) { + + ERR_FAIL_COND(!p_node->is_inside_tree()); + ERR_FAIL_COND(!network_peer.is_valid()); + + int node_id = network_peer->get_unique_id(); + bool is_master = p_node->is_network_master(); + bool skip_rset = false; + + if (p_peer_id == 0 || p_peer_id == node_id || (p_peer_id < 0 && p_peer_id != -node_id)) { + //check that send mode can use local call + + bool set_local = false; + + const Map<StringName, Node::RPCMode>::Element *E = p_node->get_node_rset_mode(p_property); + if (E) { + + set_local = _should_call_native(E->get(), is_master, skip_rset); + } + + if (set_local) { + bool valid; + p_node->set(p_property, p_value, &valid); + + if (!valid) { + String error = "rset() aborted in local set, property not found: - " + String(p_property); + ERR_PRINTS(error); + return; + } + } else if (p_node->get_script_instance()) { + //attempt with script + ScriptInstance::RPCMode rpc_mode = p_node->get_script_instance()->get_rset_mode(p_property); + + set_local = _should_call_script(rpc_mode, is_master, skip_rset); + + if (set_local) { + + bool valid = p_node->get_script_instance()->set(p_property, p_value); + + if (!valid) { + String error = "rset() aborted in local script set, property not found: - " + String(p_property); + ERR_PRINTS(error); + return; + } + } + } + } + + if (skip_rset) + return; + + const Variant *vptr = &p_value; + + _send_rpc(p_node, p_peer_id, p_unreliable, true, p_property, &vptr, 1); +} + +int MultiplayerAPI::get_network_unique_id() const { + + ERR_FAIL_COND_V(!network_peer.is_valid(), 0); + return network_peer->get_unique_id(); +} + +bool MultiplayerAPI::is_network_server() const { + + ERR_FAIL_COND_V(!network_peer.is_valid(), false); + return network_peer->is_server(); +} + +void MultiplayerAPI::set_refuse_new_network_connections(bool p_refuse) { + + ERR_FAIL_COND(!network_peer.is_valid()); + network_peer->set_refuse_new_connections(p_refuse); +} + +bool MultiplayerAPI::is_refusing_new_network_connections() const { + + ERR_FAIL_COND_V(!network_peer.is_valid(), false); + return network_peer->is_refusing_new_connections(); +} + +Vector<int> MultiplayerAPI::get_network_connected_peers() const { + + ERR_FAIL_COND_V(!network_peer.is_valid(), Vector<int>()); + + Vector<int> ret; + for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { + ret.push_back(E->get()); + } + + return ret; +} + +void MultiplayerAPI::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_root_node", "node"), &MultiplayerAPI::set_root_node); + ClassDB::bind_method(D_METHOD("has_network_peer"), &MultiplayerAPI::has_network_peer); + ClassDB::bind_method(D_METHOD("get_network_peer"), &MultiplayerAPI::get_network_peer); + ClassDB::bind_method(D_METHOD("get_network_unique_id"), &MultiplayerAPI::get_network_unique_id); + ClassDB::bind_method(D_METHOD("is_network_server"), &MultiplayerAPI::is_network_server); + ClassDB::bind_method(D_METHOD("get_rpc_sender_id"), &MultiplayerAPI::get_rpc_sender_id); + ClassDB::bind_method(D_METHOD("add_peer", "id"), &MultiplayerAPI::add_peer); + ClassDB::bind_method(D_METHOD("del_peer", "id"), &MultiplayerAPI::del_peer); + ClassDB::bind_method(D_METHOD("set_network_peer", "peer"), &MultiplayerAPI::set_network_peer); + ClassDB::bind_method(D_METHOD("poll"), &MultiplayerAPI::poll); + ClassDB::bind_method(D_METHOD("clear"), &MultiplayerAPI::clear); + + ClassDB::bind_method(D_METHOD("connected_to_server"), &MultiplayerAPI::connected_to_server); + ClassDB::bind_method(D_METHOD("connection_failed"), &MultiplayerAPI::connection_failed); + ClassDB::bind_method(D_METHOD("server_disconnected"), &MultiplayerAPI::server_disconnected); + ClassDB::bind_method(D_METHOD("get_network_connected_peers"), &MultiplayerAPI::get_network_connected_peers); + ClassDB::bind_method(D_METHOD("set_refuse_new_network_connections", "refuse"), &MultiplayerAPI::set_refuse_new_network_connections); + ClassDB::bind_method(D_METHOD("is_refusing_new_network_connections"), &MultiplayerAPI::is_refusing_new_network_connections); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_network_connections"), "set_refuse_new_network_connections", "is_refusing_new_network_connections"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "network_peer", PROPERTY_HINT_RESOURCE_TYPE, "NetworkedMultiplayerPeer", 0), "set_network_peer", "get_network_peer"); + + ADD_SIGNAL(MethodInfo("network_peer_connected", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("network_peer_disconnected", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("connected_to_server")); + ADD_SIGNAL(MethodInfo("connection_failed")); + ADD_SIGNAL(MethodInfo("server_disconnected")); +} + +MultiplayerAPI::MultiplayerAPI() { + clear(); +} + +MultiplayerAPI::~MultiplayerAPI() { + clear(); +} diff --git a/core/io/multiplayer_api.h b/core/io/multiplayer_api.h new file mode 100644 index 0000000000..e7c6ffbea6 --- /dev/null +++ b/core/io/multiplayer_api.h @@ -0,0 +1,87 @@ +#ifndef MULTIPLAYER_PROTOCOL_H +#define MULTIPLAYER_PROTOCOL_H + +#include "core/io/networked_multiplayer_peer.h" +#include "core/reference.h" + +class MultiplayerAPI : public Reference { + + GDCLASS(MultiplayerAPI, Reference); + +private: + //path sent caches + struct PathSentCache { + Map<int, bool> confirmed_peers; + int id; + }; + + //path get caches + struct PathGetCache { + struct NodeInfo { + NodePath path; + ObjectID instance; + }; + + Map<int, NodeInfo> nodes; + }; + + Ref<NetworkedMultiplayerPeer> network_peer; + int rpc_sender_id; + Set<int> connected_peers; + HashMap<NodePath, PathSentCache> path_send_cache; + Map<int, PathGetCache> path_get_cache; + int last_send_cache_id; + Vector<uint8_t> packet_cache; + Node *root_node; + +protected: + static void _bind_methods(); + + void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len); + void _process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len); + void _process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len); + Node *_process_get_node(int p_from, const uint8_t *p_packet, int p_packet_len); + void _process_rpc(Node *p_node, const StringName &p_name, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset); + void _process_rset(Node *p_node, const StringName &p_name, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset); + + void _send_rpc(Node *p_from, int p_to, bool p_unreliable, bool p_set, const StringName &p_name, const Variant **p_arg, int p_argcount); + bool _send_confirm_path(NodePath p_path, PathSentCache *psc, int p_from); + +public: + enum NetworkCommands { + NETWORK_COMMAND_REMOTE_CALL, + NETWORK_COMMAND_REMOTE_SET, + NETWORK_COMMAND_SIMPLIFY_PATH, + NETWORK_COMMAND_CONFIRM_PATH, + }; + + void poll(); + void clear(); + void set_root_node(Node *p_node); + void set_network_peer(const Ref<NetworkedMultiplayerPeer> &p_peer); + Ref<NetworkedMultiplayerPeer> get_network_peer() const; + + // Called by Node.rpc + void rpcp(Node *p_node, int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount); + // Called by Node.rset + void rsetp(Node *p_node, int p_peer_id, bool p_unreliable, const StringName &p_property, const Variant &p_value); + + void add_peer(int p_id); + void del_peer(int p_id); + void connected_to_server(); + void connection_failed(); + void server_disconnected(); + + bool has_network_peer() const { return network_peer.is_valid(); } + Vector<int> get_network_connected_peers() const; + int get_rpc_sender_id() const { return rpc_sender_id; } + int get_network_unique_id() const; + bool is_network_server() const; + void set_refuse_new_network_connections(bool p_refuse); + bool is_refusing_new_network_connections() const; + + MultiplayerAPI(); + ~MultiplayerAPI(); +}; + +#endif // MULTIPLAYER_PROTOCOL_H diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index 5dfe067902..0c626c197b 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -1162,9 +1162,11 @@ Error ResourceFormatLoaderBinary::rename_dependencies(const String &p_path, cons ERR_FAIL_V(ERR_FILE_UNRECOGNIZED); } - fw->store_32(VERSION_MAJOR); //current version - fw->store_32(VERSION_MINOR); - fw->store_32(FORMAT_VERSION); + // Since we're not actually converting the file contents, leave the version + // numbers in the file untouched. + fw->store_32(ver_major); + fw->store_32(ver_minor); + fw->store_32(ver_format); save_ustring(fw, get_ustring(f)); //type diff --git a/core/math/geometry.h b/core/math/geometry.h index ca4363e129..73a53c53b6 100644 --- a/core/math/geometry.h +++ b/core/math/geometry.h @@ -502,16 +502,15 @@ public: } static bool is_point_in_triangle(const Vector2 &s, const Vector2 &a, const Vector2 &b, const Vector2 &c) { - int as_x = s.x - a.x; - int as_y = s.y - a.y; + Vector2 an = a - s; + Vector2 bn = b - s; + Vector2 cn = c - s; - bool s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 0; + bool orientation = an.cross(bn) > 0; - if (((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0) == s_ab) return false; + if ((bn.cross(cn) > 0) != orientation) return false; - if (((c.x - b.x) * (s.y - b.y) - (c.y - b.y) * (s.x - b.x) > 0) != s_ab) return false; - - return true; + return (cn.cross(an) > 0) == orientation; } static bool is_point_in_polygon(const Vector2 &p_point, const Vector<Vector2> &p_polygon); diff --git a/core/os/input.cpp b/core/os/input.cpp index 3089ab2ce3..1d7cd7c791 100644 --- a/core/os/input.cpp +++ b/core/os/input.cpp @@ -111,7 +111,7 @@ void Input::_bind_methods() { BIND_ENUM_CONSTANT(CURSOR_HSPLIT); BIND_ENUM_CONSTANT(CURSOR_HELP); - ADD_SIGNAL(MethodInfo("joy_connection_changed", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "connected"))); + ADD_SIGNAL(MethodInfo("joy_connection_changed", PropertyInfo(Variant::INT, "device"), PropertyInfo(Variant::BOOL, "connected"))); } void Input::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const { diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 006459c5f6..2a611ccf6a 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -42,6 +42,7 @@ #include "io/config_file.h" #include "io/http_client.h" #include "io/marshalls.h" +#include "io/multiplayer_api.h" #include "io/networked_multiplayer_peer.h" #include "io/packet_peer.h" #include "io/packet_peer_udp.h" @@ -145,6 +146,7 @@ void register_core_types() { ClassDB::register_virtual_class<PacketPeer>(); ClassDB::register_class<PacketPeerStream>(); ClassDB::register_virtual_class<NetworkedMultiplayerPeer>(); + ClassDB::register_class<MultiplayerAPI>(); ClassDB::register_class<MainLoop>(); //ClassDB::register_type<OptimizedSaver>(); ClassDB::register_class<Translation>(); diff --git a/core/resource.cpp b/core/resource.cpp index 2eeed50d9d..179333aa14 100644 --- a/core/resource.cpp +++ b/core/resource.cpp @@ -226,7 +226,7 @@ Ref<Resource> Resource::duplicate(bool p_subresources) const { if (!(E->get().usage & PROPERTY_USAGE_STORAGE)) continue; - Variant p = get(E->get().name); + Variant p = get(E->get().name).duplicate(true); if (p.get_type() == Variant::OBJECT && p_subresources) { RES sr = p; diff --git a/core/ustring.cpp b/core/ustring.cpp index a7a7810837..d749146998 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -3165,11 +3165,11 @@ String String::word_wrap(int p_chars_per_line) const { return ret; } -String String::http_escape() const { +String String::percent_encode() const { const CharString temp = utf8(); String res; - for (int i = 0; i < length(); ++i) { - CharType ord = temp[i]; + for (int i = 0; i < temp.length(); ++i) { + char ord = temp[i]; if (ord == '.' || ord == '-' || ord == '_' || ord == '~' || (ord >= 'a' && ord <= 'z') || (ord >= 'A' && ord <= 'Z') || @@ -3178,9 +3178,9 @@ String String::http_escape() const { } else { char h_Val[3]; #if defined(__GNUC__) || defined(_MSC_VER) - snprintf(h_Val, 3, "%.2X", ord); + snprintf(h_Val, 3, "%hhX", ord); #else - sprintf(h_Val, "%.2X", ord); + sprintf(h_Val, "%hhX", ord); #endif res += "%"; res += h_Val; @@ -3189,7 +3189,7 @@ String String::http_escape() const { return res; } -String String::http_unescape() const { +String String::percent_decode() const { String res; for (int i = 0; i < length(); ++i) { if (ord_at(i) == '%' && i + 2 < length()) { @@ -3727,68 +3727,6 @@ String String::plus_file(const String &p_file) const { return *this + "/" + p_file; } -String String::percent_encode() const { - - CharString cs = utf8(); - String encoded; - for (int i = 0; i < cs.length(); i++) { - uint8_t c = cs[i]; - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '~' || c == '.') { - - char p[2] = { (char)c, 0 }; - encoded += p; - } else { - char p[4] = { '%', 0, 0, 0 }; - static const char hex[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - - p[1] = hex[c >> 4]; - p[2] = hex[c & 0xF]; - encoded += p; - } - } - - return encoded; -} -String String::percent_decode() const { - - CharString pe; - - CharString cs = utf8(); - for (int i = 0; i < cs.length(); i++) { - - uint8_t c = cs[i]; - if (c == '%' && i < length() - 2) { - - uint8_t a = LOWERCASE(cs[i + 1]); - uint8_t b = LOWERCASE(cs[i + 2]); - - c = 0; - if (a >= '0' && a <= '9') - c = (a - '0') << 4; - else if (a >= 'a' && a <= 'f') - c = (a - 'a' + 10) << 4; - else - continue; - - uint8_t d = 0; - - if (b >= '0' && b <= '9') - d = (b - '0'); - else if (b >= 'a' && b <= 'f') - d = (b - 'a' + 10); - else - continue; - c += d; - i += 2; - } - pe.push_back(c); - } - - pe.push_back(0); - - return String::utf8(pe.ptr()); -} - String String::get_basename() const { int pos = find_last("."); diff --git a/core/ustring.h b/core/ustring.h index bb676ce623..8023c9b95d 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -226,17 +226,14 @@ public: String xml_escape(bool p_escape_quotes = false) const; String xml_unescape() const; - String http_escape() const; - String http_unescape() const; + String percent_encode() const; + String percent_decode() const; String c_escape() const; String c_escape_multiline() const; String c_unescape() const; String json_escape() const; String word_wrap(int p_chars_per_line) const; - String percent_encode() const; - String percent_decode() const; - bool is_valid_identifier() const; bool is_valid_integer() const; bool is_valid_float() const; diff --git a/core/variant.h b/core/variant.h index 0a4afada5b..2cdb5c9ab6 100644 --- a/core/variant.h +++ b/core/variant.h @@ -338,6 +338,7 @@ public: } void zero(); + Variant duplicate(bool deep = false) const; static void blend(const Variant &a, const Variant &b, float c, Variant &r_dst); static void interpolate(const Variant &a, const Variant &b, float c, Variant &r_dst); diff --git a/core/variant_call.cpp b/core/variant_call.cpp index cda7dccf0c..c6e093010d 100644 --- a/core/variant_call.cpp +++ b/core/variant_call.cpp @@ -465,7 +465,7 @@ struct _VariantCall { VCALL_LOCALMEM0R(Dictionary, hash); VCALL_LOCALMEM0R(Dictionary, keys); VCALL_LOCALMEM0R(Dictionary, values); - VCALL_LOCALMEM0R(Dictionary, duplicate); + VCALL_LOCALMEM1R(Dictionary, duplicate); VCALL_LOCALMEM2(Array, set); VCALL_LOCALMEM1R(Array, get); @@ -494,7 +494,7 @@ struct _VariantCall { VCALL_LOCALMEM0(Array, shuffle); VCALL_LOCALMEM2R(Array, bsearch); VCALL_LOCALMEM4R(Array, bsearch_custom); - VCALL_LOCALMEM0R(Array, duplicate); + VCALL_LOCALMEM1R(Array, duplicate); VCALL_LOCALMEM0(Array, invert); static void _call_PoolByteArray_get_string_from_ascii(Variant &r_ret, Variant &p_self, const Variant **p_args) { @@ -1613,7 +1613,7 @@ void register_variant_methods() { ADDFUNC0R(DICTIONARY, INT, Dictionary, hash, varray()); ADDFUNC0R(DICTIONARY, ARRAY, Dictionary, keys, varray()); ADDFUNC0R(DICTIONARY, ARRAY, Dictionary, values, varray()); - ADDFUNC0R(DICTIONARY, DICTIONARY, Dictionary, duplicate, varray()); + ADDFUNC1R(DICTIONARY, DICTIONARY, Dictionary, duplicate, BOOL, "deep", varray(false)); ADDFUNC0R(ARRAY, INT, Array, size, varray()); ADDFUNC0R(ARRAY, BOOL, Array, empty, varray()); @@ -1641,7 +1641,7 @@ void register_variant_methods() { ADDFUNC2R(ARRAY, INT, Array, bsearch, NIL, "value", BOOL, "before", varray(true)); ADDFUNC4R(ARRAY, INT, Array, bsearch_custom, NIL, "value", OBJECT, "obj", STRING, "func", BOOL, "before", varray(true)); ADDFUNC0NC(ARRAY, NIL, Array, invert, varray()); - ADDFUNC0RNC(ARRAY, ARRAY, Array, duplicate, varray()); + ADDFUNC1R(ARRAY, ARRAY, Array, duplicate, BOOL, "deep", varray(false)); ADDFUNC0R(POOL_BYTE_ARRAY, INT, PoolByteArray, size, varray()); ADDFUNC2(POOL_BYTE_ARRAY, NIL, PoolByteArray, set, INT, "idx", INT, "byte", varray()); diff --git a/core/variant_op.cpp b/core/variant_op.cpp index 842f5f0af6..8ad981ed0e 100644 --- a/core/variant_op.cpp +++ b/core/variant_op.cpp @@ -3415,6 +3415,19 @@ Variant Variant::iter_get(const Variant &r_iter, bool &r_valid) const { return Variant(); } +Variant Variant::duplicate(bool deep) const { + switch (type) { + // case OBJECT: + // return operator Object *()->duplicate(); + case DICTIONARY: + return operator Dictionary().duplicate(deep); + case ARRAY: + return operator Array().duplicate(deep); + default: + return *this; + } +} + void Variant::blend(const Variant &a, const Variant &b, float c, Variant &r_dst) { if (a.type != b.type) { if (a.is_num() && b.is_num()) { diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 178c714a20..ed859169fd 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -103,6 +103,14 @@ Get the blend time (in seconds) between two animations, referenced by their names. </description> </method> + <method name="get_playing_speed" qualifiers="const"> + <return type="float"> + </return> + <description> + Get the actual playing speed of current animation or 0 if not playing. This speed is the [code]playback_speed[/code] property multiplied by [code]custom_speed[/code] argument specified when calling the [code]play[/code] method. + </description> + </method> + <method name="has_animation" qualifiers="const"> <return type="bool"> </return> diff --git a/doc/classes/HTTPClient.xml b/doc/classes/HTTPClient.xml index 5bd7c4cd3e..018b548ef1 100644 --- a/doc/classes/HTTPClient.xml +++ b/doc/classes/HTTPClient.xml @@ -111,6 +111,12 @@ String queryString = httpClient.query_string_from_dict(fields) returns:= "username=user&password=pass" [/codeblock] + Furthermore, if a key has a null value, only the key itself is added, without equal sign and value. If the value is an array, for each value in it a pair with the same key is added. + [codeblock] + var fields = {"single": 123, "not_valued": null, "multiple": [22, 33, 44]} + String queryString = httpClient.query_string_from_dict(fields) + returns:= "single=123&not_valued&multiple=22&multiple=33&multiple=44" + [/codeblock] </description> </method> <method name="read_response_body_chunk"> diff --git a/doc/classes/ItemList.xml b/doc/classes/ItemList.xml index e4db5aeb84..0147887768 100644 --- a/doc/classes/ItemList.xml +++ b/doc/classes/ItemList.xml @@ -87,6 +87,15 @@ <description> </description> </method> + <method name="get_item_icon_modulate" qualifiers="const"> + <return type="Color"> + </return> + <argument index="0" name="idx" type="int"> + </argument> + <description> + Returns a [Color] modulating item's icon at the specified index. + </description> + </method> <method name="get_item_icon_region" qualifiers="const"> <return type="Rect2"> </return> @@ -225,6 +234,17 @@ Set (or replace) icon of the item at the specified index. </description> </method> + <method name="set_item_icon_modulate"> + <return type="void"> + </return> + <argument index="0" name="idx" type="int"> + </argument> + <argument index="1" name="modulate" type="Color"> + </argument> + <description> + Sets a modulating [Color] for item's icon at the specified index. + </description> + </method> <method name="set_item_icon_region"> <return type="void"> </return> diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 70e6fab5b6..6ead220236 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -24,6 +24,19 @@ Add a new checkable item with text "label". An id can optionally be provided, as well as an accelerator. If no id is provided, one will be created from the index. Note that checkable items just display a checkmark, but don't have any built-in checking behavior and must be checked/unchecked manually. </description> </method> + <method name="add_radio_check_item"> + <return type="void"> + </return> + <argument index="0" name="label" type="String"> + </argument> + <argument index="1" name="id" type="int" default="-1"> + </argument> + <argument index="2" name="accel" type="int" default="0"> + </argument> + <description> + The same as [method add_check_item] but the inserted item will look as a radio button. Remember this is just cosmetic and you have to add the logic for checking/unchecking items in radio groups. + </description> + </method> <method name="add_check_shortcut"> <return type="void"> </return> @@ -36,6 +49,18 @@ <description> </description> </method> + <method name="add_radio_check_shortcut"> + <return type="void"> + </return> + <argument index="0" name="shortcut" type="ShortCut"> + </argument> + <argument index="1" name="id" type="int" default="-1"> + </argument> + <argument index="2" name="global" type="bool" default="false"> + </argument> + <description> + </description> + </method> <method name="add_icon_check_item"> <return type="void"> </return> @@ -239,7 +264,16 @@ <argument index="0" name="idx" type="int"> </argument> <description> - Return whether the item at index "idx" has a checkbox. Note that checkable items just display a checkmark, but don't have any built-in checking behavior and must be checked/unchecked manually. + Return whether the item at index "idx" is checkable in some way, i.e., whether has a checkbox or radio button. Note that checkable items just display a checkmark or radio button, but don't have any built-in checking behavior and must be checked/unchecked manually. + </description> + </method> + <method name="is_item_radio_checkable" qualifiers="const"> + <return type="bool"> + </return> + <argument index="0" name="idx" type="int"> + </argument> + <description> + Return whether the item at index "idx" has radio-button-style checkability. Remember this is just cosmetic and you have to add the logic for checking/unchecking items in radio groups. </description> </method> <method name="is_item_checked" qualifiers="const"> @@ -248,7 +282,7 @@ <argument index="0" name="idx" type="int"> </argument> <description> - Return the checkstate status of the item at index "idx". + Return whether the item at index "idx" is checked. </description> </method> <method name="is_item_disabled" qualifiers="const"> @@ -300,6 +334,18 @@ Set whether the item at index "idx" has a checkbox. Note that checkable items just display a checkmark, but don't have any built-in checking behavior and must be checked/unchecked manually. </description> </method> + <method name="set_item_as_radio_checkable"> + <return type="void"> + </return> + <argument index="0" name="idx" type="int"> + </argument> + <argument index="1" name="enable" type="bool"> + </argument> + <description> + The same as [method set_item_as_checkable] but placing a radio button in case of enabling. If used for disabling, it's the same. + Remember this is just cosmetic and you have to add the logic for checking/unchecking items in radio groups. + </description> + </method> <method name="set_item_as_separator"> <return type="void"> </return> diff --git a/doc/classes/SurfaceTool.xml b/doc/classes/SurfaceTool.xml index d8644c5419..7d78d71330 100644 --- a/doc/classes/SurfaceTool.xml +++ b/doc/classes/SurfaceTool.xml @@ -196,8 +196,11 @@ <method name="generate_normals"> <return type="void"> </return> + <argument index="0" name="flip" type="bool" default="false"> + </argument> <description> Generates normals from Vertices so you do not have to do it manually. + Setting "flip" [code]true[/code] inverts resulting normals. </description> </method> <method name="generate_tangents"> diff --git a/drivers/alsa/audio_driver_alsa.cpp b/drivers/alsa/audio_driver_alsa.cpp index 0bb03d23ea..1e17e72532 100644 --- a/drivers/alsa/audio_driver_alsa.cpp +++ b/drivers/alsa/audio_driver_alsa.cpp @@ -37,19 +37,20 @@ #include <errno.h> -Error AudioDriverALSA::init() { - - active = false; - thread_exited = false; - exit_thread = false; - pcm_open = false; - samples_in = NULL; - samples_out = NULL; - +Error AudioDriverALSA::init_device() { mix_rate = GLOBAL_DEF("audio/mix_rate", DEFAULT_MIX_RATE); speaker_mode = SPEAKER_MODE_STEREO; channels = 2; + // If there is a specified device check that it is really present + if (device_name != "Default") { + Array list = get_device_list(); + if (list.find(device_name) == -1) { + device_name = "Default"; + new_device = "Default"; + } + } + int status; snd_pcm_hw_params_t *hwparams; snd_pcm_sw_params_t *swparams; @@ -65,7 +66,16 @@ Error AudioDriverALSA::init() { //6 chans - "plug:surround51" //4 chans - "plug:surround40"; - status = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (device_name == "Default") { + status = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + } else { + String device = device_name; + int pos = device.find(";"); + if (pos != -1) { + device = device.substr(0, pos); + } + status = snd_pcm_open(&pcm_handle, device.utf8().get_data(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + } ERR_FAIL_COND_V(status < 0, ERR_CANT_OPEN); @@ -129,15 +139,28 @@ Error AudioDriverALSA::init() { status = snd_pcm_sw_params(pcm_handle, swparams); CHECK_FAIL(status < 0); - samples_in = memnew_arr(int32_t, period_size * channels); - samples_out = memnew_arr(int16_t, period_size * channels); + samples_in.resize(period_size * channels); + samples_out.resize(period_size * channels); snd_pcm_nonblock(pcm_handle, 0); - mutex = Mutex::create(); - thread = Thread::create(AudioDriverALSA::thread_func, this); - return OK; +} + +Error AudioDriverALSA::init() { + + active = false; + thread_exited = false; + exit_thread = false; + pcm_open = false; + + Error err = init_device(); + if (err == OK) { + mutex = Mutex::create(); + thread = Thread::create(AudioDriverALSA::thread_func, this); + } + + return err; }; void AudioDriverALSA::thread_func(void *p_udata) { @@ -152,7 +175,7 @@ void AudioDriverALSA::thread_func(void *p_udata) { } else { ad->lock(); - ad->audio_server_process(ad->period_size, ad->samples_in); + ad->audio_server_process(ad->period_size, ad->samples_in.ptrw()); ad->unlock(); @@ -167,7 +190,7 @@ void AudioDriverALSA::thread_func(void *p_udata) { while (todo) { if (ad->exit_thread) break; - uint8_t *src = (uint8_t *)ad->samples_out; + uint8_t *src = (uint8_t *)ad->samples_out.ptr(); int wrote = snd_pcm_writei(ad->pcm_handle, (void *)(src + (total * ad->channels)), todo); if (wrote < 0) { @@ -193,6 +216,26 @@ void AudioDriverALSA::thread_func(void *p_udata) { total += wrote; todo -= wrote; }; + + // User selected a new device, finish the current one so we'll init the new device + if (ad->device_name != ad->new_device) { + ad->device_name = ad->new_device; + ad->finish_device(); + + Error err = ad->init_device(); + if (err != OK) { + ERR_PRINT("ALSA: init_device error"); + ad->device_name = "Default"; + ad->new_device = "Default"; + + err = ad->init_device(); + if (err != OK) { + ad->active = false; + ad->exit_thread = true; + break; + } + } + } }; ad->thread_exited = true; @@ -213,6 +256,49 @@ AudioDriver::SpeakerMode AudioDriverALSA::get_speaker_mode() const { return speaker_mode; }; +Array AudioDriverALSA::get_device_list() { + + Array list; + + list.push_back("Default"); + + void **hints; + + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return list; + + for (void **n = hints; *n != NULL; n++) { + char *name = snd_device_name_get_hint(*n, "NAME"); + char *desc = snd_device_name_get_hint(*n, "DESC"); + + if (name != NULL && !strncmp(name, "plughw", 6)) { + if (desc) { + list.push_back(String(name) + ";" + String(desc)); + } else { + list.push_back(String(name)); + } + } + + if (desc != NULL) + free(desc); + if (name != NULL) + free(name); + } + snd_device_name_free_hint(hints); + + return list; +} + +String AudioDriverALSA::get_device() { + + return device_name; +} + +void AudioDriverALSA::set_device(String device) { + + new_device = device; +} + void AudioDriverALSA::lock() { if (!thread || !mutex) @@ -227,6 +313,14 @@ void AudioDriverALSA::unlock() { mutex->unlock(); }; +void AudioDriverALSA::finish_device() { + + if (pcm_open) { + snd_pcm_close(pcm_handle); + pcm_open = NULL; + } +} + void AudioDriverALSA::finish() { if (!thread) @@ -235,17 +329,13 @@ void AudioDriverALSA::finish() { exit_thread = true; Thread::wait_to_finish(thread); - if (pcm_open) - snd_pcm_close(pcm_handle); - - if (samples_in) { - memdelete_arr(samples_in); - memdelete_arr(samples_out); - }; + finish_device(); memdelete(thread); - if (mutex) + if (mutex) { memdelete(mutex); + mutex = NULL; + } thread = NULL; }; @@ -254,6 +344,9 @@ AudioDriverALSA::AudioDriverALSA() { mutex = NULL; thread = NULL; pcm_handle = NULL; + + device_name = "Default"; + new_device = "Default"; }; AudioDriverALSA::~AudioDriverALSA(){ diff --git a/drivers/alsa/audio_driver_alsa.h b/drivers/alsa/audio_driver_alsa.h index 8ed60dfdc7..2878e100a2 100644 --- a/drivers/alsa/audio_driver_alsa.h +++ b/drivers/alsa/audio_driver_alsa.h @@ -44,8 +44,14 @@ class AudioDriverALSA : public AudioDriver { snd_pcm_t *pcm_handle; - int32_t *samples_in; - int16_t *samples_out; + String device_name; + String new_device; + + Vector<int32_t> samples_in; + Vector<int16_t> samples_out; + + Error init_device(); + void finish_device(); static void thread_func(void *p_udata); @@ -71,6 +77,9 @@ public: virtual void start(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; + virtual Array get_device_list(); + virtual String get_device(); + virtual void set_device(String device); virtual void lock(); virtual void unlock(); virtual void finish(); diff --git a/drivers/coreaudio/audio_driver_coreaudio.cpp b/drivers/coreaudio/audio_driver_coreaudio.cpp index 0b39f9ebc3..c84469f26f 100644 --- a/drivers/coreaudio/audio_driver_coreaudio.cpp +++ b/drivers/coreaudio/audio_driver_coreaudio.cpp @@ -37,16 +37,22 @@ #define kOutputBus 0 #ifdef OSX_ENABLED -static OSStatus outputDeviceAddressCB(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inClientData) { +OSStatus AudioDriverCoreAudio::output_device_address_cb(AudioObjectID inObjectID, + UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, + void *inClientData) { AudioDriverCoreAudio *driver = (AudioDriverCoreAudio *)inClientData; - driver->reopen(); + // If our selected device is the Default call set_device to update the + // kAudioOutputUnitProperty_CurrentDevice property + if (driver->device_name == "Default") { + driver->set_device("Default"); + } return noErr; } #endif -Error AudioDriverCoreAudio::initDevice() { +Error AudioDriverCoreAudio::init_device() { AudioComponentDescription desc; zeromem(&desc, sizeof(desc)); desc.componentType = kAudioUnitType_Output; @@ -129,7 +135,7 @@ Error AudioDriverCoreAudio::initDevice() { return OK; } -Error AudioDriverCoreAudio::finishDevice() { +Error AudioDriverCoreAudio::finish_device() { OSStatus result; if (active) { @@ -153,49 +159,18 @@ Error AudioDriverCoreAudio::init() { channels = 2; #ifdef OSX_ENABLED - outputDeviceAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; - outputDeviceAddress.mScope = kAudioObjectPropertyScopeGlobal; - outputDeviceAddress.mElement = kAudioObjectPropertyElementMaster; + AudioObjectPropertyAddress prop; + prop.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + prop.mScope = kAudioObjectPropertyScopeGlobal; + prop.mElement = kAudioObjectPropertyElementMaster; - result = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &outputDeviceAddress, &outputDeviceAddressCB, this); + result = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &prop, &output_device_address_cb, this); ERR_FAIL_COND_V(result != noErr, FAILED); #endif - return initDevice(); + return init_device(); }; -Error AudioDriverCoreAudio::reopen() { - bool restart = false; - - lock(); - - if (active) { - restart = true; - } - - Error err = finishDevice(); - if (err != OK) { - ERR_PRINT("finishDevice failed"); - unlock(); - return err; - } - - err = initDevice(); - if (err != OK) { - ERR_PRINT("initDevice failed"); - unlock(); - return err; - } - - if (restart) { - start(); - } - - unlock(); - - return OK; -} - OSStatus AudioDriverCoreAudio::output_callback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, @@ -257,6 +232,150 @@ AudioDriver::SpeakerMode AudioDriverCoreAudio::get_speaker_mode() const { return get_speaker_mode_by_total_channels(channels); }; +#ifdef OSX_ENABLED + +Array AudioDriverCoreAudio::get_device_list() { + + Array list; + + list.push_back("Default"); + + AudioObjectPropertyAddress prop; + + prop.mSelector = kAudioHardwarePropertyDevices; + prop.mScope = kAudioObjectPropertyScopeGlobal; + prop.mElement = kAudioObjectPropertyElementMaster; + + UInt32 size = 0; + AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, NULL, &size); + AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(size); + AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL, &size, audioDevices); + + UInt32 deviceCount = size / sizeof(AudioDeviceID); + for (UInt32 i = 0; i < deviceCount; i++) { + prop.mScope = kAudioDevicePropertyScopeOutput; + prop.mSelector = kAudioDevicePropertyStreamConfiguration; + + AudioObjectGetPropertyDataSize(audioDevices[i], &prop, 0, NULL, &size); + AudioBufferList *bufferList = (AudioBufferList *)malloc(size); + AudioObjectGetPropertyData(audioDevices[i], &prop, 0, NULL, &size, bufferList); + + UInt32 outputChannelCount = 0; + for (UInt32 j = 0; j < bufferList->mNumberBuffers; j++) + outputChannelCount += bufferList->mBuffers[j].mNumberChannels; + + free(bufferList); + + if (outputChannelCount >= 1) { + CFStringRef cfname; + + size = sizeof(CFStringRef); + prop.mSelector = kAudioObjectPropertyName; + + AudioObjectGetPropertyData(audioDevices[i], &prop, 0, NULL, &size, &cfname); + + CFIndex length = CFStringGetLength(cfname); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + char *buffer = (char *)malloc(maxSize); + if (CFStringGetCString(cfname, buffer, maxSize, kCFStringEncodingUTF8)) { + // Append the ID to the name in case we have devices with duplicate name + list.push_back(String(buffer) + " (" + itos(audioDevices[i]) + ")"); + } + + free(buffer); + } + } + + free(audioDevices); + + return list; +} + +String AudioDriverCoreAudio::get_device() { + + return device_name; +} + +void AudioDriverCoreAudio::set_device(String device) { + + device_name = device; + if (!active) { + return; + } + + AudioDeviceID deviceId; + bool found = false; + if (device_name != "Default") { + AudioObjectPropertyAddress prop; + + prop.mSelector = kAudioHardwarePropertyDevices; + prop.mScope = kAudioObjectPropertyScopeGlobal; + prop.mElement = kAudioObjectPropertyElementMaster; + + UInt32 size = 0; + AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, NULL, &size); + AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(size); + AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL, &size, audioDevices); + + UInt32 deviceCount = size / sizeof(AudioDeviceID); + for (UInt32 i = 0; i < deviceCount && !found; i++) { + prop.mScope = kAudioDevicePropertyScopeOutput; + prop.mSelector = kAudioDevicePropertyStreamConfiguration; + + AudioObjectGetPropertyDataSize(audioDevices[i], &prop, 0, NULL, &size); + AudioBufferList *bufferList = (AudioBufferList *)malloc(size); + AudioObjectGetPropertyData(audioDevices[i], &prop, 0, NULL, &size, bufferList); + + UInt32 outputChannelCount = 0; + for (UInt32 j = 0; j < bufferList->mNumberBuffers; j++) + outputChannelCount += bufferList->mBuffers[j].mNumberChannels; + + free(bufferList); + + if (outputChannelCount >= 1) { + CFStringRef cfname; + + size = sizeof(CFStringRef); + prop.mSelector = kAudioObjectPropertyName; + + AudioObjectGetPropertyData(audioDevices[i], &prop, 0, NULL, &size, &cfname); + + CFIndex length = CFStringGetLength(cfname); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + char *buffer = (char *)malloc(maxSize); + if (CFStringGetCString(cfname, buffer, maxSize, kCFStringEncodingUTF8)) { + String name = String(buffer) + " (" + itos(audioDevices[i]) + ")"; + if (name == device_name) { + deviceId = audioDevices[i]; + found = true; + } + } + + free(buffer); + } + } + + free(audioDevices); + } + + if (!found) { + UInt32 size = sizeof(AudioDeviceID); + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + + OSStatus result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &property, 0, NULL, &size, &deviceId); + ERR_FAIL_COND(result != noErr); + + found = true; + } + + if (found) { + OSStatus result = AudioUnitSetProperty(audio_unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &deviceId, sizeof(AudioDeviceID)); + ERR_FAIL_COND(result != noErr); + } +} + +#endif + void AudioDriverCoreAudio::lock() { if (mutex) mutex->lock(); @@ -276,10 +395,15 @@ bool AudioDriverCoreAudio::try_lock() { void AudioDriverCoreAudio::finish() { OSStatus result; - finishDevice(); + finish_device(); #ifdef OSX_ENABLED - result = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &outputDeviceAddress, &outputDeviceAddressCB, this); + AudioObjectPropertyAddress prop; + prop.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + prop.mScope = kAudioObjectPropertyScopeGlobal; + prop.mElement = kAudioObjectPropertyElementMaster; + + result = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &prop, &output_device_address_cb, this); if (result != noErr) { ERR_PRINT("AudioObjectRemovePropertyListener failed"); } @@ -309,6 +433,8 @@ AudioDriverCoreAudio::AudioDriverCoreAudio() { buffer_frames = 0; samples_in.clear(); + + device_name = "Default"; }; AudioDriverCoreAudio::~AudioDriverCoreAudio(){}; diff --git a/drivers/coreaudio/audio_driver_coreaudio.h b/drivers/coreaudio/audio_driver_coreaudio.h index 51256085d8..1ef474ac19 100644 --- a/drivers/coreaudio/audio_driver_coreaudio.h +++ b/drivers/coreaudio/audio_driver_coreaudio.h @@ -43,12 +43,12 @@ class AudioDriverCoreAudio : public AudioDriver { AudioComponentInstance audio_unit; -#ifdef OSX_ENABLED - AudioObjectPropertyAddress outputDeviceAddress; -#endif + bool active; Mutex *mutex; + String device_name; + int mix_rate; unsigned int channels; unsigned int buffer_frames; @@ -56,14 +56,18 @@ class AudioDriverCoreAudio : public AudioDriver { Vector<int32_t> samples_in; + static OSStatus output_device_address_cb(AudioObjectID inObjectID, + UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, + void *inClientData); + static OSStatus output_callback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData); - Error initDevice(); - Error finishDevice(); + Error init_device(); + Error finish_device(); public: const char *get_name() const { @@ -74,12 +78,16 @@ public: virtual void start(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; +#ifdef OSX_ENABLED + virtual Array get_device_list(); + virtual String get_device(); + virtual void set_device(String device); +#endif virtual void lock(); virtual void unlock(); virtual void finish(); bool try_lock(); - Error reopen(); AudioDriverCoreAudio(); ~AudioDriverCoreAudio(); diff --git a/drivers/gles2/shader_compiler_gles2.cpp b/drivers/gles2/shader_compiler_gles2.cpp index f41bfe838b..ad6c2f850a 100644 --- a/drivers/gles2/shader_compiler_gles2.cpp +++ b/drivers/gles2/shader_compiler_gles2.cpp @@ -872,9 +872,9 @@ ShaderCompilerGLES2::ShaderCompilerGLES2() { actions[VS::SHADER_PARTICLES].renames["EMISSION_TRANSFORM"] = "emission_transform"; actions[VS::SHADER_PARTICLES].renames["RANDOM_SEED"] = "random_seed"; - actions[VS::SHADER_SPATIAL].render_mode_defines["disable_force"] = "#define DISABLE_FORCE\n"; - actions[VS::SHADER_SPATIAL].render_mode_defines["disable_velocity"] = "#define DISABLE_VELOCITY\n"; - actions[VS::SHADER_SPATIAL].render_mode_defines["keep_data"] = "#define ENABLE_KEEP_DATA\n"; + actions[VS::SHADER_PARTICLES].render_mode_defines["disable_force"] = "#define DISABLE_FORCE\n"; + actions[VS::SHADER_PARTICLES].render_mode_defines["disable_velocity"] = "#define DISABLE_VELOCITY\n"; + actions[VS::SHADER_PARTICLES].render_mode_defines["keep_data"] = "#define ENABLE_KEEP_DATA\n"; vertex_name = "vertex"; fragment_name = "fragment"; diff --git a/drivers/gles3/rasterizer_storage_gles3.cpp b/drivers/gles3/rasterizer_storage_gles3.cpp index a287dca1ed..f91ed35331 100644 --- a/drivers/gles3/rasterizer_storage_gles3.cpp +++ b/drivers/gles3/rasterizer_storage_gles3.cpp @@ -29,6 +29,7 @@ /*************************************************************************/ #include "rasterizer_storage_gles3.h" +#include "engine.h" #include "project_settings.h" #include "rasterizer_canvas_gles3.h" #include "rasterizer_scene_gles3.h" @@ -5855,6 +5856,8 @@ void RasterizerStorageGLES3::update_particles() { shaders.particles.set_uniform(ParticlesShaderGLES3::EMITTING, particles->emitting); shaders.particles.set_uniform(ParticlesShaderGLES3::RANDOMNESS, particles->randomness); + bool zero_time_scale = Engine::get_singleton()->get_time_scale() <= 0.0; + if (particles->clear && particles->pre_process_time > 0.0) { float frame_time; @@ -5872,7 +5875,15 @@ void RasterizerStorageGLES3::update_particles() { } if (particles->fixed_fps > 0) { - float frame_time = 1.0 / particles->fixed_fps; + float frame_time; + float decr; + if (zero_time_scale) { + frame_time = 0.0; + decr = 1.0 / particles->fixed_fps; + } else { + frame_time = 1.0 / particles->fixed_fps; + decr = frame_time; + } float delta = frame.delta; if (delta > 0.1) { //avoid recursive stalls if fps goes below 10 delta = 0.1; @@ -5883,13 +5894,16 @@ void RasterizerStorageGLES3::update_particles() { while (todo >= frame_time) { _particles_process(particles, frame_time); - todo -= frame_time; + todo -= decr; } particles->frame_remainder = todo; } else { - _particles_process(particles, frame.delta); + if (zero_time_scale) + _particles_process(particles, 0.0); + else + _particles_process(particles, frame.delta); } particle_update_list.remove(particle_update_list.first()); diff --git a/drivers/pulseaudio/audio_driver_pulseaudio.cpp b/drivers/pulseaudio/audio_driver_pulseaudio.cpp index 9639f06345..5dddb1a900 100644 --- a/drivers/pulseaudio/audio_driver_pulseaudio.cpp +++ b/drivers/pulseaudio/audio_driver_pulseaudio.cpp @@ -37,131 +37,102 @@ #include "os/os.h" #include "project_settings.h" -void pa_state_cb(pa_context *c, void *userdata) { - pa_context_state_t state; - int *pa_ready = (int *)userdata; +void AudioDriverPulseAudio::pa_state_cb(pa_context *c, void *userdata) { + AudioDriverPulseAudio *ad = (AudioDriverPulseAudio *)userdata; - state = pa_context_get_state(c); - switch (state) { - case PA_CONTEXT_FAILED: + switch (pa_context_get_state(c)) { case PA_CONTEXT_TERMINATED: - *pa_ready = 2; + case PA_CONTEXT_FAILED: + ad->pa_ready = -1; break; case PA_CONTEXT_READY: - *pa_ready = 1; + ad->pa_ready = 1; break; } } -void sink_info_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) { - unsigned int *channels = (unsigned int *)userdata; +void AudioDriverPulseAudio::pa_sink_info_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) { + AudioDriverPulseAudio *ad = (AudioDriverPulseAudio *)userdata; // If eol is set to a positive number, you're at the end of the list if (eol > 0) { return; } - *channels = l->channel_map.channels; + ad->pa_channels = l->channel_map.channels; + ad->pa_status++; } -void server_info_cb(pa_context *c, const pa_server_info *i, void *userdata) { - char *default_output = (char *)userdata; +void AudioDriverPulseAudio::pa_server_info_cb(pa_context *c, const pa_server_info *i, void *userdata) { + AudioDriverPulseAudio *ad = (AudioDriverPulseAudio *)userdata; - strncpy(default_output, i->default_sink_name, 1024); + ad->default_device = i->default_sink_name; + ad->pa_status++; } -static unsigned int detect_channels() { - - pa_mainloop *pa_ml; - pa_mainloop_api *pa_mlapi; - pa_operation *pa_op; - pa_context *pa_ctx; - - int state = 0; - int pa_ready = 0; - - char default_output[1024]; - unsigned int channels = 2; - - pa_ml = pa_mainloop_new(); - pa_mlapi = pa_mainloop_get_api(pa_ml); - pa_ctx = pa_context_new(pa_mlapi, "Godot"); - - int ret = pa_context_connect(pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); - if (ret < 0) { - pa_context_unref(pa_ctx); - pa_mainloop_free(pa_ml); +void AudioDriverPulseAudio::detect_channels() { - return 2; - } + pa_channels = 2; - pa_context_set_state_callback(pa_ctx, pa_state_cb, &pa_ready); + if (device_name == "Default") { + // Get the default output device name + pa_status = 0; + pa_operation *pa_op = pa_context_get_server_info(pa_ctx, &AudioDriverPulseAudio::pa_server_info_cb, (void *)this); + if (pa_op) { + while (pa_status == 0) { + int ret = pa_mainloop_iterate(pa_ml, 1, NULL); + if (ret < 0) { + ERR_PRINT("pa_mainloop_iterate error"); + } + } - // Wait until the pa server is ready - while (pa_ready == 0) { - pa_mainloop_iterate(pa_ml, 1, NULL); + pa_operation_unref(pa_op); + } else { + ERR_PRINT("pa_context_get_server_info error"); + } } - // Check if there was an error connecting to the pa server - if (pa_ready == 2) { - pa_context_disconnect(pa_ctx); - pa_context_unref(pa_ctx); - pa_mainloop_free(pa_ml); - - return 2; + char device[1024]; + if (device_name == "Default") { + strcpy(device, default_device.utf8().get_data()); + } else { + strcpy(device, device_name.utf8().get_data()); } - // Get the default output device name - pa_op = pa_context_get_server_info(pa_ctx, &server_info_cb, (void *)default_output); + // Now using the device name get the amount of channels + pa_status = 0; + pa_operation *pa_op = pa_context_get_sink_info_by_name(pa_ctx, device, &AudioDriverPulseAudio::pa_sink_info_cb, (void *)this); if (pa_op) { - while (pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING) { - ret = pa_mainloop_iterate(pa_ml, 1, NULL); + while (pa_status == 0) { + int ret = pa_mainloop_iterate(pa_ml, 1, NULL); if (ret < 0) { ERR_PRINT("pa_mainloop_iterate error"); } } pa_operation_unref(pa_op); - - // Now using the device name get the amount of channels - pa_op = pa_context_get_sink_info_by_name(pa_ctx, default_output, &sink_info_cb, (void *)&channels); - if (pa_op) { - while (pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING) { - ret = pa_mainloop_iterate(pa_ml, 1, NULL); - if (ret < 0) { - ERR_PRINT("pa_mainloop_iterate error"); - } - } - - pa_operation_unref(pa_op); - } else { - ERR_PRINT("pa_context_get_sink_info_by_name error"); - } } else { - ERR_PRINT("pa_context_get_server_info error"); + ERR_PRINT("pa_context_get_sink_info_by_name error"); } - - pa_context_disconnect(pa_ctx); - pa_context_unref(pa_ctx); - pa_mainloop_free(pa_ml); - - return channels; } -Error AudioDriverPulseAudio::init() { - - active = false; - thread_exited = false; - exit_thread = false; +Error AudioDriverPulseAudio::init_device() { - mix_rate = GLOBAL_DEF("audio/mix_rate", DEFAULT_MIX_RATE); + // If there is a specified device check that it is really present + if (device_name != "Default") { + Array list = get_device_list(); + if (list.find(device_name) == -1) { + device_name = "Default"; + new_device = "Default"; + } + } // Detect the amount of channels PulseAudio is using // Note: If using an even amount of channels (2, 4, etc) channels and pa_channels will be equal, // if not then pa_channels will have the real amount of channels PulseAudio is using and channels // will have the amount of channels Godot is using (in this case it's pa_channels + 1) - pa_channels = detect_channels(); + detect_channels(); switch (pa_channels) { case 1: // Mono case 3: // Surround 2.1 @@ -205,27 +176,76 @@ Error AudioDriverPulseAudio::init() { attr.maxlength = (uint32_t)-1; attr.minreq = (uint32_t)-1; - int error_code; - pulse = pa_simple_new(NULL, // default server - "Godot", // application name - PA_STREAM_PLAYBACK, - NULL, // default device - "Sound", // stream description - &spec, - NULL, // use default channel map - &attr, // use buffering attributes from above - &error_code); - - if (pulse == NULL) { - fprintf(stderr, "PulseAudio ERR: %s\n", pa_strerror(error_code)); - ERR_FAIL_COND_V(pulse == NULL, ERR_CANT_OPEN); - } + pa_str = pa_stream_new(pa_ctx, "Sound", &spec, NULL); + ERR_FAIL_COND_V(pa_ctx == NULL, ERR_CANT_OPEN); + + const char *dev = device_name == "Default" ? NULL : device_name.utf8().get_data(); + pa_stream_flags flags = pa_stream_flags(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE); + int error_code = pa_stream_connect_playback(pa_str, dev, &attr, flags, NULL, NULL); + ERR_FAIL_COND_V(error_code < 0, ERR_CANT_OPEN); samples_in.resize(buffer_frames * channels); samples_out.resize(pa_buffer_size); - mutex = Mutex::create(); - thread = Thread::create(AudioDriverPulseAudio::thread_func, this); + return OK; +} + +Error AudioDriverPulseAudio::init() { + + active = false; + thread_exited = false; + exit_thread = false; + + mix_rate = GLOBAL_DEF("audio/mix_rate", DEFAULT_MIX_RATE); + + pa_ml = pa_mainloop_new(); + ERR_FAIL_COND_V(pa_ml == NULL, ERR_CANT_OPEN); + + pa_ctx = pa_context_new(pa_mainloop_get_api(pa_ml), "Godot"); + ERR_FAIL_COND_V(pa_ctx == NULL, ERR_CANT_OPEN); + + pa_ready = 0; + pa_context_set_state_callback(pa_ctx, pa_state_cb, (void *)this); + + int ret = pa_context_connect(pa_ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); + if (ret < 0) { + if (pa_ctx) { + pa_context_unref(pa_ctx); + pa_ctx = NULL; + } + + if (pa_ml) { + pa_mainloop_free(pa_ml); + pa_ml = NULL; + } + + return ERR_CANT_OPEN; + } + + while (pa_ready == 0) { + pa_mainloop_iterate(pa_ml, 1, NULL); + } + + if (pa_ready < 0) { + if (pa_ctx) { + pa_context_disconnect(pa_ctx); + pa_context_unref(pa_ctx); + pa_ctx = NULL; + } + + if (pa_ml) { + pa_mainloop_free(pa_ml); + pa_ml = NULL; + } + + return ERR_CANT_OPEN; + } + + Error err = init_device(); + if (err == OK) { + mutex = Mutex::create(); + thread = Thread::create(AudioDriverPulseAudio::thread_func, this); + } return OK; } @@ -233,9 +253,24 @@ Error AudioDriverPulseAudio::init() { float AudioDriverPulseAudio::get_latency() { if (latency == 0) { //only do this once since it's approximate anyway - int error_code; - pa_usec_t palat = pa_simple_get_latency(pulse, &error_code); - latency = double(palat) / 1000000.0; + lock(); + + pa_usec_t palat = 0; + if (pa_stream_get_state(pa_str) == PA_STREAM_READY) { + int negative = 0; + + if (pa_stream_get_latency(pa_str, &palat, &negative) >= 0) { + if (negative) { + palat = 0; + } + } + } + + if (palat > 0) { + latency = double(palat) / 1000000.0; + } + + unlock(); } return latency; @@ -278,17 +313,57 @@ void AudioDriverPulseAudio::thread_func(void *p_udata) { } } - // pa_simple_write always consumes the entire buffer - int error_code; int byte_size = ad->pa_buffer_size * sizeof(int16_t); - if (pa_simple_write(ad->pulse, ad->samples_out.ptr(), byte_size, &error_code) < 0) { - // can't recover here - fprintf(stderr, "PulseAudio failed and can't recover: %s\n", pa_strerror(error_code)); - ad->active = false; - ad->exit_thread = true; - break; + + ad->lock(); + + int ret; + do { + ret = pa_mainloop_iterate(ad->pa_ml, 0, NULL); + } while (ret > 0); + + if (pa_stream_get_state(ad->pa_str) == PA_STREAM_READY) { + const void *ptr = ad->samples_out.ptr(); + while (byte_size > 0) { + size_t bytes = pa_stream_writable_size(ad->pa_str); + if (bytes > 0) { + if (bytes > byte_size) { + bytes = byte_size; + } + + int ret = pa_stream_write(ad->pa_str, ptr, bytes, NULL, 0LL, PA_SEEK_RELATIVE); + if (ret >= 0) { + byte_size -= bytes; + ptr = (const char *)ptr + bytes; + } + } else { + pa_mainloop_iterate(ad->pa_ml, 1, NULL); + } + } } + + // User selected a new device, finish the current one so we'll init the new device + if (ad->device_name != ad->new_device) { + ad->device_name = ad->new_device; + ad->finish_device(); + + Error err = ad->init_device(); + if (err != OK) { + ERR_PRINT("PulseAudio: init_device error"); + ad->device_name = "Default"; + ad->new_device = "Default"; + + err = ad->init_device(); + if (err != OK) { + ad->active = false; + ad->exit_thread = true; + break; + } + } + } + + ad->unlock(); } ad->thread_exited = true; @@ -309,6 +384,61 @@ AudioDriver::SpeakerMode AudioDriverPulseAudio::get_speaker_mode() const { return get_speaker_mode_by_total_channels(channels); } +void AudioDriverPulseAudio::pa_sinklist_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) { + AudioDriverPulseAudio *ad = (AudioDriverPulseAudio *)userdata; + int ctr = 0; + + // If eol is set to a positive number, you're at the end of the list + if (eol > 0) { + return; + } + + ad->pa_devices.push_back(l->name); + ad->pa_status++; +} + +Array AudioDriverPulseAudio::get_device_list() { + + pa_devices.clear(); + pa_devices.push_back("Default"); + + if (pa_ctx == NULL) { + return pa_devices; + } + + lock(); + + // Get the device list + pa_status = 0; + pa_operation *pa_op = pa_context_get_sink_info_list(pa_ctx, pa_sinklist_cb, (void *)this); + if (pa_op) { + while (pa_status == 0) { + int ret = pa_mainloop_iterate(pa_ml, 1, NULL); + if (ret < 0) { + ERR_PRINT("pa_mainloop_iterate error"); + } + } + + pa_operation_unref(pa_op); + } else { + ERR_PRINT("pa_context_get_server_info error"); + } + + unlock(); + + return pa_devices; +} + +String AudioDriverPulseAudio::get_device() { + + return device_name; +} + +void AudioDriverPulseAudio::set_device(String device) { + + new_device = device; +} + void AudioDriverPulseAudio::lock() { if (!thread || !mutex) @@ -323,6 +453,15 @@ void AudioDriverPulseAudio::unlock() { mutex->unlock(); } +void AudioDriverPulseAudio::finish_device() { + + if (pa_str) { + pa_stream_disconnect(pa_str); + pa_stream_unref(pa_str); + pa_str = NULL; + } +} + void AudioDriverPulseAudio::finish() { if (!thread) @@ -331,9 +470,17 @@ void AudioDriverPulseAudio::finish() { exit_thread = true; Thread::wait_to_finish(thread); - if (pulse) { - pa_simple_free(pulse); - pulse = NULL; + finish_device(); + + if (pa_ctx) { + pa_context_disconnect(pa_ctx); + pa_context_unref(pa_ctx); + pa_ctx = NULL; + } + + if (pa_ml) { + pa_mainloop_free(pa_ml); + pa_ml = NULL; } memdelete(thread); @@ -347,9 +494,16 @@ void AudioDriverPulseAudio::finish() { AudioDriverPulseAudio::AudioDriverPulseAudio() { + pa_ml = NULL; + pa_ctx = NULL; + pa_str = NULL; + mutex = NULL; thread = NULL; - pulse = NULL; + + device_name = "Default"; + new_device = "Default"; + default_device = ""; samples_in.clear(); samples_out.clear(); @@ -359,6 +513,8 @@ AudioDriverPulseAudio::AudioDriverPulseAudio() { pa_buffer_size = 0; channels = 0; pa_channels = 0; + pa_ready = 0; + pa_status = 0; active = false; thread_exited = false; diff --git a/drivers/pulseaudio/audio_driver_pulseaudio.h b/drivers/pulseaudio/audio_driver_pulseaudio.h index 5737f24314..934031e2a5 100644 --- a/drivers/pulseaudio/audio_driver_pulseaudio.h +++ b/drivers/pulseaudio/audio_driver_pulseaudio.h @@ -37,14 +37,20 @@ #include "core/os/thread.h" #include "servers/audio_server.h" -#include <pulse/simple.h> +#include <pulse/pulseaudio.h> class AudioDriverPulseAudio : public AudioDriver { Thread *thread; Mutex *mutex; - pa_simple *pulse; + pa_mainloop *pa_ml; + pa_context *pa_ctx; + pa_stream *pa_str; + + String device_name; + String new_device; + String default_device; Vector<int32_t> samples_in; Vector<int16_t> samples_out; @@ -54,6 +60,9 @@ class AudioDriverPulseAudio : public AudioDriver { unsigned int pa_buffer_size; int channels; int pa_channels; + int pa_ready; + int pa_status; + Array pa_devices; bool active; bool thread_exited; @@ -61,6 +70,16 @@ class AudioDriverPulseAudio : public AudioDriver { float latency; + static void pa_state_cb(pa_context *c, void *userdata); + static void pa_sink_info_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata); + static void pa_server_info_cb(pa_context *c, const pa_server_info *i, void *userdata); + static void pa_sinklist_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata); + + Error init_device(); + void finish_device(); + + void detect_channels(); + static void thread_func(void *p_udata); public: @@ -72,6 +91,9 @@ public: virtual void start(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; + virtual Array get_device_list(); + virtual String get_device(); + virtual void set_device(String device); virtual void lock(); virtual void unlock(); virtual void finish(); diff --git a/drivers/wasapi/audio_driver_wasapi.cpp b/drivers/wasapi/audio_driver_wasapi.cpp index aae6c0d308..966b69a67e 100644 --- a/drivers/wasapi/audio_driver_wasapi.cpp +++ b/drivers/wasapi/audio_driver_wasapi.cpp @@ -35,6 +35,8 @@ #include "os/os.h" #include "project_settings.h" +#include <functiondiscoverykeys.h> + const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); const IID IID_IAudioClient = __uuidof(IAudioClient); @@ -121,7 +123,61 @@ Error AudioDriverWASAPI::init_device(bool reinit) { HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&enumerator); ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN); - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); + if (device_name == "Default") { + hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); + } else { + IMMDeviceCollection *devices = NULL; + + hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices); + ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN); + + LPWSTR strId = NULL; + bool found = false; + + UINT count = 0; + hr = devices->GetCount(&count); + ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN); + + for (ULONG i = 0; i < count && !found; i++) { + IMMDevice *device = NULL; + + hr = devices->Item(i, &device); + ERR_BREAK(hr != S_OK); + + IPropertyStore *props = NULL; + hr = device->OpenPropertyStore(STGM_READ, &props); + ERR_BREAK(hr != S_OK); + + PROPVARIANT propvar; + PropVariantInit(&propvar); + + hr = props->GetValue(PKEY_Device_FriendlyName, &propvar); + ERR_BREAK(hr != S_OK); + + if (device_name == String(propvar.pwszVal)) { + hr = device->GetId(&strId); + ERR_BREAK(hr != S_OK); + + found = true; + } + + PropVariantClear(&propvar); + props->Release(); + device->Release(); + } + + if (found) { + hr = enumerator->GetDevice(strId, &device); + } + + if (strId) { + CoTaskMemFree(strId); + } + + if (device == NULL) { + hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); + } + } if (reinit) { // In case we're trying to re-initialize the device prevent throwing this error on the console, // otherwise if there is currently no device available this will spam the console. @@ -294,6 +350,64 @@ AudioDriver::SpeakerMode AudioDriverWASAPI::get_speaker_mode() const { return get_speaker_mode_by_total_channels(channels); } +Array AudioDriverWASAPI::get_device_list() { + + Array list; + IMMDeviceCollection *devices = NULL; + IMMDeviceEnumerator *enumerator = NULL; + + list.push_back(String("Default")); + + CoInitialize(NULL); + + HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&enumerator); + ERR_FAIL_COND_V(hr != S_OK, Array()); + + hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices); + ERR_FAIL_COND_V(hr != S_OK, Array()); + + UINT count = 0; + hr = devices->GetCount(&count); + ERR_FAIL_COND_V(hr != S_OK, Array()); + + for (ULONG i = 0; i < count; i++) { + IMMDevice *device = NULL; + + hr = devices->Item(i, &device); + ERR_BREAK(hr != S_OK); + + IPropertyStore *props = NULL; + hr = device->OpenPropertyStore(STGM_READ, &props); + ERR_BREAK(hr != S_OK); + + PROPVARIANT propvar; + PropVariantInit(&propvar); + + hr = props->GetValue(PKEY_Device_FriendlyName, &propvar); + ERR_BREAK(hr != S_OK); + + list.push_back(String(propvar.pwszVal)); + + PropVariantClear(&propvar); + props->Release(); + device->Release(); + } + + devices->Release(); + enumerator->Release(); + return list; +} + +String AudioDriverWASAPI::get_device() { + + return device_name; +} + +void AudioDriverWASAPI::set_device(String device) { + + new_device = device; +} + void AudioDriverWASAPI::write_sample(AudioDriverWASAPI *ad, BYTE *buffer, int i, int32_t sample) { if (ad->format_tag == WAVE_FORMAT_PCM) { switch (ad->bits_per_sample) { @@ -409,7 +523,8 @@ void AudioDriverWASAPI::thread_func(void *p_udata) { } } - if (default_device_changed) { + // If we're using the Default device and it changed finish it so we'll re-init the device + if (ad->device_name == "Default" && default_device_changed) { Error err = ad->finish_device(); if (err != OK) { ERR_PRINT("WASAPI: finish_device error"); @@ -418,6 +533,15 @@ void AudioDriverWASAPI::thread_func(void *p_udata) { default_device_changed = false; } + // User selected a new device, finish the current one so we'll init the new device + if (ad->device_name != ad->new_device) { + ad->device_name = ad->new_device; + Error err = ad->finish_device(); + if (err != OK) { + ERR_PRINT("WASAPI: finish_device error"); + } + } + if (!ad->audio_client) { Error err = ad->init_device(true); if (err == OK) { @@ -492,6 +616,9 @@ AudioDriverWASAPI::AudioDriverWASAPI() { thread_exited = false; exit_thread = false; active = false; + + device_name = "Default"; + new_device = "Default"; } #endif diff --git a/drivers/wasapi/audio_driver_wasapi.h b/drivers/wasapi/audio_driver_wasapi.h index 2b19f0cca1..c97f4c288c 100644 --- a/drivers/wasapi/audio_driver_wasapi.h +++ b/drivers/wasapi/audio_driver_wasapi.h @@ -49,6 +49,9 @@ class AudioDriverWASAPI : public AudioDriver { Mutex *mutex; Thread *thread; + String device_name; + String new_device; + WORD format_tag; WORD bits_per_sample; @@ -80,6 +83,9 @@ public: virtual void start(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; + virtual Array get_device_list(); + virtual String get_device(); + virtual void set_device(String device); virtual void lock(); virtual void unlock(); virtual void finish(); diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 9ae9ab1501..4c7f2f53cc 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -212,35 +212,39 @@ void FindReplaceBar::_replace_all() { text_edit->begin_complex_operation(); - while (search_next()) { - - // replace area - Point2i match_from(result_line, result_col); - Point2i match_to(result_line, result_col + search_text_len); - - if (match_from < prev_match) - break; // done + if (search_current()) { + do { + // replace area + Point2i match_from(result_line, result_col); + Point2i match_to(result_line, result_col + search_text_len); + + if (match_from < prev_match) { + break; // done + } - prev_match = Point2i(result_line, result_col + replace_text.length()); + prev_match = Point2i(result_line, result_col + replace_text.length()); - text_edit->unfold_line(result_line); - text_edit->select(result_line, result_col, result_line, match_to.y); + text_edit->unfold_line(result_line); + text_edit->select(result_line, result_col, result_line, match_to.y); - if (selection_enabled && is_selection_only()) { + if (selection_enabled && is_selection_only()) { + if (match_from < selection_begin || match_to > selection_end) { + continue; + } - if (match_from < selection_begin || match_to > selection_end) - continue; + // replace but adjust selection bounds + text_edit->insert_text_at_cursor(replace_text); + if (match_to.x == selection_end.x) { + selection_end.y += replace_text.length() - search_text_len; + } - // replace but adjust selection bounds - text_edit->insert_text_at_cursor(replace_text); - if (match_to.x == selection_end.x) - selection_end.y += replace_text.length() - search_text_len; - } else { - // just replace - text_edit->insert_text_at_cursor(replace_text); - } + } else { + // just replace + text_edit->insert_text_at_cursor(replace_text); + } - rc++; + rc++; + } while (search_next()); } text_edit->end_complex_operation(); diff --git a/editor/create_dialog.cpp b/editor/create_dialog.cpp index 13ef6e9e0e..78fb35e354 100644 --- a/editor/create_dialog.cpp +++ b/editor/create_dialog.cpp @@ -433,26 +433,7 @@ Object *CreateDialog::instance_selected() { custom = md; if (custom != String()) { - if (EditorNode::get_editor_data().get_custom_types().has(custom)) { - - for (int i = 0; i < EditorNode::get_editor_data().get_custom_types()[custom].size(); i++) { - if (EditorNode::get_editor_data().get_custom_types()[custom][i].name == selected->get_text(0)) { - Ref<Texture> icon = EditorNode::get_editor_data().get_custom_types()[custom][i].icon; - Ref<Script> script = EditorNode::get_editor_data().get_custom_types()[custom][i].script; - String name = selected->get_text(0); - - Object *ob = ClassDB::instance(custom); - ERR_FAIL_COND_V(!ob, NULL); - if (ob->is_class("Node")) { - ob->call("set_name", name); - } - ob->set_script(script.get_ref_ptr()); - if (icon.is_valid()) - ob->set_meta("_editor_icon", icon); - return ob; - } - } - } + return EditorNode::get_editor_data().instance_custom_type(selected->get_text(0), custom); } else { return ClassDB::instance(selected->get_text(0)); } diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp index 95ed40d889..ef9265ecd2 100644 --- a/editor/editor_data.cpp +++ b/editor/editor_data.cpp @@ -452,6 +452,31 @@ void EditorData::add_custom_type(const String &p_type, const String &p_inherits, custom_types[p_inherits].push_back(ct); } +Object *EditorData::instance_custom_type(const String &p_type, const String &p_inherits) { + + if (get_custom_types().has(p_inherits)) { + + for (int i = 0; i < get_custom_types()[p_inherits].size(); i++) { + if (get_custom_types()[p_inherits][i].name == p_type) { + Ref<Texture> icon = get_custom_types()[p_inherits][i].icon; + Ref<Script> script = get_custom_types()[p_inherits][i].script; + + Object *ob = ClassDB::instance(p_inherits); + ERR_FAIL_COND_V(!ob, NULL); + if (ob->is_class("Node")) { + ob->call("set_name", p_type); + } + ob->set_script(script.get_ref_ptr()); + if (icon.is_valid()) + ob->set_meta("_editor_icon", icon); + return ob; + } + } + } + + return NULL; +} + void EditorData::remove_custom_type(const String &p_type) { for (Map<String, Vector<CustomType> >::Element *E = custom_types.front(); E; E = E->next()) { diff --git a/editor/editor_data.h b/editor/editor_data.h index 844145853d..1a498a6150 100644 --- a/editor/editor_data.h +++ b/editor/editor_data.h @@ -171,6 +171,7 @@ public: void restore_editor_global_states(); void add_custom_type(const String &p_type, const String &p_inherits, const Ref<Script> &p_script, const Ref<Texture> &p_icon); + Object *instance_custom_type(const String &p_type, const String &p_inherits); void remove_custom_type(const String &p_type); const Map<String, Vector<CustomType> > &get_custom_types() const { return custom_types; } diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 7936cf446f..141769b16a 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -470,24 +470,28 @@ void EditorNode::_fs_changed() { } if (preset.is_null()) { String err = "Unknown export preset: " + export_defer.preset; - ERR_PRINT(err.utf8().get_data()); + ERR_PRINTS(err); } else { Ref<EditorExportPlatform> platform = preset->get_platform(); if (platform.is_null()) { String err = "Preset \"" + export_defer.preset + "\" doesn't have a platform."; - ERR_PRINT(err.utf8().get_data()); + ERR_PRINTS(err); } else { // ensures export_project does not loop infinitely, because notifications may // come during the export export_defer.preset = ""; + Error err; if (!preset->is_runnable() && (export_defer.path.ends_with(".pck") || export_defer.path.ends_with(".zip"))) { if (export_defer.path.ends_with(".zip")) { - platform->save_zip(preset, export_defer.path); + err = platform->save_zip(preset, export_defer.path); } else if (export_defer.path.ends_with(".pck")) { - platform->save_pack(preset, export_defer.path); + err = platform->save_pack(preset, export_defer.path); } } else { - platform->export_project(preset, export_defer.debug, export_defer.path, /*p_flags*/ 0); + err = platform->export_project(preset, export_defer.debug, export_defer.path, /*p_flags*/ 0); + } + if (err != OK) { + ERR_PRINTS(vformat(TTR("Project export failed with error code %d."), (int)err)); } } } @@ -1230,29 +1234,25 @@ void EditorNode::_dialog_action(String p_file) { } break; case FILE_EXPORT_TILESET: { - Ref<TileSet> ml; - if (FileAccess::exists(p_file)) { - ml = ResourceLoader::load(p_file, "TileSet"); + Ref<TileSet> tileset; + if (FileAccess::exists(p_file) && file_export_lib_merge->is_pressed()) { + tileset = ResourceLoader::load(p_file, "TileSet"); - if (ml.is_null()) { - if (file_export_lib_merge->is_pressed()) { - current_option = -1; - accept->get_ok()->set_text(TTR("I see..")); - accept->set_text(TTR("Can't load TileSet for merging!")); - accept->popup_centered_minsize(); - return; - } - } else if (!file_export_lib_merge->is_pressed()) { - ml->clear(); + if (tileset.is_null()) { + current_option = -1; + accept->get_ok()->set_text(TTR("I see..")); + accept->set_text(TTR("Can't load TileSet for merging!")); + accept->popup_centered_minsize(); + return; } } else { - ml = Ref<TileSet>(memnew(TileSet)); + tileset = Ref<TileSet>(memnew(TileSet)); } - TileSetEditor::update_library_file(editor_data.get_edited_scene_root(), ml, true); + TileSetEditor::update_library_file(editor_data.get_edited_scene_root(), tileset, true); - Error err = ResourceSaver::save(p_file, ml); + Error err = ResourceSaver::save(p_file, tileset); if (err) { current_option = -1; diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index b10f785a28..3582379e34 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -626,8 +626,8 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_color("font_color_disabled", "PopupMenu", font_color_disabled); theme->set_icon("checked", "PopupMenu", theme->get_icon("GuiChecked", "EditorIcons")); theme->set_icon("unchecked", "PopupMenu", theme->get_icon("GuiUnchecked", "EditorIcons")); - theme->set_icon("radio_checked", "PopupMenu", theme->get_icon("GuiChecked", "EditorIcons")); - theme->set_icon("radio_unchecked", "PopupMenu", theme->get_icon("GuiUnchecked", "EditorIcons")); + theme->set_icon("radio_checked", "PopupMenu", theme->get_icon("GuiRadioChecked", "EditorIcons")); + theme->set_icon("radio_unchecked", "PopupMenu", theme->get_icon("GuiRadioUnchecked", "EditorIcons")); theme->set_icon("submenu", "PopupMenu", theme->get_icon("ArrowRight", "EditorIcons")); theme->set_icon("visibility_hidden", "PopupMenu", theme->get_icon("GuiVisibilityHidden", "EditorIcons")); theme->set_icon("visibility_visible", "PopupMenu", theme->get_icon("GuiVisibilityVisible", "EditorIcons")); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index d5783c9b1a..16223dbb16 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -719,12 +719,13 @@ void FileSystemDock::_push_to_history() { button_hist_next->set_disabled(history_pos == history.size() - 1); } -void FileSystemDock::_get_all_files_in_dir(EditorFileSystemDirectory *efsd, Vector<String> &files) const { +void FileSystemDock::_get_all_items_in_dir(EditorFileSystemDirectory *efsd, Vector<String> &files, Vector<String> &folders) const { if (efsd == NULL) return; for (int i = 0; i < efsd->get_subdir_count(); i++) { - _get_all_files_in_dir(efsd->get_subdir(i), files); + folders.push_back(efsd->get_subdir(i)->get_path()); + _get_all_items_in_dir(efsd->get_subdir(i), files, folders); } for (int i = 0; i < efsd->get_file_count(); i++) { files.push_back(efsd->get_file_path(i)); @@ -746,7 +747,8 @@ void FileSystemDock::_find_remaps(EditorFileSystemDirectory *efsd, const Map<Str } } -void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_new_path, Map<String, String> &p_renames) const { +void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_new_path, + Map<String, String> &p_file_renames, Map<String, String> &p_folder_renames) const { //Ensure folder paths end with "/" String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/"); String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/"); @@ -763,11 +765,13 @@ void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_ } //Build a list of files which will have new paths as a result of this operation - Vector<String> changed_paths; + Vector<String> file_changed_paths; + Vector<String> folder_changed_paths; if (p_item.is_file) { - changed_paths.push_back(old_path); + file_changed_paths.push_back(old_path); } else { - _get_all_files_in_dir(EditorFileSystem::get_singleton()->get_filesystem_path(old_path), changed_paths); + folder_changed_paths.push_back(old_path); + _get_all_items_in_dir(EditorFileSystem::get_singleton()->get_filesystem_path(old_path), file_changed_paths, folder_changed_paths); } DirAccess *da = DirAccess::create(DirAccess::ACCESS_RESOURCES); @@ -783,12 +787,12 @@ void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_ } // update scene if it is open - for (int i = 0; i < changed_paths.size(); ++i) { - String new_item_path = p_item.is_file ? new_path : changed_paths[i].replace_first(old_path, new_path); - if (ResourceLoader::get_resource_type(new_item_path) == "PackedScene" && editor->is_scene_open(changed_paths[i])) { + for (int i = 0; i < file_changed_paths.size(); ++i) { + String new_item_path = p_item.is_file ? new_path : file_changed_paths[i].replace_first(old_path, new_path); + if (ResourceLoader::get_resource_type(new_item_path) == "PackedScene" && editor->is_scene_open(file_changed_paths[i])) { EditorData *ed = &editor->get_editor_data(); for (int j = 0; j < ed->get_edited_scene_count(); j++) { - if (ed->get_scene_path(j) == changed_paths[i]) { + if (ed->get_scene_path(j) == file_changed_paths[i]) { ed->get_edited_scene_root(j)->set_filename(new_item_path); break; } @@ -797,9 +801,12 @@ void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_ } //Only treat as a changed dependency if it was successfully moved - for (int i = 0; i < changed_paths.size(); ++i) { - p_renames[changed_paths[i]] = changed_paths[i].replace_first(old_path, new_path); - print_line(" Remap: " + changed_paths[i] + " -> " + p_renames[changed_paths[i]]); + for (int i = 0; i < file_changed_paths.size(); ++i) { + p_file_renames[file_changed_paths[i]] = file_changed_paths[i].replace_first(old_path, new_path); + print_line(" Remap: " + file_changed_paths[i] + " -> " + p_file_renames[file_changed_paths[i]]); + } + for (int i = 0; i < folder_changed_paths.size(); ++i) { + p_folder_renames[folder_changed_paths[i]] = folder_changed_paths[i].replace_first(old_path, new_path); } } else { EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + "\n"); @@ -912,6 +919,25 @@ void FileSystemDock::_update_dependencies_after_move(const Map<String, String> & } } +void FileSystemDock::_update_favorite_dirs_list_after_move(const Map<String, String> &p_renames) const { + + Vector<String> favorite_dirs = EditorSettings::get_singleton()->get_favorite_dirs(); + Vector<String> new_favorite_dirs; + + for (int i = 0; i < favorite_dirs.size(); i++) { + String old_path = favorite_dirs[i] + "/"; + + if (p_renames.has(old_path)) { + String new_path = p_renames[old_path]; + new_favorite_dirs.push_back(new_path.substr(0, new_path.length() - 1)); + } else { + new_favorite_dirs.push_back(favorite_dirs[i]); + } + } + + EditorSettings::get_singleton()->set_favorite_dirs(new_favorite_dirs); +} + void FileSystemDock::_make_dir_confirm() { String dir_name = make_dir_dialog_text->get_text().strip_edges(); @@ -970,10 +996,12 @@ void FileSystemDock::_rename_operation_confirm() { } memdelete(da); - Map<String, String> renames; - _try_move_item(to_rename, new_path, renames); - _update_dependencies_after_move(renames); - _update_resource_paths_after_move(renames); + Map<String, String> file_renames; + Map<String, String> folder_renames; + _try_move_item(to_rename, new_path, file_renames, folder_renames); + _update_dependencies_after_move(file_renames); + _update_resource_paths_after_move(file_renames); + _update_favorite_dirs_list_after_move(folder_renames); //Rescan everything print_line("call rescan!"); @@ -1017,15 +1045,17 @@ void FileSystemDock::_duplicate_operation_confirm() { void FileSystemDock::_move_operation_confirm(const String &p_to_path) { - Map<String, String> renames; + Map<String, String> file_renames; + Map<String, String> folder_renames; for (int i = 0; i < to_move.size(); i++) { String old_path = to_move[i].path.ends_with("/") ? to_move[i].path.substr(0, to_move[i].path.length() - 1) : to_move[i].path; String new_path = p_to_path.plus_file(old_path.get_file()); - _try_move_item(to_move[i], new_path, renames); + _try_move_item(to_move[i], new_path, file_renames, folder_renames); } - _update_dependencies_after_move(renames); - _update_resource_paths_after_move(renames); + _update_dependencies_after_move(file_renames); + _update_resource_paths_after_move(file_renames); + _update_favorite_dirs_list_after_move(folder_renames); print_line("call rescan!"); _rescan(); diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index 377316d1ba..c8448a1022 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -176,12 +176,13 @@ private: void _file_selected(); void _dir_selected(); - void _get_all_files_in_dir(EditorFileSystemDirectory *efsd, Vector<String> &files) const; + void _get_all_items_in_dir(EditorFileSystemDirectory *efsd, Vector<String> &files, Vector<String> &folders) const; void _find_remaps(EditorFileSystemDirectory *efsd, const Map<String, String> &renames, Vector<String> &to_remaps) const; - void _try_move_item(const FileOrFolder &p_item, const String &p_new_path, Map<String, String> &p_renames) const; + void _try_move_item(const FileOrFolder &p_item, const String &p_new_path, Map<String, String> &p_file_renames, Map<String, String> &p_folder_renames) const; void _try_duplicate_item(const FileOrFolder &p_item, const String &p_new_path) const; void _update_dependencies_after_move(const Map<String, String> &p_renames) const; void _update_resource_paths_after_move(const Map<String, String> &p_renames) const; + void _update_favorite_dirs_list_after_move(const Map<String, String> &p_renames) const; void _make_dir_confirm(); void _rename_operation_confirm(); diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp new file mode 100644 index 0000000000..9442bbc0e8 --- /dev/null +++ b/editor/find_in_files.cpp @@ -0,0 +1,829 @@ +/*************************************************************************/ +/* find_in_files.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "find_in_files.h" +#include "core/os/dir_access.h" +#include "core/os/os.h" +#include "editor_scale.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" +#include "scene/gui/file_dialog.h" +#include "scene/gui/grid_container.h" +#include "scene/gui/item_list.h" +#include "scene/gui/label.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/progress_bar.h" + +#define ROOT_PREFIX "res://" + +const char *FindInFiles::SIGNAL_RESULT_FOUND = "result_found"; +const char *FindInFiles::SIGNAL_FINISHED = "finished"; + +// TODO Would be nice in Vector and PoolVectors +template <typename T> +inline void pop_back(T &container) { + container.resize(container.size() - 1); +} + +// TODO Copied from TextEdit private, would be nice to extract it in a single place +static bool is_text_char(CharType c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +FindInFiles::FindInFiles() { + _root_prefix = ROOT_PREFIX; + _extension_filter.insert("gd"); + _extension_filter.insert("cs"); + _searching = false; + _whole_words = true; + _match_case = true; +} + +void FindInFiles::set_search_text(String p_pattern) { + _pattern = p_pattern; +} + +void FindInFiles::set_whole_words(bool p_whole_word) { + _whole_words = p_whole_word; +} + +void FindInFiles::set_match_case(bool p_match_case) { + _match_case = p_match_case; +} + +void FindInFiles::set_folder(String folder) { + _root_dir = folder; +} + +void FindInFiles::set_filter(const Set<String> &exts) { + _extension_filter = exts; +} + +void FindInFiles::_notification(int p_notification) { + if (p_notification == NOTIFICATION_PROCESS) { + _process(); + } +} + +void FindInFiles::start() { + if (_pattern == "") { + print_line("Nothing to search, pattern is empty"); + emit_signal(SIGNAL_FINISHED); + return; + } + if (_extension_filter.size() == 0) { + print_line("Nothing to search, filter matches no files"); + emit_signal(SIGNAL_FINISHED); + return; + } + + // Init search + _current_dir = ""; + PoolStringArray init_folder; + init_folder.append(_root_dir); + _folders_stack.push_back(init_folder); + + _initial_files_count = 0; + + _searching = true; + set_process(true); +} + +void FindInFiles::stop() { + _searching = false; + _current_dir = ""; + set_process(false); +} + +void FindInFiles::_process() { + // This part can be moved to a thread if needed + + OS &os = *OS::get_singleton(); + float duration = 0.0; + while (duration < 1.0 / 120.0) { + float time_before = os.get_ticks_msec(); + _iterate(); + duration += (os.get_ticks_msec() - time_before); + } +} + +void FindInFiles::_iterate() { + + if (_folders_stack.size() != 0) { + + // Scan folders first so we can build a list of files and have progress info later + + PoolStringArray &folders_to_scan = _folders_stack[_folders_stack.size() - 1]; + + if (folders_to_scan.size() != 0) { + // Scan one folder below + + String folder_name = folders_to_scan[folders_to_scan.size() - 1]; + pop_back(folders_to_scan); + + _current_dir = _current_dir.plus_file(folder_name); + + PoolStringArray sub_dirs; + _scan_dir(_root_prefix + _current_dir, sub_dirs); + + if (sub_dirs.size() != 0) { + _folders_stack.push_back(sub_dirs); + } + + } else { + // Go back one level + + pop_back(_folders_stack); + _current_dir = _current_dir.get_base_dir(); + + if (_folders_stack.size() == 0) { + // All folders scanned + _initial_files_count = _files_to_scan.size(); + } + } + + } else if (_files_to_scan.size() != 0) { + + // Then scan files + + String fpath = _files_to_scan[_files_to_scan.size() - 1]; + pop_back(_files_to_scan); + _scan_file(_root_prefix + fpath); + + } else { + print_line("Search complete"); + set_process(false); + _current_dir = ""; + _searching = false; + emit_signal(SIGNAL_FINISHED); + } +} + +float FindInFiles::get_progress() const { + if (_initial_files_count != 0) { + return static_cast<float>(_initial_files_count - _files_to_scan.size()) / static_cast<float>(_initial_files_count); + } + return 0; +} + +void FindInFiles::_scan_dir(String path, PoolStringArray &out_folders) { + + DirAccess *dir = DirAccess::open(path); + if (dir == NULL) { + print_line("Cannot open directory! " + path); + return; + } + + //print_line(String("Scanning ") + path); + + dir->list_dir_begin(); + + for (int i = 0; i < 1000; ++i) { + String file = dir->get_next(); + + if (file == "") + break; + + // Ignore special dirs and hidden dirs (such as .git and .import) + if (file == "." || file == ".." || file.begins_with(".")) + continue; + + if (dir->current_is_dir()) + out_folders.append(file); + + else { + String file_ext = file.get_extension(); + if (_extension_filter.has(file_ext)) { + _files_to_scan.push_back(file); + } + } + } +} + +void FindInFiles::_scan_file(String fpath) { + + FileAccess *f = FileAccess::open(fpath, FileAccess::READ); + if (f == NULL) { + f->close(); + print_line(String("Cannot open file ") + fpath); + return; + } + + int line_number = 0; + + while (!f->eof_reached()) { + + // line number starts at 1 + ++line_number; + + int begin = 0; + int end = 0; + + String line = f->get_line(); + + // Find all occurrences in the current line + while (true) { + begin = _match_case ? line.find(_pattern, end) : line.findn(_pattern, end); + + if (begin == -1) + break; + + end = begin + _pattern.length(); + + if (_whole_words) { + if (begin > 0 && is_text_char(line[begin - 1])) { + continue; + } + if (end < line.size() && is_text_char(line[end])) { + continue; + } + } + + emit_signal(SIGNAL_RESULT_FOUND, fpath, line_number, begin, end, line); + } + } + + f->close(); +} + +void FindInFiles::_bind_methods() { + + ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_FOUND, + PropertyInfo(Variant::STRING, "path"), + PropertyInfo(Variant::INT, "line_number"), + PropertyInfo(Variant::INT, "begin"), + PropertyInfo(Variant::INT, "end"), + PropertyInfo(Variant::STRING, "text"))); + + ADD_SIGNAL(MethodInfo(SIGNAL_FINISHED)); +} + +//----------------------------------------------------------------------------- +const char *FindInFilesDialog::SIGNAL_FIND_REQUESTED = "find_requested"; +const char *FindInFilesDialog::SIGNAL_REPLACE_REQUESTED = "replace_requested"; + +FindInFilesDialog::FindInFilesDialog() { + + set_custom_minimum_size(Size2(400, 190)); + set_resizable(true); + set_title(TTR("Find in files")); + + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, -8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, -8 * EDSCALE); + add_child(vbc); + + GridContainer *gc = memnew(GridContainer); + gc->set_columns(2); + vbc->add_child(gc); + + Label *find_label = memnew(Label); + find_label->set_text(TTR("Find: ")); + gc->add_child(find_label); + + _search_text_line_edit = memnew(LineEdit); + _search_text_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + _search_text_line_edit->connect("text_changed", this, "_on_search_text_modified"); + _search_text_line_edit->connect("text_entered", this, "_on_search_text_entered"); + gc->add_child(_search_text_line_edit); + + { + Control *placeholder = memnew(Control); + gc->add_child(placeholder); + } + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + _whole_words_checkbox = memnew(CheckBox); + _whole_words_checkbox->set_text(TTR("Whole words")); + _whole_words_checkbox->set_pressed(true); + hbc->add_child(_whole_words_checkbox); + + _match_case_checkbox = memnew(CheckBox); + _match_case_checkbox->set_text(TTR("Match case")); + _match_case_checkbox->set_pressed(true); + hbc->add_child(_match_case_checkbox); + + gc->add_child(hbc); + } + + Label *folder_label = memnew(Label); + folder_label->set_text(TTR("Folder: ")); + gc->add_child(folder_label); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Label *prefix_label = memnew(Label); + prefix_label->set_text(ROOT_PREFIX); + hbc->add_child(prefix_label); + + _folder_line_edit = memnew(LineEdit); + _folder_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + hbc->add_child(_folder_line_edit); + + Button *folder_button = memnew(Button); + folder_button->set_text("..."); + folder_button->connect("pressed", this, "_on_folder_button_pressed"); + hbc->add_child(folder_button); + + _folder_dialog = memnew(FileDialog); + _folder_dialog->set_mode(FileDialog::MODE_OPEN_DIR); + _folder_dialog->connect("dir_selected", this, "_on_folder_selected"); + add_child(_folder_dialog); + + gc->add_child(hbc); + } + + Label *filter_label = memnew(Label); + filter_label->set_text(TTR("Filter: ")); + gc->add_child(filter_label); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Vector<String> exts; + exts.push_back("gd"); + exts.push_back("cs"); + + for (int i = 0; i < exts.size(); ++i) { + CheckBox *cb = memnew(CheckBox); + cb->set_text(exts[i]); + cb->set_pressed(true); + hbc->add_child(cb); + _filters.push_back(cb); + } + + gc->add_child(hbc); + } + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(0, EDSCALE * 16)); + vbc->add_child(placeholder); + } + + { + HBoxContainer *hbc = memnew(HBoxContainer); + hbc->set_alignment(HBoxContainer::ALIGN_CENTER); + + _find_button = memnew(Button); + _find_button->set_text(TTR("Find...")); + _find_button->connect("pressed", this, "_on_find_button_pressed"); + _find_button->set_disabled(true); + hbc->add_child(_find_button); + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(EDSCALE * 16, 0)); + hbc->add_child(placeholder); + } + + _replace_button = memnew(Button); + _replace_button->set_text(TTR("Replace...")); + _replace_button->connect("pressed", this, "_on_replace_button_pressed"); + _replace_button->set_disabled(true); + hbc->add_child(_replace_button); + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(EDSCALE * 16, 0)); + hbc->add_child(placeholder); + } + + Button *cancel_button = memnew(Button); + cancel_button->set_text(TTR("Cancel")); + cancel_button->connect("pressed", this, "hide"); + hbc->add_child(cancel_button); + + vbc->add_child(hbc); + } +} + +void FindInFilesDialog::set_search_text(String text) { + _search_text_line_edit->set_text(text); +} + +String FindInFilesDialog::get_search_text() const { + String text = _search_text_line_edit->get_text(); + return text.strip_edges(); +} + +bool FindInFilesDialog::is_match_case() const { + return _match_case_checkbox->is_pressed(); +} + +bool FindInFilesDialog::is_whole_words() const { + return _whole_words_checkbox->is_pressed(); +} + +String FindInFilesDialog::get_folder() const { + String text = _folder_line_edit->get_text(); + return text.strip_edges(); +} + +Set<String> FindInFilesDialog::get_filter() const { + Set<String> filters; + for (int i = 0; i < _filters.size(); ++i) { + CheckBox *cb = _filters[i]; + if (cb->is_pressed()) { + filters.insert(_filters[i]->get_text()); + } + } + return filters; +} + +void FindInFilesDialog::_notification(int p_what) { + if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { + if (is_visible()) { + // Doesn't work more than once if not deferred... + _search_text_line_edit->call_deferred("grab_focus"); + _search_text_line_edit->select_all(); + } + } +} + +void FindInFilesDialog::_on_folder_button_pressed() { + _folder_dialog->popup_centered_ratio(); +} + +void FindInFilesDialog::_on_find_button_pressed() { + emit_signal(SIGNAL_FIND_REQUESTED); + hide(); +} + +void FindInFilesDialog::_on_replace_button_pressed() { + emit_signal(SIGNAL_REPLACE_REQUESTED); + hide(); +} + +void FindInFilesDialog::_on_search_text_modified(String text) { + + ERR_FAIL_COND(!_find_button); + ERR_FAIL_COND(!_replace_button); + + _find_button->set_disabled(get_search_text().empty()); + _replace_button->set_disabled(get_search_text().empty()); +} + +void FindInFilesDialog::_on_search_text_entered(String text) { + // This allows to trigger a global search without leaving the keyboard + if (!_find_button->is_disabled()) + _on_find_button_pressed(); +} + +void FindInFilesDialog::_on_folder_selected(String path) { + int i = path.find("://"); + if (i != -1) + path = path.right(i + 3); + _folder_line_edit->set_text(path); +} + +void FindInFilesDialog::_bind_methods() { + + ClassDB::bind_method("_on_folder_button_pressed", &FindInFilesDialog::_on_folder_button_pressed); + ClassDB::bind_method("_on_find_button_pressed", &FindInFilesDialog::_on_find_button_pressed); + ClassDB::bind_method("_on_replace_button_pressed", &FindInFilesDialog::_on_replace_button_pressed); + ClassDB::bind_method("_on_folder_selected", &FindInFilesDialog::_on_folder_selected); + ClassDB::bind_method("_on_search_text_modified", &FindInFilesDialog::_on_search_text_modified); + ClassDB::bind_method("_on_search_text_entered", &FindInFilesDialog::_on_search_text_entered); + + ADD_SIGNAL(MethodInfo(SIGNAL_FIND_REQUESTED)); + ADD_SIGNAL(MethodInfo(SIGNAL_REPLACE_REQUESTED)); +} + +//----------------------------------------------------------------------------- +const char *FindInFilesPanel::SIGNAL_RESULT_SELECTED = "result_selected"; +const char *FindInFilesPanel::SIGNAL_FILES_MODIFIED = "files_modified"; + +FindInFilesPanel::FindInFilesPanel() { + + _finder = memnew(FindInFiles); + _finder->connect(FindInFiles::SIGNAL_RESULT_FOUND, this, "_on_result_found"); + _finder->connect(FindInFiles::SIGNAL_FINISHED, this, "_on_finished"); + add_child(_finder); + + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); + vbc->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); + vbc->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); + vbc->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + add_child(vbc); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Label *find_label = memnew(Label); + find_label->set_text(TTR("Find: ")); + hbc->add_child(find_label); + + _search_text_label = memnew(Label); + _search_text_label->add_font_override("font", get_font("source", "EditorFonts")); + hbc->add_child(_search_text_label); + + _progress_bar = memnew(ProgressBar); + _progress_bar->set_h_size_flags(SIZE_EXPAND_FILL); + hbc->add_child(_progress_bar); + set_progress_visible(false); + + _status_label = memnew(Label); + hbc->add_child(_status_label); + + _cancel_button = memnew(Button); + _cancel_button->set_text(TTR("Cancel")); + _cancel_button->connect("pressed", this, "_on_cancel_button_clicked"); + _cancel_button->set_disabled(true); + hbc->add_child(_cancel_button); + + vbc->add_child(hbc); + } + + // In the future, this should be replaced by a more specific list container, + // which can highlight text regions and change opacity for enabled/disabled states + _results_display = memnew(ItemList); + _results_display->add_font_override("font", get_font("source", "EditorFonts")); + _results_display->set_v_size_flags(SIZE_EXPAND_FILL); + _results_display->connect("item_selected", this, "_on_result_selected"); + vbc->add_child(_results_display); + + { + _replace_container = memnew(HBoxContainer); + + Label *replace_label = memnew(Label); + replace_label->set_text(TTR("Replace: ")); + _replace_container->add_child(replace_label); + + _replace_line_edit = memnew(LineEdit); + _replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + _replace_line_edit->connect("text_changed", this, "_on_replace_text_changed"); + _replace_container->add_child(_replace_line_edit); + + _replace_all_button = memnew(Button); + _replace_all_button->set_text(TTR("Replace all (no undo)")); + _replace_all_button->connect("pressed", this, "_on_replace_all_clicked"); + _replace_container->add_child(_replace_all_button); + + _replace_container->hide(); + + vbc->add_child(_replace_container); + } +} + +void FindInFilesPanel::set_with_replace(bool with_replace) { + + _replace_container->set_visible(with_replace); +} + +void FindInFilesPanel::start_search() { + + _results_display->clear(); + _status_label->set_text(TTR("Searching...")); + _search_text_label->set_text(_finder->get_search_text()); + + set_process(true); + set_progress_visible(true); + + _finder->start(); + + update_replace_buttons(); + _cancel_button->set_disabled(false); +} + +void FindInFilesPanel::stop_search() { + + _finder->stop(); + + _status_label->set_text(""); + update_replace_buttons(); + set_progress_visible(false); + _cancel_button->set_disabled(true); +} + +void FindInFilesPanel::_notification(int p_what) { + if (p_what == NOTIFICATION_PROCESS) { + _progress_bar->set_as_ratio(_finder->get_progress()); + } +} + +void FindInFilesPanel::_on_result_found(String fpath, int line_number, int begin, int end, String text) { + + int i = _results_display->get_item_count(); + _results_display->add_item(fpath + ": " + String::num(line_number) + ": " + text.replace("\t", " ")); + _results_display->set_item_metadata(i, varray(fpath, line_number, begin, end)); +} + +void FindInFilesPanel::_on_finished() { + + _status_label->set_text(TTR("Search complete")); + update_replace_buttons(); + set_progress_visible(false); + _cancel_button->set_disabled(true); +} + +void FindInFilesPanel::_on_cancel_button_clicked() { + stop_search(); +} + +void FindInFilesPanel::_on_result_selected(int i) { + + Array meta = _results_display->get_item_metadata(i); + emit_signal(SIGNAL_RESULT_SELECTED, meta[0], meta[1], meta[2], meta[3]); +} + +void FindInFilesPanel::_on_replace_text_changed(String text) { + update_replace_buttons(); +} + +void FindInFilesPanel::_on_replace_all_clicked() { + + String replace_text = get_replace_text(); + ERR_FAIL_COND(replace_text.empty()); + + String last_fpath; + PoolIntArray locations; + PoolStringArray modified_files; + + for (int i = 0; i < _results_display->get_item_count(); ++i) { + + Array meta = _results_display->get_item_metadata(i); + + String fpath = meta[0]; + + // Results are sorted by file, so we can batch replaces + if (fpath != last_fpath) { + if (locations.size() != 0) { + apply_replaces_in_file(last_fpath, locations, replace_text); + modified_files.append(last_fpath); + locations.resize(0); + } + } + + locations.append(meta[1]); // line_number + locations.append(meta[2]); // begin + locations.append(meta[3]); // end + + last_fpath = fpath; + } + + if (locations.size() != 0) { + apply_replaces_in_file(last_fpath, locations, replace_text); + modified_files.append(last_fpath); + } + + // Hide replace bar so we can't trigger the action twice without doing a new search + set_with_replace(false); + + emit_signal(SIGNAL_FILES_MODIFIED, modified_files); +} + +// Same as get_line, but preserves line ending characters +class ConservativeGetLine { +public: + String get_line(FileAccess *f) { + + _line_buffer.clear(); + + CharType c = f->get_8(); + + while (!f->eof_reached()) { + + if (c == '\n') { + _line_buffer.push_back(c); + _line_buffer.push_back(0); + return String::utf8(_line_buffer.ptr()); + + } else if (c == '\0') { + _line_buffer.push_back(c); + return String::utf8(_line_buffer.ptr()); + + } else if (c != '\r') { + _line_buffer.push_back(c); + } + + c = f->get_8(); + } + + _line_buffer.push_back(0); + return String::utf8(_line_buffer.ptr()); + } + +private: + Vector<char> _line_buffer; +}; + +void FindInFilesPanel::apply_replaces_in_file(String fpath, PoolIntArray locations, String text) { + + ERR_FAIL_COND(locations.size() % 3 != 0); + + //print_line(String("Replacing {0} occurrences in {1}").format(varray(fpath, locations.size() / 3))); + + // If the file is already open, I assume the editor will reload it. + // If there are unsaved changes, the user will be asked on focus, + // however that means either loosing changes or loosing replaces. + + FileAccess *f = FileAccess::open(fpath, FileAccess::READ); + ERR_FAIL_COND(f == NULL); + + String buffer; + int current_line = 1; + + ConservativeGetLine conservative; + + String line = conservative.get_line(f); + + PoolIntArray::Read locations_read = locations.read(); + for (int i = 0; i < locations.size(); i += 3) { + + int repl_line_number = locations_read[i]; + int repl_begin = locations_read[i + 1]; + int repl_end = locations_read[i + 2]; + + while (current_line < repl_line_number) { + buffer += line; + line = conservative.get_line(f); + ++current_line; + } + + line = line.left(repl_begin) + text + line.right(repl_end); + } + + buffer += line; + + while (!f->eof_reached()) { + buffer += conservative.get_line(f); + } + + // Now the modified contents are in the buffer, rewrite the file with our changes + + Error err = f->reopen(fpath, FileAccess::WRITE); + ERR_FAIL_COND(err != OK); + + f->store_string(buffer); + + f->close(); +} + +String FindInFilesPanel::get_replace_text() { + return _replace_line_edit->get_text().strip_edges(); +} + +void FindInFilesPanel::update_replace_buttons() { + + String text = get_replace_text(); + bool disabled = text.empty() || _finder->is_searching(); + + _replace_all_button->set_disabled(disabled); +} + +void FindInFilesPanel::set_progress_visible(bool visible) { + _progress_bar->set_self_modulate(Color(1, 1, 1, visible ? 1 : 0)); +} + +void FindInFilesPanel::_bind_methods() { + + ClassDB::bind_method("_on_result_found", &FindInFilesPanel::_on_result_found); + ClassDB::bind_method("_on_finished", &FindInFilesPanel::_on_finished); + ClassDB::bind_method("_on_cancel_button_clicked", &FindInFilesPanel::_on_cancel_button_clicked); + ClassDB::bind_method("_on_result_selected", &FindInFilesPanel::_on_result_selected); + ClassDB::bind_method("_on_replace_text_changed", &FindInFilesPanel::_on_replace_text_changed); + ClassDB::bind_method("_on_replace_all_clicked", &FindInFilesPanel::_on_replace_all_clicked); + + ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_SELECTED, + PropertyInfo(Variant::STRING, "path"), + PropertyInfo(Variant::INT, "line_number"), + PropertyInfo(Variant::INT, "begin"), + PropertyInfo(Variant::INT, "end"))); + + ADD_SIGNAL(MethodInfo(SIGNAL_FILES_MODIFIED, PropertyInfo(Variant::STRING, "paths"))); +} diff --git a/editor/find_in_files.h b/editor/find_in_files.h new file mode 100644 index 0000000000..d57184960b --- /dev/null +++ b/editor/find_in_files.h @@ -0,0 +1,184 @@ +/*************************************************************************/ +/* find_in_files.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef FIND_IN_FILES_H +#define FIND_IN_FILES_H + +#include "scene/gui/dialogs.h" + +// Performs the actual search +class FindInFiles : public Node { + GDCLASS(FindInFiles, Node) +public: + static const char *SIGNAL_RESULT_FOUND; + static const char *SIGNAL_FINISHED; + + FindInFiles(); + + void set_search_text(String p_pattern); + void set_whole_words(bool p_whole_word); + void set_match_case(bool p_match_case); + void set_folder(String folder); + void set_filter(const Set<String> &exts); + + String get_search_text() const { return _pattern; } + + bool is_whole_words() const { return _whole_words; } + bool is_match_case() const { return _match_case; } + + void start(); + void stop(); + + bool is_searching() const { return _searching; } + float get_progress() const; + +protected: + void _notification(int p_notification); + + static void _bind_methods(); + +private: + void _process(); + void _iterate(); + void _scan_dir(String path, PoolStringArray &out_folders); + void _scan_file(String fpath); + + // Config + String _pattern; + Set<String> _extension_filter; + String _root_prefix; + String _root_dir; + bool _whole_words; + bool _match_case; + + // State + bool _searching; + String _current_dir; + Vector<PoolStringArray> _folders_stack; + Vector<String> _files_to_scan; + int _initial_files_count; +}; + +class LineEdit; +class CheckBox; +class FileDialog; + +// Prompts search parameters +class FindInFilesDialog : public WindowDialog { + GDCLASS(FindInFilesDialog, WindowDialog) +public: + static const char *SIGNAL_FIND_REQUESTED; + static const char *SIGNAL_REPLACE_REQUESTED; + + FindInFilesDialog(); + + void set_search_text(String text); + + String get_search_text() const; + bool is_match_case() const; + bool is_whole_words() const; + String get_folder() const; + Set<String> get_filter() const; + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +private: + void _on_folder_button_pressed(); + void _on_find_button_pressed(); + void _on_replace_button_pressed(); + void _on_folder_selected(String path); + void _on_search_text_modified(String text); + void _on_search_text_entered(String text); + + LineEdit *_search_text_line_edit; + LineEdit *_folder_line_edit; + Vector<CheckBox *> _filters; + CheckBox *_match_case_checkbox; + CheckBox *_whole_words_checkbox; + Button *_find_button; + Button *_replace_button; + FileDialog *_folder_dialog; +}; + +class Button; +class ItemList; +class ProgressBar; + +// Display search results +class FindInFilesPanel : public Control { + GDCLASS(FindInFilesPanel, Control) +public: + static const char *SIGNAL_RESULT_SELECTED; + static const char *SIGNAL_FILES_MODIFIED; + + FindInFilesPanel(); + + FindInFiles *get_finder() const { return _finder; } + + void set_with_replace(bool with_replace); + + void start_search(); + void stop_search(); + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +private: + void _on_result_found(String fpath, int line_number, int begin, int end, String text); + void _on_finished(); + void _on_cancel_button_clicked(); + void _on_result_selected(int i); + void _on_replace_text_changed(String text); + void _on_replace_all_clicked(); + + void apply_replaces_in_file(String fpath, PoolIntArray locations, String text); + + void update_replace_buttons(); + String get_replace_text(); + void set_progress_visible(bool visible); + + FindInFiles *_finder; + Label *_search_text_label; + ItemList *_results_display; + Label *_status_label; + Button *_cancel_button; + ProgressBar *_progress_bar; + + HBoxContainer *_replace_container; + LineEdit *_replace_line_edit; + Button *_replace_all_button; +}; + +#endif // FIND_IN_FILES_H diff --git a/editor/import/editor_import_collada.cpp b/editor/import/editor_import_collada.cpp index 863b13cbd7..c1e897a04c 100644 --- a/editor/import/editor_import_collada.cpp +++ b/editor/import/editor_import_collada.cpp @@ -480,7 +480,7 @@ Error ColladaImport::_create_material(const String &p_target) { } } - float roughness = Math::sqrt(1.0 - ((Math::log(effect.shininess) / Math::log(2.0)) / 8.0)); //not very right.. + float roughness = (effect.shininess - 1.0) / 510; material->set_roughness(roughness); if (effect.double_sided) { diff --git a/editor/import/editor_scene_importer_gltf.cpp b/editor/import/editor_scene_importer_gltf.cpp index 321d29f2f6..af79f9946a 100644 --- a/editor/import/editor_scene_importer_gltf.cpp +++ b/editor/import/editor_scene_importer_gltf.cpp @@ -1732,14 +1732,19 @@ void EditorSceneImporterGLTF::_generate_bone(GLTFState &state, int p_node, Vecto for (int i = 0; i < n->joints.size(); i++) { ERR_FAIL_COND(n->joints[i].skin < 0); - int bone_index = skeletons[n->joints[i].skin]->get_bone_count(); - skeletons[n->joints[i].skin]->add_bone(n->name); + int bone_index = n->joints[i].bone; + + Skeleton *s = skeletons[n->joints[i].skin]; + while (s->get_bone_count() <= bone_index) { + s->add_bone("Bone " + itos(s->get_bone_count())); + } + if (p_parent_bones.size()) { - skeletons[n->joints[i].skin]->set_bone_parent(bone_index, p_parent_bones[i]); + s->set_bone_parent(bone_index, p_parent_bones[i]); } - skeletons[n->joints[i].skin]->set_bone_rest(bone_index, state.skins[n->joints[i].skin].bones[n->joints[i].bone].inverse_bind.affine_inverse()); + s->set_bone_rest(bone_index, state.skins[n->joints[i].skin].bones[n->joints[i].bone].inverse_bind.affine_inverse()); - n->godot_nodes.push_back(skeletons[n->joints[i].skin]); + n->godot_nodes.push_back(s); n->joints[i].godot_bone_index = bone_index; parent_bones.push_back(bone_index); } diff --git a/editor/import/resource_importer_wav.cpp b/editor/import/resource_importer_wav.cpp index 03155b3a48..debdeb1c4a 100644 --- a/editor/import/resource_importer_wav.cpp +++ b/editor/import/resource_importer_wav.cpp @@ -304,17 +304,23 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s int limit_rate_hz = p_options["force/max_rate_hz"]; if (limit_rate && rate > limit_rate_hz && rate > 0 && frames > 0) { //resampleeee!!! - int new_data_frames = frames * limit_rate_hz / rate; + int new_data_frames = (int)(frames * (float)limit_rate_hz / (float)rate); + + print_line("\tresampling ratio: " + rtos((float)limit_rate_hz / (float)rate)); + print_line("\tnew frames: " + itos(new_data_frames)); + Vector<float> new_data; new_data.resize(new_data_frames * format_channels); for (int c = 0; c < format_channels; c++) { + float frac = .0f; + int ipos = 0; + for (int i = 0; i < new_data_frames; i++) { //simple cubic interpolation should be enough. - float pos = float(i) * frames / new_data_frames; - float mu = pos - Math::floor(pos); - int ipos = int(Math::floor(pos)); + + float mu = frac; float y0 = data[MAX(0, ipos - 1) * format_channels + c]; float y1 = data[ipos * format_channels + c]; @@ -330,14 +336,22 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s float res = (a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3); new_data[i * format_channels + c] = res; + + // update position and always keep fractional part within ]0...1] + // in order to avoid 32bit floating point precision errors + + frac += (float)rate / (float)limit_rate_hz; + int tpos = (int)Math::floor(frac); + ipos += tpos; + frac -= tpos; } } if (loop) { - - loop_begin = loop_begin * new_data_frames / frames; - loop_end = loop_end * new_data_frames / frames; + loop_begin = (int)(loop_begin * (float)new_data_frames / (float)frames); + loop_end = (int)(loop_end * (float)new_data_frames / (float)frames); } + data = new_data; rate = limit_rate_hz; frames = new_data_frames; diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 04c9246aed..7612f18aff 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -1679,10 +1679,10 @@ AnimationPlayerEditor::AnimationPlayerEditor(EditorNode *p_editor, AnimationPlay onion_skinning->get_popup()->add_separator(); onion_skinning->get_popup()->add_item(TTR("Depth"), -1); onion_skinning->get_popup()->set_item_disabled(onion_skinning->get_popup()->get_item_count() - 1, true); - onion_skinning->get_popup()->add_check_item(TTR("1 step"), ONION_SKINNING_1_STEP); + onion_skinning->get_popup()->add_radio_check_item(TTR("1 step"), ONION_SKINNING_1_STEP); onion_skinning->get_popup()->set_item_checked(onion_skinning->get_popup()->get_item_count() - 1, true); - onion_skinning->get_popup()->add_check_item(TTR("2 steps"), ONION_SKINNING_2_STEPS); - onion_skinning->get_popup()->add_check_item(TTR("3 steps"), ONION_SKINNING_3_STEPS); + onion_skinning->get_popup()->add_radio_check_item(TTR("2 steps"), ONION_SKINNING_2_STEPS); + onion_skinning->get_popup()->add_radio_check_item(TTR("3 steps"), ONION_SKINNING_3_STEPS); onion_skinning->get_popup()->add_separator(); onion_skinning->get_popup()->add_check_item(TTR("Differences Only"), ONION_SKINNING_DIFFERENCES_ONLY); onion_skinning->get_popup()->add_check_item(TTR("Force White Modulate"), ONION_SKINNING_FORCE_WHITE_MODULATE); diff --git a/editor/plugins/asset_library_editor_plugin.cpp b/editor/plugins/asset_library_editor_plugin.cpp index c203b4b81e..b72c9b25be 100644 --- a/editor/plugins/asset_library_editor_plugin.cpp +++ b/editor/plugins/asset_library_editor_plugin.cpp @@ -902,7 +902,7 @@ void EditorAssetLibrary::_search(int p_page) { } if (filter->get_text() != String()) { - args += "&filter=" + filter->get_text().http_escape(); + args += "&filter=" + filter->get_text().percent_encode(); } if (p_page > 0) { diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 0184bff050..b8d0958c99 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -1672,8 +1672,6 @@ bool CanvasItemEditor::_gui_input_select(const Ref<InputEvent> &p_event) { selection_menu_additive_selection = b->get_shift(); selection_menu->set_global_position(b->get_global_position()); selection_menu->popup(); - selection_menu->call_deferred("grab_click_focus"); - selection_menu->set_invalidate_click_until_motion(); return true; } } diff --git a/editor/plugins/item_list_editor_plugin.cpp b/editor/plugins/item_list_editor_plugin.cpp index 5020f5b851..8b44f672b0 100644 --- a/editor/plugins/item_list_editor_plugin.cpp +++ b/editor/plugins/item_list_editor_plugin.cpp @@ -42,9 +42,18 @@ bool ItemListPlugin::_set(const StringName &p_name, const Variant &p_value) { set_item_text(idx, p_value); else if (what == "icon") set_item_icon(idx, p_value); - else if (what == "checkable") - set_item_checkable(idx, p_value); - else if (what == "checked") + else if (what == "checkable") { + // This keeps compatibility to/from versions where this property was a boolean, before radio buttons + switch ((int)p_value) { + case 0: + case 1: + set_item_checkable(idx, p_value); + break; + case 2: + set_item_radio_checkable(idx, true); + break; + } + } else if (what == "checked") set_item_checked(idx, p_value); else if (what == "id") set_item_id(idx, p_value); @@ -68,9 +77,14 @@ bool ItemListPlugin::_get(const StringName &p_name, Variant &r_ret) const { r_ret = get_item_text(idx); else if (what == "icon") r_ret = get_item_icon(idx); - else if (what == "checkable") - r_ret = is_item_checkable(idx); - else if (what == "checked") + else if (what == "checkable") { + // This keeps compatibility to/from versions where this property was a boolean, before radio buttons + if (!is_item_checkable(idx)) { + r_ret = 0; + } else { + r_ret = is_item_radio_checkable(idx) ? 2 : 1; + } + } else if (what == "checked") r_ret = is_item_checked(idx); else if (what == "id") r_ret = get_item_id(idx); @@ -95,7 +109,7 @@ void ItemListPlugin::_get_property_list(List<PropertyInfo> *p_list) const { int flags = get_flags(); if (flags & FLAG_CHECKABLE) { - p_list->push_back(PropertyInfo(Variant::BOOL, base + "checkable")); + p_list->push_back(PropertyInfo(Variant::BOOL, base + "checkable", PROPERTY_HINT_ENUM, "No,As checkbox,As radio button")); p_list->push_back(PropertyInfo(Variant::BOOL, base + "checked")); } diff --git a/editor/plugins/item_list_editor_plugin.h b/editor/plugins/item_list_editor_plugin.h index bd7d3db10d..d6a071b9b9 100644 --- a/editor/plugins/item_list_editor_plugin.h +++ b/editor/plugins/item_list_editor_plugin.h @@ -74,7 +74,9 @@ public: virtual Ref<Texture> get_item_icon(int p_idx) const { return Ref<Texture>(); }; virtual void set_item_checkable(int p_idx, bool p_check) {} + virtual void set_item_radio_checkable(int p_idx, bool p_check) {} virtual bool is_item_checkable(int p_idx) const { return false; }; + virtual bool is_item_radio_checkable(int p_idx) const { return false; }; virtual void set_item_checked(int p_idx, bool p_checked) {} virtual bool is_item_checked(int p_idx) const { return false; }; @@ -145,7 +147,9 @@ public: virtual Ref<Texture> get_item_icon(int p_idx) const { return pp->get_item_icon(p_idx); } virtual void set_item_checkable(int p_idx, bool p_check) { pp->set_item_as_checkable(p_idx, p_check); } + virtual void set_item_radio_checkable(int p_idx, bool p_check) { pp->set_item_as_radio_checkable(p_idx, p_check); } virtual bool is_item_checkable(int p_idx) const { return pp->is_item_checkable(p_idx); } + virtual bool is_item_radio_checkable(int p_idx) const { return pp->is_item_radio_checkable(p_idx); } virtual void set_item_checked(int p_idx, bool p_checked) { pp->set_item_checked(p_idx, p_checked); } virtual bool is_item_checked(int p_idx) const { return pp->is_item_checked(p_idx); } diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 2ce36ee8d5..9193b3fbbf 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -39,9 +39,11 @@ #include "core/project_settings.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "editor/find_in_files.h" #include "editor/node_dock.h" #include "editor/script_editor_debugger.h" #include "scene/main/viewport.h" +#include "script_text_editor.h" /*** SCRIPT EDITOR ****/ @@ -54,6 +56,8 @@ void ScriptEditorBase::_bind_methods() { ADD_SIGNAL(MethodInfo("request_open_script_at_line", PropertyInfo(Variant::OBJECT, "script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("request_save_history")); ADD_SIGNAL(MethodInfo("go_to_help", PropertyInfo(Variant::STRING, "what"))); + // TODO This signal is no use for VisualScript... + ADD_SIGNAL(MethodInfo("search_in_files_requested", PropertyInfo(Variant::STRING, "text"))); } static bool _can_open_in_editor(Script *p_script) { @@ -298,15 +302,9 @@ void ScriptEditor::_script_created(Ref<Script> p_script) { void ScriptEditor::_goto_script_line2(int p_line) { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) - return; - - current->goto_line(p_line); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->goto_line(p_line); } void ScriptEditor::_goto_script_line(REF p_script, int p_line) { @@ -316,19 +314,22 @@ void ScriptEditor::_goto_script_line(REF p_script, int p_line) { if (edit(p_script, p_line, 0)) { editor->push_item(p_script.ptr()); - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) - return; - - current->goto_line(p_line, true); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->goto_line(p_line, true); } } } +ScriptEditorBase *ScriptEditor::_get_current_editor() const { + + int selected = tab_container->get_current_tab(); + if (selected < 0 || selected >= tab_container->get_child_count()) + return NULL; + + return Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); +} + void ScriptEditor::_update_history_arrows() { script_back->set_disabled(history_pos <= 0); @@ -587,7 +588,7 @@ void ScriptEditor::_close_docs_tab() { } void ScriptEditor::_copy_script_path() { - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_child(tab_container->get_current_tab())); + ScriptEditorBase *se = _get_current_editor(); Ref<Script> script = se->get_edited_script(); OS::get_singleton()->set_clipboard(script->get_path()); } @@ -819,11 +820,8 @@ void ScriptEditor::_file_dialog_action(String p_file) { Ref<Script> ScriptEditor::_get_current_script() { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return NULL; + ScriptEditorBase *current = _get_current_editor(); - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); if (current) { return current->get_edited_script(); } else { @@ -939,11 +937,7 @@ void ScriptEditor::_menu_option(int p_option) { } } - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); + ScriptEditorBase *current = _get_current_editor(); if (current) { switch (p_option) { @@ -1033,7 +1027,7 @@ void ScriptEditor::_menu_option(int p_option) { _copy_script_path(); } break; case SHOW_IN_FILE_SYSTEM: { - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_child(tab_container->get_current_tab())); + ScriptEditorBase *se = _get_current_editor(); Ref<Script> script = se->get_edited_script(); FileSystemDock *file_system_dock = EditorNode::get_singleton()->get_filesystem_dock(); file_system_dock->navigate_to_path(script->get_path()); @@ -1223,6 +1217,17 @@ void ScriptEditor::_notification(int p_what) { recent_scripts->set_as_minsize(); } break; + case CanvasItem::NOTIFICATION_VISIBILITY_CHANGED: { + + if (is_visible()) { + find_in_files_button->show(); + } else { + find_in_files->hide(); + find_in_files_button->hide(); + } + + } break; + default: break; } @@ -1230,15 +1235,11 @@ void ScriptEditor::_notification(int p_what) { bool ScriptEditor::can_take_away_focus() const { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return true; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) + ScriptEditorBase *current = _get_current_editor(); + if (current) + return current->can_lose_focus_on_node_selection(); + else return true; - - return current->can_lose_focus_on_node_selection(); } void ScriptEditor::close_builtin_scripts_from_scene(const String &p_scene) { @@ -1315,20 +1316,13 @@ void ScriptEditor::ensure_focus_current() { if (!is_inside_tree()) return; - int cidx = tab_container->get_current_tab(); - if (cidx < 0 || cidx >= tab_container->get_tab_count()) - return; - - Control *c = Object::cast_to<Control>(tab_container->get_child(cidx)); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(c); - if (!se) - return; - se->ensure_focus(); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->ensure_focus(); } void ScriptEditor::_members_overview_selected(int p_idx) { - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { return; } @@ -1362,18 +1356,12 @@ void ScriptEditor::ensure_select_current() { if (tab_container->get_child_count() && tab_container->get_current_tab() >= 0) { - Node *current = tab_container->get_child(tab_container->get_current_tab()); - - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (se) { - Ref<Script> script = se->get_edited_script(); - if (!grab_focus_block && is_visible_in_tree()) se->ensure_focus(); } - - EditorHelp *eh = Object::cast_to<EditorHelp>(current); } _update_selected_editor_menu(); @@ -1413,12 +1401,7 @@ struct _ScriptEditorItemData { void ScriptEditor::_update_members_overview_visibility() { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { members_overview->set_visible(false); return; @@ -1434,12 +1417,7 @@ void ScriptEditor::_update_members_overview_visibility() { void ScriptEditor::_update_members_overview() { members_overview->clear(); - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { return; } @@ -1813,6 +1791,7 @@ bool ScriptEditor::edit(const Ref<Script> &p_script, int p_line, int p_col, bool se->connect("request_open_script_at_line", this, "_goto_script_line"); se->connect("go_to_help", this, "_help_class_goto"); se->connect("request_save_history", this, "_save_history"); + se->connect("search_in_files_requested", this, "_on_find_in_files_requested"); //test for modification, maybe the script was not edited but was loaded @@ -2530,6 +2509,48 @@ void ScriptEditor::_script_changed() { NodeDock::singleton->update_lists(); } +void ScriptEditor::_on_find_in_files_requested(String text) { + + find_in_files_dialog->set_search_text(text); + find_in_files_dialog->popup_centered_minsize(); +} + +void ScriptEditor::_on_find_in_files_result_selected(String fpath, int line_number, int begin, int end) { + + Ref<Resource> res = ResourceLoader::load(fpath); + edit(res); + + ScriptEditorBase *seb = _get_current_editor(); + + ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(seb); + if (ste) { + ste->goto_line_selection(line_number - 1, begin, end); + } +} + +void ScriptEditor::_start_find_in_files(bool with_replace) { + + FindInFiles *f = find_in_files->get_finder(); + + f->set_search_text(find_in_files_dialog->get_search_text()); + f->set_match_case(find_in_files_dialog->is_match_case()); + f->set_whole_words(find_in_files_dialog->is_match_case()); + f->set_folder(find_in_files_dialog->get_folder()); + f->set_filter(find_in_files_dialog->get_filter()); + + find_in_files->set_with_replace(with_replace); + find_in_files->start_search(); + + find_in_files_button->set_pressed(true); + find_in_files->show(); +} + +void ScriptEditor::_on_find_in_files_modified_files(PoolStringArray paths) { + + _test_script_times_on_disk(); + _update_modified_scripts_for_external_editor(); +} + void ScriptEditor::_bind_methods() { ClassDB::bind_method("_file_dialog_action", &ScriptEditor::_file_dialog_action); @@ -2577,6 +2598,10 @@ void ScriptEditor::_bind_methods() { ClassDB::bind_method("_script_list_gui_input", &ScriptEditor::_script_list_gui_input); ClassDB::bind_method("_script_changed", &ScriptEditor::_script_changed); ClassDB::bind_method("_update_recent_scripts", &ScriptEditor::_update_recent_scripts); + ClassDB::bind_method("_on_find_in_files_requested", &ScriptEditor::_on_find_in_files_requested); + ClassDB::bind_method("_start_find_in_files", &ScriptEditor::_start_find_in_files); + ClassDB::bind_method("_on_find_in_files_result_selected", &ScriptEditor::_on_find_in_files_result_selected); + ClassDB::bind_method("_on_find_in_files_modified_files", &ScriptEditor::_on_find_in_files_modified_files); ClassDB::bind_method(D_METHOD("get_drag_data_fw", "point", "from"), &ScriptEditor::get_drag_data_fw); ClassDB::bind_method(D_METHOD("can_drop_data_fw", "point", "data", "from"), &ScriptEditor::can_drop_data_fw); @@ -2838,6 +2863,19 @@ ScriptEditor::ScriptEditor(EditorNode *p_editor) { add_child(help_index); help_index->connect("open_class", this, "_help_class_open"); + find_in_files_dialog = memnew(FindInFilesDialog); + find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_FIND_REQUESTED, this, "_start_find_in_files", varray(false)); + find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_REPLACE_REQUESTED, this, "_start_find_in_files", varray(true)); + add_child(find_in_files_dialog); + find_in_files = memnew(FindInFilesPanel); + find_in_files_button = editor->add_bottom_panel_item(TTR("Search results"), find_in_files); + find_in_files_button->set_tooltip(TTR("Search in files")); + find_in_files->set_custom_minimum_size(Size2(0, 200)); + find_in_files->connect(FindInFilesPanel::SIGNAL_RESULT_SELECTED, this, "_on_find_in_files_result_selected"); + find_in_files->connect(FindInFilesPanel::SIGNAL_FILES_MODIFIED, this, "_on_find_in_files_modified_files"); + find_in_files->hide(); + find_in_files_button->hide(); + history_pos = -1; //debugger_gui->hide(); diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index f947351089..9f37b18d7d 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -119,6 +119,8 @@ typedef SyntaxHighlighter *(*CreateSyntaxHighlighterFunc)(); typedef ScriptEditorBase *(*CreateScriptEditorFunc)(const Ref<Script> &p_script); class EditorScriptCodeCompletionCache; +class FindInFilesDialog; +class FindInFilesPanel; class ScriptEditor : public PanelContainer { @@ -217,6 +219,10 @@ class ScriptEditor : public PanelContainer { ToolButton *script_back; ToolButton *script_forward; + FindInFilesDialog *find_in_files_dialog; + FindInFilesPanel *find_in_files; + Button *find_in_files_button; + enum { SCRIPT_EDITOR_FUNC_MAX = 32, SYNTAX_HIGHLIGHTER_FUNC_MAX = 32 @@ -304,6 +310,8 @@ class ScriptEditor : public PanelContainer { void _update_window_menu(); void _script_created(Ref<Script> p_script); + ScriptEditorBase *_get_current_editor() const; + void _save_layout(); void _editor_settings_changed(); void _autosave_scripts(); @@ -359,6 +367,11 @@ class ScriptEditor : public PanelContainer { Ref<Script> _get_current_script(); Array _get_open_scripts() const; + void _on_find_in_files_requested(String text); + void _on_find_in_files_result_selected(String fpath, int line_number, int begin, int end); + void _start_find_in_files(bool with_replace); + void _on_find_in_files_modified_files(PoolStringArray paths); + static void _open_script_request(const String &p_path); static ScriptEditor *script_editor; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 711a313902..bcc575a7ac 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -529,6 +529,14 @@ void ScriptTextEditor::goto_line(int p_line, bool p_with_error) { tx->call_deferred("cursor_set_line", p_line); } +void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { + TextEdit *tx = code_editor->get_text_edit(); + tx->unfold_line(p_line); + tx->call_deferred("cursor_set_line", p_line); + tx->call_deferred("cursor_set_column", p_begin); + tx->select(p_line, p_begin, p_line, p_end); +} + void ScriptTextEditor::ensure_focus() { code_editor->get_text_edit()->grab_focus(); @@ -1173,6 +1181,15 @@ void ScriptTextEditor::_edit_option(int p_op) { code_editor->get_find_replace_bar()->popup_replace(); } break; + case SEARCH_IN_FILES: { + + String selected_text = code_editor->get_text_edit()->get_selection_text(); + + // Yep, because it doesn't make sense to instance this dialog for every single script open... + // So this will be delegated to the ScriptEditor + emit_signal("search_in_files_requested", selected_text); + + } break; case SEARCH_LOCATE_FUNCTION: { quick_open->popup(get_functions()); @@ -1660,6 +1677,8 @@ ScriptTextEditor::ScriptTextEditor() { search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_previous"), SEARCH_FIND_PREV); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace"), SEARCH_REPLACE); search_menu->get_popup()->add_separator(); + search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_in_files"), SEARCH_IN_FILES); + search_menu->get_popup()->add_separator(); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_line"), SEARCH_GOTO_LINE); search_menu->get_popup()->add_separator(); @@ -1739,7 +1758,9 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/find_previous", TTR("Find Previous"), KEY_MASK_SHIFT | KEY_F3); ED_SHORTCUT("script_text_editor/replace", TTR("Replace.."), KEY_MASK_CMD | KEY_R); - ED_SHORTCUT("script_text_editor/goto_function", TTR("Goto Function.."), KEY_MASK_SHIFT | KEY_MASK_CMD | KEY_F); + ED_SHORTCUT("script_text_editor/find_in_files", TTR("Find in files..."), KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_F); + + ED_SHORTCUT("script_text_editor/goto_function", TTR("Goto Function.."), KEY_MASK_ALT | KEY_MASK_CMD | KEY_F); ED_SHORTCUT("script_text_editor/goto_line", TTR("Goto Line.."), KEY_MASK_CMD | KEY_L); ED_SHORTCUT("script_text_editor/contextual_help", TTR("Contextual Help"), KEY_MASK_SHIFT | KEY_F1); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index eb52d2593a..a93e1a6fa8 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -106,6 +106,7 @@ class ScriptTextEditor : public ScriptEditorBase { SEARCH_REPLACE, SEARCH_LOCATE_FUNCTION, SEARCH_GOTO_LINE, + SEARCH_IN_FILES, DEBUG_TOGGLE_BREAKPOINT, DEBUG_REMOVE_ALL_BREAKPOINTS, DEBUG_GOTO_NEXT_BREAKPOINT, @@ -170,6 +171,7 @@ public: virtual void tag_saved_version(); virtual void goto_line(int p_line, bool p_with_error = false); + void goto_line_selection(int p_line, int p_begin, int p_end); virtual void reload(bool p_soft); virtual void get_breakpoints(List<int> *p_breakpoints); diff --git a/editor/plugins/shader_graph_editor_plugin.cpp b/editor/plugins/shader_graph_editor_plugin.cpp index 59085c203f..e1d28cc215 100644 --- a/editor/plugins/shader_graph_editor_plugin.cpp +++ b/editor/plugins/shader_graph_editor_plugin.cpp @@ -2769,8 +2769,6 @@ void ShaderGraphEditor::_popup_requested(const Vector2 &p_position) popup->set_global_position(p_position); popup->set_size( Size2( 200, 0) ); popup->popup(); - popup->call_deferred("grab_click_focus"); - popup->set_invalidate_click_until_motion(); } void ShaderGraphEditor::_notification(int p_what) { diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp index e484140527..9d7c582e0e 100644 --- a/editor/plugins/spatial_editor_plugin.cpp +++ b/editor/plugins/spatial_editor_plugin.cpp @@ -886,8 +886,6 @@ void SpatialEditorViewport::_list_select(Ref<InputEventMouseButton> b) { selection_menu->set_global_position(b->get_global_position()); selection_menu->popup(); - selection_menu->call_deferred("grab_click_focus"); - selection_menu->set_invalidate_click_until_motion(); } } @@ -3353,14 +3351,14 @@ SpatialEditorViewport::SpatialEditorViewport(SpatialEditor *p_spatial_editor, Ed view_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/front_view"), VIEW_FRONT); view_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/rear_view"), VIEW_REAR); view_menu->get_popup()->add_separator(); - view_menu->get_popup()->add_check_item(TTR("Perspective") + " (" + ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal")->get_as_text() + ")", VIEW_PERSPECTIVE); - view_menu->get_popup()->add_check_item(TTR("Orthogonal") + " (" + ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal")->get_as_text() + ")", VIEW_ORTHOGONAL); + view_menu->get_popup()->add_radio_check_item(TTR("Perspective") + " (" + ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal")->get_as_text() + ")", VIEW_PERSPECTIVE); + view_menu->get_popup()->add_radio_check_item(TTR("Orthogonal") + " (" + ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal")->get_as_text() + ")", VIEW_ORTHOGONAL); view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), true); view_menu->get_popup()->add_separator(); - view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_normal", TTR("Display Normal")), VIEW_DISPLAY_NORMAL); - view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_wireframe", TTR("Display Wireframe")), VIEW_DISPLAY_WIREFRAME); - view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_overdraw", TTR("Display Overdraw")), VIEW_DISPLAY_OVERDRAW); - view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_unshaded", TTR("Display Unshaded")), VIEW_DISPLAY_SHADELESS); + view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_normal", TTR("Display Normal")), VIEW_DISPLAY_NORMAL); + view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_wireframe", TTR("Display Wireframe")), VIEW_DISPLAY_WIREFRAME); + view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_overdraw", TTR("Display Overdraw")), VIEW_DISPLAY_OVERDRAW); + view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_unshaded", TTR("Display Unshaded")), VIEW_DISPLAY_SHADELESS); view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_DISPLAY_NORMAL), true); view_menu->get_popup()->add_separator(); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_environment", TTR("View Environment")), VIEW_ENVIRONMENT); @@ -5111,12 +5109,12 @@ SpatialEditor::SpatialEditor(EditorNode *p_editor) { accept = memnew(AcceptDialog); editor->get_gui_base()->add_child(accept); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/1_viewport", TTR("1 Viewport"), KEY_MASK_CMD + KEY_1), MENU_VIEW_USE_1_VIEWPORT); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports", TTR("2 Viewports"), KEY_MASK_CMD + KEY_2), MENU_VIEW_USE_2_VIEWPORTS); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports_alt", TTR("2 Viewports (Alt)"), KEY_MASK_ALT + KEY_MASK_CMD + KEY_2), MENU_VIEW_USE_2_VIEWPORTS_ALT); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports", TTR("3 Viewports"), KEY_MASK_CMD + KEY_3), MENU_VIEW_USE_3_VIEWPORTS); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports_alt", TTR("3 Viewports (Alt)"), KEY_MASK_ALT + KEY_MASK_CMD + KEY_3), MENU_VIEW_USE_3_VIEWPORTS_ALT); - p->add_check_shortcut(ED_SHORTCUT("spatial_editor/4_viewports", TTR("4 Viewports"), KEY_MASK_CMD + KEY_4), MENU_VIEW_USE_4_VIEWPORTS); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/1_viewport", TTR("1 Viewport"), KEY_MASK_CMD + KEY_1), MENU_VIEW_USE_1_VIEWPORT); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports", TTR("2 Viewports"), KEY_MASK_CMD + KEY_2), MENU_VIEW_USE_2_VIEWPORTS); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports_alt", TTR("2 Viewports (Alt)"), KEY_MASK_ALT + KEY_MASK_CMD + KEY_2), MENU_VIEW_USE_2_VIEWPORTS_ALT); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports", TTR("3 Viewports"), KEY_MASK_CMD + KEY_3), MENU_VIEW_USE_3_VIEWPORTS); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports_alt", TTR("3 Viewports (Alt)"), KEY_MASK_ALT + KEY_MASK_CMD + KEY_3), MENU_VIEW_USE_3_VIEWPORTS_ALT); + p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/4_viewports", TTR("4 Viewports"), KEY_MASK_CMD + KEY_4), MENU_VIEW_USE_4_VIEWPORTS); p->add_separator(); p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTR("View Origin")), MENU_VIEW_ORIGIN); diff --git a/editor/plugins/texture_region_editor_plugin.cpp b/editor/plugins/texture_region_editor_plugin.cpp index 4367fe8976..5ba3931689 100644 --- a/editor/plugins/texture_region_editor_plugin.cpp +++ b/editor/plugins/texture_region_editor_plugin.cpp @@ -805,12 +805,10 @@ TextureRegionEditor::TextureRegionEditor(EditorNode *p_editor) { snap_mode_button->set_text(TTR("<None>")); PopupMenu *p = snap_mode_button->get_popup(); p->set_hide_on_checkable_item_selection(false); - p->add_item(TTR("<None>"), 0); - p->add_item(TTR("Pixel Snap"), 1); - p->add_item(TTR("Grid Snap"), 2); - p->add_item(TTR("Auto Slice"), 3); - for (int i = 0; i < 4; i++) - p->set_item_as_checkable(i, true); + p->add_radio_check_item(TTR("<None>"), 0); + p->add_radio_check_item(TTR("Pixel Snap"), 1); + p->add_radio_check_item(TTR("Grid Snap"), 2); + p->add_radio_check_item(TTR("Auto Slice"), 3); p->set_item_checked(0, true); p->connect("id_pressed", this, "_set_snap_mode"); hb_grid = memnew(HBoxContainer); diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 295d9439ad..550dfb3ae1 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -692,6 +692,10 @@ ThemeEditor::ThemeEditor() { test_menu_button->get_popup()->add_check_item(TTR("Check Item")); test_menu_button->get_popup()->add_check_item(TTR("Checked Item")); test_menu_button->get_popup()->set_item_checked(2, true); + test_menu_button->get_popup()->add_separator(); + test_menu_button->get_popup()->add_check_item(TTR("Radio Item")); + test_menu_button->get_popup()->add_radio_check_item(TTR("Checked Radio Item")); + test_menu_button->get_popup()->set_item_checked(5, true); first_vb->add_child(test_menu_button); OptionButton *test_option_button = memnew(OptionButton); diff --git a/editor/plugins/tile_set_editor_plugin.cpp b/editor/plugins/tile_set_editor_plugin.cpp index 2311439728..41692e805f 100644 --- a/editor/plugins/tile_set_editor_plugin.cpp +++ b/editor/plugins/tile_set_editor_plugin.cpp @@ -278,10 +278,11 @@ void TileSetEditor::_changed_callback(Object *p_changed, const char *p_prop) { preview->set_region_rect(tileset->tile_get_region(get_current_tile())); } else if (p_prop == StringName("name")) { update_tile_list_icon(); - } else if (p_prop == StringName("texture") || p_prop == StringName("tile_mode")) { + } else if (p_prop == StringName("texture") || p_prop == StringName("modulate") || p_prop == StringName("tile_mode")) { _on_tile_list_selected(get_current_tile()); workspace->update(); preview->set_texture(tileset->tile_get_texture(get_current_tile())); + preview->set_modulate(tileset->tile_get_modulate(get_current_tile())); preview->set_region_rect(tileset->tile_get_region(get_current_tile())); if (tileset->tile_get_tile_mode(get_current_tile()) == TileSet::AUTO_TILE) property_editor->show(); @@ -578,6 +579,7 @@ void TileSetEditor::_on_tile_list_selected(int p_index) { if (get_current_tile() >= 0) { current_item_index = p_index; preview->set_texture(tileset->tile_get_texture(get_current_tile())); + preview->set_modulate(tileset->tile_get_modulate(get_current_tile())); preview->set_region_rect(tileset->tile_get_region(get_current_tile())); workspace->set_custom_minimum_size(tileset->tile_get_region(get_current_tile()).size); update_workspace_tile_mode(); @@ -1736,6 +1738,7 @@ void TileSetEditor::update_tile_list() { region.position += pos; } tile_list->set_item_icon_region(tile_list->get_item_count() - 1, region); + tile_list->set_item_icon_modulate(tile_list->get_item_count() - 1, tileset->tile_get_modulate(E->get())); } if (tile_list->get_item_count() > 0 && selected_tile < tile_list->get_item_count()) { tile_list->select(selected_tile); @@ -1763,6 +1766,7 @@ void TileSetEditor::update_tile_list_icon() { tile_list->set_item_metadata(current_idx, E->get()); tile_list->set_item_icon(current_idx, tileset->tile_get_texture(E->get())); tile_list->set_item_icon_region(current_idx, region); + tile_list->set_item_icon_modulate(current_idx, tileset->tile_get_modulate(E->get())); tile_list->set_item_text(current_idx, tileset->tile_get_name(E->get())); current_idx += 1; } diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 75523cd843..6e3be343ba 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -31,6 +31,7 @@ #include "project_settings_editor.h" #include "core/global_constants.h" +#include "core/input_map.h" #include "core/os/keyboard.h" #include "core/project_settings.h" #include "core/translation.h" @@ -212,7 +213,7 @@ void ProjectSettingsEditor::_device_input_add() { Ref<InputEventMouseButton> mb; mb.instance(); mb->set_button_index(device_index->get_selected() + 1); - mb->set_device(device_id->get_value()); + mb->set_device(_get_current_device()); for (int i = 0; i < arr.size(); i++) { @@ -233,7 +234,7 @@ void ProjectSettingsEditor::_device_input_add() { jm.instance(); jm->set_axis(device_index->get_selected() >> 1); jm->set_axis_value(device_index->get_selected() & 1 ? 1 : -1); - jm->set_device(device_id->get_value()); + jm->set_device(_get_current_device()); for (int i = 0; i < arr.size(); i++) { @@ -254,7 +255,7 @@ void ProjectSettingsEditor::_device_input_add() { jb.instance(); jb->set_button_index(device_index->get_selected()); - jb->set_device(device_id->get_value()); + jb->set_device(_get_current_device()); for (int i = 0; i < arr.size(); i++) { @@ -289,6 +290,20 @@ void ProjectSettingsEditor::_device_input_add() { _show_last_added(ie, name); } +void ProjectSettingsEditor::_set_current_device(int i_device) { + device_id->select(i_device + 1); +} + +int ProjectSettingsEditor::_get_current_device() { + return device_id->get_selected() - 1; +} + +String ProjectSettingsEditor::_get_device_string(int i_device) { + if (i_device == InputMap::ALL_DEVICES) + return TTR("All Devices"); + return TTR("Device") + " " + itos(i_device); +} + void ProjectSettingsEditor::_press_a_key_confirm() { if (last_wait_for_key.is_null()) @@ -422,10 +437,10 @@ void ProjectSettingsEditor::_add_item(int p_item, Ref<InputEvent> p_exiting_even Ref<InputEventMouseButton> mb = p_exiting_event; if (mb.is_valid()) { device_index->select(mb->get_button_index() - 1); - device_id->set_value(mb->get_device()); + _set_current_device(mb->get_device()); device_input->get_ok()->set_text(TTR("Change")); } else { - device_id->set_value(0); + _set_current_device(0); device_input->get_ok()->set_text(TTR("Add")); } } break; @@ -443,10 +458,10 @@ void ProjectSettingsEditor::_add_item(int p_item, Ref<InputEvent> p_exiting_even Ref<InputEventJoypadMotion> jm = p_exiting_event; if (jm.is_valid()) { device_index->select(jm->get_axis() * 2 + (jm->get_axis_value() > 0 ? 1 : 0)); - device_id->set_value(jm->get_device()); + _set_current_device(jm->get_device()); device_input->get_ok()->set_text(TTR("Change")); } else { - device_id->set_value(0); + _set_current_device(0); device_input->get_ok()->set_text(TTR("Add")); } } break; @@ -464,10 +479,10 @@ void ProjectSettingsEditor::_add_item(int p_item, Ref<InputEvent> p_exiting_even Ref<InputEventJoypadButton> jb = p_exiting_event; if (jb.is_valid()) { device_index->select(jb->get_button_index()); - device_id->set_value(jb->get_device()); + _set_current_device(jb->get_device()); device_input->get_ok()->set_text(TTR("Change")); } else { - device_id->set_value(0); + _set_current_device(0); device_input->get_ok()->set_text(TTR("Add")); } @@ -676,7 +691,7 @@ void ProjectSettingsEditor::_update_actions() { if (jb.is_valid()) { - String str = TTR("Device") + " " + itos(jb->get_device()) + ", " + TTR("Button") + " " + itos(jb->get_button_index()); + String str = _get_device_string(jb->get_device()) + ", " + TTR("Button") + " " + itos(jb->get_button_index()); if (jb->get_button_index() >= 0 && jb->get_button_index() < JOY_BUTTON_MAX) str += String() + " (" + _button_names[jb->get_button_index()] + ")."; else @@ -689,7 +704,7 @@ void ProjectSettingsEditor::_update_actions() { Ref<InputEventMouseButton> mb = ie; if (mb.is_valid()) { - String str = TTR("Device") + " " + itos(mb->get_device()) + ", "; + String str = _get_device_string(mb->get_device()) + ", "; switch (mb->get_button_index()) { case BUTTON_LEFT: str += TTR("Left Button."); break; case BUTTON_RIGHT: str += TTR("Right Button."); break; @@ -710,7 +725,7 @@ void ProjectSettingsEditor::_update_actions() { int ax = jm->get_axis(); int n = 2 * ax + (jm->get_axis_value() < 0 ? 0 : 1); String desc = _axis_names[n]; - String str = TTR("Device") + " " + itos(jm->get_device()) + ", " + TTR("Axis") + " " + itos(ax) + " " + (jm->get_axis_value() < 0 ? "-" : "+") + desc + "."; + String str = _get_device_string(jm->get_device()) + ", " + TTR("Axis") + " " + itos(ax) + " " + (jm->get_axis_value() < 0 ? "-" : "+") + desc + "."; action->set_text(0, str); action->set_icon(0, get_icon("JoyAxis", "EditorIcons")); } @@ -1781,8 +1796,10 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { l->set_text(TTR("Device:")); vbc_left->add_child(l); - device_id = memnew(SpinBox); - device_id->set_value(0); + device_id = memnew(OptionButton); + for (int i = -1; i < 8; i++) + device_id->add_item(_get_device_string(i)); + _set_current_device(0); vbc_left->add_child(device_id); VBoxContainer *vbc_right = memnew(VBoxContainer); diff --git a/editor/project_settings_editor.h b/editor/project_settings_editor.h index 9364b0ff2c..0ced88d7f6 100644 --- a/editor/project_settings_editor.h +++ b/editor/project_settings_editor.h @@ -80,7 +80,7 @@ class ProjectSettingsEditor : public AcceptDialog { ConfirmationDialog *press_a_key; Label *press_a_key_label; ConfirmationDialog *device_input; - SpinBox *device_id; + OptionButton *device_id; OptionButton *device_index; Label *device_index_label; MenuButton *popup_copy_to_feature; @@ -170,6 +170,10 @@ protected: void _notification(int p_what); static void _bind_methods(); + int _get_current_device(); + void _set_current_device(int i_device); + String _get_device_string(int i_device); + public: void add_translation(const String &p_translation); static ProjectSettingsEditor *get_singleton() { return singleton; } diff --git a/editor/property_editor.cpp b/editor/property_editor.cpp index 043add046e..4bd70d0c29 100644 --- a/editor/property_editor.cpp +++ b/editor/property_editor.cpp @@ -285,6 +285,11 @@ void CustomPropertyEditor::_menu_option(int p_which) { } Object *obj = ClassDB::instance(intype); + + if (!obj) { + obj = EditorNode::get_editor_data().instance_custom_type(intype, "Resource"); + } + ERR_BREAK(!obj); Resource *res = Object::cast_to<Resource>(obj); ERR_BREAK(!res); @@ -877,6 +882,12 @@ bool CustomPropertyEditor::edit(Object *p_owner, const String &p_name, Variant:: } else if (hint_text != "") { int idx = 0; + Vector<EditorData::CustomType> custom_resources; + + if (EditorNode::get_editor_data().get_custom_types().has("Resource")) { + custom_resources = EditorNode::get_editor_data().get_custom_types()["Resource"]; + } + for (int i = 0; i < hint_text.get_slice_count(","); i++) { String base = hint_text.get_slice(",", i); @@ -885,6 +896,11 @@ bool CustomPropertyEditor::edit(Object *p_owner, const String &p_name, Variant:: valid_inheritors.insert(base); List<StringName> inheritors; ClassDB::get_inheriters_from_class(base.strip_edges(), &inheritors); + + for (int i = 0; i < custom_resources.size(); i++) { + inheritors.push_back(custom_resources[i].name); + } + List<StringName>::Element *E = inheritors.front(); while (E) { valid_inheritors.insert(E->get()); @@ -893,14 +909,34 @@ bool CustomPropertyEditor::edit(Object *p_owner, const String &p_name, Variant:: for (Set<String>::Element *E = valid_inheritors.front(); E; E = E->next()) { String t = E->get(); - if (!ClassDB::can_instance(t)) + + bool is_custom_resource = false; + Ref<Texture> icon; + if (!custom_resources.empty()) { + for (int i = 0; i < custom_resources.size(); i++) { + if (custom_resources[i].name == t) { + is_custom_resource = true; + if (custom_resources[i].icon.is_valid()) + icon = custom_resources[i].icon; + break; + } + } + } + + if (!is_custom_resource && !ClassDB::can_instance(t)) continue; + inheritors_array.push_back(t); int id = TYPE_BASE_ID + idx; - if (has_icon(t, "EditorIcons")) { - menu->add_icon_item(get_icon(t, "EditorIcons"), vformat(TTR("New %s"), t), id); + if (!icon.is_valid() && has_icon(t, "EditorIcons")) { + icon = get_icon(t, "EditorIcons"); + } + + if (icon.is_valid()) { + + menu->add_icon_item(icon, vformat(TTR("New %s"), t), id); } else { menu->add_item(vformat(TTR("New %s"), t), id); @@ -1094,6 +1130,10 @@ void CustomPropertyEditor::_type_create_selected(int p_idx) { Object *obj = ClassDB::instance(intype); + if (!obj) { + obj = EditorNode::get_editor_data().instance_custom_type(intype, "Resource"); + } + ERR_FAIL_COND(!obj); Resource *res = Object::cast_to<Resource>(obj); @@ -1291,6 +1331,11 @@ void CustomPropertyEditor::_action_pressed(int p_which) { if (hint == PROPERTY_HINT_RESOURCE_TYPE) { Object *obj = ClassDB::instance(intype); + + if (!obj) { + obj = EditorNode::get_editor_data().instance_custom_type(intype, "Resource"); + } + ERR_BREAK(!obj); Resource *res = Object::cast_to<Resource>(obj); ERR_BREAK(!res); @@ -3939,7 +3984,7 @@ void PropertyEditor::_edit_button(Object *p_item, int p_column, int p_button) { if (_might_be_in_instance() && _get_instanced_node_original_property(prop, vorig)) { - _edit_set(prop, vorig); + _edit_set(prop, vorig.duplicate(true)); // Set, making sure to duplicate arrays properly return; } diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 1644ae5b55..b13b238fd7 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -916,9 +916,6 @@ void SceneTreeDock::perform_node_renames(Node *p_base, List<Pair<NodePath, NodeP if (!r_rem_anims) r_rem_anims = &rem_anims; - if (!bool(EDITOR_DEF("editors/animation/autorename_animation_tracks", true))) - return; - if (!p_base) { p_base = edited_scene; @@ -927,7 +924,54 @@ void SceneTreeDock::perform_node_renames(Node *p_base, List<Pair<NodePath, NodeP if (!p_base) return; - if (Object::cast_to<AnimationPlayer>(p_base)) { + // Renaming node paths used in script instances + if (p_base->get_script_instance()) { + + ScriptInstance *si = p_base->get_script_instance(); + + if (si) { + + List<PropertyInfo> properties; + si->get_property_list(&properties); + + for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { + + String propertyname = E->get().name; + Variant p = p_base->get(propertyname); + if (p.get_type() == Variant::NODE_PATH) { + + // Goes through all paths to check if its matching + for (List<Pair<NodePath, NodePath> >::Element *E = p_renames->front(); E; E = E->next()) { + + NodePath root_path = p_base->get_path(); + + NodePath rel_path_old = root_path.rel_path_to(E->get().first); + + NodePath rel_path_new = E->get().second; + + // if not empty, get new relative path + if (E->get().second != NodePath()) { + rel_path_new = root_path.rel_path_to(E->get().second); + } + + // if old path detected, then it needs to be replaced with the new one + if (p == rel_path_old) { + + editor_data->get_undo_redo().add_do_property(p_base, propertyname, rel_path_new); + editor_data->get_undo_redo().add_undo_property(p_base, propertyname, rel_path_old); + + p_base->set(propertyname, rel_path_new); + break; + } + } + } + } + } + } + + bool autorename_animation_tracks = bool(EDITOR_DEF("editors/animation/autorename_animation_tracks", true)); + + if (autorename_animation_tracks && Object::cast_to<AnimationPlayer>(p_base)) { AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(p_base); List<StringName> anims; @@ -1158,7 +1202,30 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V editor_data->get_undo_redo().add_do_method(new_parent, "move_child", node, p_position_in_parent + inc); ScriptEditorDebugger *sed = ScriptEditor::get_singleton()->get_debugger(); + String old_name = former_names[ni]; String new_name = new_parent->validate_child_name(node); + + // name was modified, fix the path renames + if (old_name.casecmp_to(new_name) != 0) { + + // Fix the to name to have the new name + NodePath old_new_name = path_renames[ni].second; + NodePath new_path; + + Vector<StringName> unfixed_new_names = old_new_name.get_names(); + Vector<StringName> fixed_new_names; + + // Get last name and replace with fixed new name + for (int a = 0; a < (unfixed_new_names.size() - 1); a++) { + fixed_new_names.push_back(unfixed_new_names[a]); + } + fixed_new_names.push_back(new_name); + + NodePath fixed_node_path = NodePath(fixed_new_names, true); + + path_renames[ni].second = fixed_node_path; + } + editor_data->get_undo_redo().add_do_method(sed, "live_debug_reparent_node", edited_scene->get_path_to(node), edited_scene->get_path_to(new_parent), new_name, -1); editor_data->get_undo_redo().add_undo_method(sed, "live_debug_reparent_node", NodePath(String(edited_scene->get_path_to(new_parent)) + "/" + new_name), edited_scene->get_path_to(node->get_parent()), node->get_name(), node->get_index()); diff --git a/editor/settings_config_dialog.cpp b/editor/settings_config_dialog.cpp index d75ef5df8a..36d7a83930 100644 --- a/editor/settings_config_dialog.cpp +++ b/editor/settings_config_dialog.cpp @@ -199,6 +199,14 @@ void EditorSettingsDialog::_update_icons() { void EditorSettingsDialog::_update_shortcuts() { + Map<String, bool> collapsed; + + if (shortcuts->get_root() && shortcuts->get_root()->get_children()) { + for (TreeItem *item = shortcuts->get_root()->get_children(); item; item = item->get_next()) { + collapsed[item->get_text(0)] = item->is_collapsed(); + } + } + shortcuts->clear(); List<String> slist; @@ -223,7 +231,13 @@ void EditorSettingsDialog::_update_shortcuts() { section = sections[section_name]; } else { section = shortcuts->create_item(root); - section->set_text(0, section_name.capitalize()); + + String item_name = section_name.capitalize(); + section->set_text(0, item_name); + + if (collapsed.has(item_name)) { + section->set_collapsed(collapsed[item_name]); + } sections[section_name] = section; section->set_custom_bg_color(0, get_color("prop_subsection", "Editor")); diff --git a/main/tests/test_gui.cpp b/main/tests/test_gui.cpp index b7b6c21692..305b749717 100644 --- a/main/tests/test_gui.cpp +++ b/main/tests/test_gui.cpp @@ -185,6 +185,10 @@ public: popup->add_item("Popup"); popup->add_check_item("Check Popup"); popup->set_item_checked(4, true); + popup->add_separator(); + popup->add_radio_check_item("Option A"); + popup->set_item_checked(6, true); + popup->add_radio_check_item("Option B"); OptionButton *options = memnew(OptionButton); diff --git a/modules/bullet/area_bullet.cpp b/modules/bullet/area_bullet.cpp index 648919e612..ec78cddb6a 100644 --- a/modules/bullet/area_bullet.cpp +++ b/modules/bullet/area_bullet.cpp @@ -68,7 +68,8 @@ AreaBullet::AreaBullet() : } AreaBullet::~AreaBullet() { - remove_all_overlapping_instantly(); + // Call "remove_all_overlapping_instantly();" is not necessary because the exit + // signal are handled by godot, so just clear the array } void AreaBullet::dispatch_callbacks() { @@ -131,12 +132,13 @@ void AreaBullet::remove_all_overlapping_instantly() { overlappingObjects.clear(); } -void AreaBullet::remove_overlapping_instantly(CollisionObjectBullet *p_object) { +void AreaBullet::remove_overlapping_instantly(CollisionObjectBullet *p_object, bool p_notify) { CollisionObjectBullet *supportObject; for (int i = overlappingObjects.size() - 1; 0 <= i; --i) { supportObject = overlappingObjects[i].object; if (supportObject == p_object) { - call_event(supportObject, PhysicsServer::AREA_BODY_REMOVED); + if (p_notify) + call_event(supportObject, PhysicsServer::AREA_BODY_REMOVED); supportObject->on_exit_area(this); overlappingObjects.remove(i); break; diff --git a/modules/bullet/area_bullet.h b/modules/bullet/area_bullet.h index 78136d574b..4104780de9 100644 --- a/modules/bullet/area_bullet.h +++ b/modules/bullet/area_bullet.h @@ -152,7 +152,7 @@ public: void remove_all_overlapping_instantly(); // Dispatch the callbacks and removes from overlapping list - void remove_overlapping_instantly(CollisionObjectBullet *p_object); + void remove_overlapping_instantly(CollisionObjectBullet *p_object, bool p_notify); virtual void on_collision_filters_change(); virtual void on_collision_checker_start() {} diff --git a/modules/bullet/collision_object_bullet.cpp b/modules/bullet/collision_object_bullet.cpp index 34aff68a4a..77f8df34cb 100644 --- a/modules/bullet/collision_object_bullet.cpp +++ b/modules/bullet/collision_object_bullet.cpp @@ -68,12 +68,10 @@ CollisionObjectBullet::CollisionObjectBullet(Type p_type) : force_shape_reset(false) {} CollisionObjectBullet::~CollisionObjectBullet() { - // Remove all overlapping + // Remove all overlapping, notify is not required since godot take care of it for (int i = areasOverlapped.size() - 1; 0 <= i; --i) { - areasOverlapped[i]->remove_overlapping_instantly(this); + areasOverlapped[i]->remove_overlapping_instantly(this, /*Notify*/ false); } - // not required - // areasOverlapped.clear(); destroyBulletCollisionObject(); } diff --git a/modules/gdnative/gdnative/string.cpp b/modules/gdnative/gdnative/string.cpp index 7f5dbc12be..ab0f0e0a50 100644 --- a/modules/gdnative/gdnative/string.cpp +++ b/modules/gdnative/gdnative/string.cpp @@ -1168,24 +1168,6 @@ godot_string GDAPI godot_string_c_unescape(const godot_string *p_self) { return result; } -godot_string GDAPI godot_string_http_escape(const godot_string *p_self) { - const String *self = (const String *)p_self; - godot_string result; - String return_value = self->http_escape(); - memnew_placement(&result, String(return_value)); - - return result; -} - -godot_string GDAPI godot_string_http_unescape(const godot_string *p_self) { - const String *self = (const String *)p_self; - godot_string result; - String return_value = self->http_unescape(); - memnew_placement(&result, String(return_value)); - - return result; -} - godot_string GDAPI godot_string_json_escape(const godot_string *p_self) { const String *self = (const String *)p_self; godot_string result; diff --git a/modules/gdnative/gdnative_api.json b/modules/gdnative/gdnative_api.json index f41c3859bd..9da2a69360 100644 --- a/modules/gdnative/gdnative_api.json +++ b/modules/gdnative/gdnative_api.json @@ -5468,20 +5468,6 @@ ] }, { - "name": "godot_string_http_escape", - "return_type": "godot_string", - "arguments": [ - ["const godot_string *", "p_self"] - ] - }, - { - "name": "godot_string_http_unescape", - "return_type": "godot_string", - "arguments": [ - ["const godot_string *", "p_self"] - ] - }, - { "name": "godot_string_json_escape", "return_type": "godot_string", "arguments": [ diff --git a/modules/gdnative/include/gdnative/string.h b/modules/gdnative/include/gdnative/string.h index 73245160c1..8fc59e21da 100644 --- a/modules/gdnative/include/gdnative/string.h +++ b/modules/gdnative/include/gdnative/string.h @@ -228,17 +228,14 @@ godot_string GDAPI godot_string_simplify_path(const godot_string *p_self); godot_string GDAPI godot_string_c_escape(const godot_string *p_self); godot_string GDAPI godot_string_c_escape_multiline(const godot_string *p_self); godot_string GDAPI godot_string_c_unescape(const godot_string *p_self); -godot_string GDAPI godot_string_http_escape(const godot_string *p_self); -godot_string GDAPI godot_string_http_unescape(const godot_string *p_self); +godot_string GDAPI godot_string_percent_decode(const godot_string *p_self); +godot_string GDAPI godot_string_percent_encode(const godot_string *p_self); godot_string GDAPI godot_string_json_escape(const godot_string *p_self); godot_string GDAPI godot_string_word_wrap(const godot_string *p_self, godot_int p_chars_per_line); godot_string GDAPI godot_string_xml_escape(const godot_string *p_self); godot_string GDAPI godot_string_xml_escape_with_quotes(const godot_string *p_self); godot_string GDAPI godot_string_xml_unescape(const godot_string *p_self); -godot_string GDAPI godot_string_percent_decode(const godot_string *p_self); -godot_string GDAPI godot_string_percent_encode(const godot_string *p_self); - godot_bool GDAPI godot_string_is_valid_float(const godot_string *p_self); godot_bool GDAPI godot_string_is_valid_hex_number(const godot_string *p_self, godot_bool p_with_prefix); godot_bool GDAPI godot_string_is_valid_html_color(const godot_string *p_self); diff --git a/modules/gdnative/nativescript/godot_nativescript.cpp b/modules/gdnative/nativescript/godot_nativescript.cpp index 593ff34a62..ace2ecac5c 100644 --- a/modules/gdnative/nativescript/godot_nativescript.cpp +++ b/modules/gdnative/nativescript/godot_nativescript.cpp @@ -339,13 +339,11 @@ const void GDAPI *godot_nativescript_get_type_tag(const godot_object *p_object) const Object *o = (Object *)p_object; if (!o->get_script_instance()) { - ERR_EXPLAIN("Attempted to get type tag on an object without a script!"); - ERR_FAIL_V(NULL); + return NULL; } else { NativeScript *script = Object::cast_to<NativeScript>(o->get_script_instance()->get_script().ptr()); if (!script) { - ERR_EXPLAIN("Attempted to get type tag on an object without a nativescript attached"); - ERR_FAIL_V(NULL); + return NULL; } if (script->get_script_desc()) diff --git a/modules/gdnative/pluginscript/register_types.cpp b/modules/gdnative/pluginscript/register_types.cpp index 8888f9e157..924abf29df 100644 --- a/modules/gdnative/pluginscript/register_types.cpp +++ b/modules/gdnative/pluginscript/register_types.cpp @@ -64,7 +64,7 @@ static Error _check_language_desc(const godot_pluginscript_language_desc *desc) // desc->make_function is not mandatory // desc->complete_code is not mandatory // desc->auto_indent_code is not mandatory - // desc->add_global_constant is not mandatory + ERR_FAIL_COND_V(!desc->add_global_constant, ERR_BUG); // desc->debug_get_error is not mandatory // desc->debug_get_stack_level_count is not mandatory // desc->debug_get_stack_level_line is not mandatory @@ -78,7 +78,7 @@ static Error _check_language_desc(const godot_pluginscript_language_desc *desc) // desc->profiling_stop is not mandatory // desc->profiling_get_accumulated_data is not mandatory // desc->profiling_get_frame_data is not mandatory - // desc->frame is not mandatory + // desc->profiling_frame is not mandatory ERR_FAIL_COND_V(!desc->script_desc.init, ERR_BUG); ERR_FAIL_COND_V(!desc->script_desc.finish, ERR_BUG); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 87d8fe1bf5..0d52f0a995 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1333,13 +1333,23 @@ static void _find_identifiers_in_block(GDScriptCompletionContext &context, int p for (int i = 0; i < context.block->statements.size(); i++) { - if (context.block->statements[i]->line > p_line) + GDScriptParser::Node *statement = context.block->statements[i]; + if (statement->line > p_line) continue; - if (context.block->statements[i]->type == GDScriptParser::BlockNode::TYPE_LOCAL_VAR) { + GDScriptParser::BlockNode::Type statementType = statement->type; + if (statementType == GDScriptParser::BlockNode::TYPE_LOCAL_VAR) { - const GDScriptParser::LocalVarNode *lv = static_cast<const GDScriptParser::LocalVarNode *>(context.block->statements[i]); + const GDScriptParser::LocalVarNode *lv = static_cast<const GDScriptParser::LocalVarNode *>(statement); result.insert(lv->name.operator String()); + } else if (statementType == GDScriptParser::BlockNode::TYPE_CONTROL_FLOW) { + + const GDScriptParser::ControlFlowNode *cf = static_cast<const GDScriptParser::ControlFlowNode *>(statement); + if (cf->cf_type == GDScriptParser::ControlFlowNode::CF_FOR) { + + const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(cf->arguments[0]); + result.insert(id->name.operator String()); + } } } } diff --git a/modules/gridmap/grid_map_editor_plugin.cpp b/modules/gridmap/grid_map_editor_plugin.cpp index 869492232c..f523eef895 100644 --- a/modules/gridmap/grid_map_editor_plugin.cpp +++ b/modules/gridmap/grid_map_editor_plugin.cpp @@ -1046,14 +1046,14 @@ GridMapEditor::GridMapEditor(EditorNode *p_editor) { options->get_popup()->add_item(TTR("Previous Floor"), MENU_OPTION_PREV_LEVEL, KEY_Q); options->get_popup()->add_item(TTR("Next Floor"), MENU_OPTION_NEXT_LEVEL, KEY_E); options->get_popup()->add_separator(); - options->get_popup()->add_check_item(TTR("Clip Disabled"), MENU_OPTION_CLIP_DISABLED); + options->get_popup()->add_radio_check_item(TTR("Clip Disabled"), MENU_OPTION_CLIP_DISABLED); options->get_popup()->set_item_checked(options->get_popup()->get_item_index(MENU_OPTION_CLIP_DISABLED), true); - options->get_popup()->add_check_item(TTR("Clip Above"), MENU_OPTION_CLIP_ABOVE); - options->get_popup()->add_check_item(TTR("Clip Below"), MENU_OPTION_CLIP_BELOW); + options->get_popup()->add_radio_check_item(TTR("Clip Above"), MENU_OPTION_CLIP_ABOVE); + options->get_popup()->add_radio_check_item(TTR("Clip Below"), MENU_OPTION_CLIP_BELOW); options->get_popup()->add_separator(); - options->get_popup()->add_check_item(TTR("Edit X Axis"), MENU_OPTION_X_AXIS, KEY_Z); - options->get_popup()->add_check_item(TTR("Edit Y Axis"), MENU_OPTION_Y_AXIS, KEY_X); - options->get_popup()->add_check_item(TTR("Edit Z Axis"), MENU_OPTION_Z_AXIS, KEY_C); + options->get_popup()->add_radio_check_item(TTR("Edit X Axis"), MENU_OPTION_X_AXIS, KEY_Z); + options->get_popup()->add_radio_check_item(TTR("Edit Y Axis"), MENU_OPTION_Y_AXIS, KEY_X); + options->get_popup()->add_radio_check_item(TTR("Edit Z Axis"), MENU_OPTION_Z_AXIS, KEY_C); options->get_popup()->set_item_checked(options->get_popup()->get_item_index(MENU_OPTION_Y_AXIS), true); options->get_popup()->add_separator(); options->get_popup()->add_item(TTR("Cursor Rotate X"), MENU_OPTION_CURSOR_ROTATE_X, KEY_A); diff --git a/modules/mono/editor/mono_bottom_panel.cpp b/modules/mono/editor/mono_bottom_panel.cpp index f1cf0bcdf5..1b5a303835 100644 --- a/modules/mono/editor/mono_bottom_panel.cpp +++ b/modules/mono/editor/mono_bottom_panel.cpp @@ -407,9 +407,14 @@ void MonoBuildTab::stop_build() { void MonoBuildTab::_issue_activated(int p_idx) { - ERR_FAIL_INDEX(p_idx, issues.size()); + ERR_FAIL_INDEX(p_idx, issues_list->get_item_count()); - const BuildIssue &issue = issues[p_idx]; + // Get correct issue idx from issue list + int issue_idx = this->issues_list->get_item_metadata(p_idx); + + ERR_FAIL_INDEX(issue_idx, issues.size()); + + const BuildIssue &issue = issues[issue_idx]; if (issue.project_file.empty() && issue.file.empty()) return; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index 18e5fc1404..0c5524dd08 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -1507,9 +1507,7 @@ void OS_OSX::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, c Ref<Texture> texture = p_cursor; Ref<Image> image = texture->get_data(); - int image_size = 32 * 32; - - ERR_FAIL_COND(texture->get_width() != 32 || texture->get_height() != 32); + ERR_FAIL_COND(texture->get_width() > 256 || texture->get_height() > 256); NSBitmapImageRep *imgrep = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 13fe781ff3..f51d969c16 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -1896,26 +1896,25 @@ void OS_Windows::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shap Ref<Texture> texture = p_cursor; Ref<Image> image = texture->get_data(); - UINT image_size = 32 * 32; + UINT image_size = texture->get_width() * texture->get_height(); UINT size = sizeof(UINT) * image_size; - ERR_FAIL_COND(texture->get_width() != 32 || texture->get_height() != 32); + ERR_FAIL_COND(texture->get_width() > 256 || texture->get_height() > 256); // Create the BITMAP with alpha channel COLORREF *buffer = (COLORREF *)malloc(sizeof(COLORREF) * image_size); image->lock(); for (UINT index = 0; index < image_size; index++) { - int column_index = floor(index / 32); - int row_index = index % 32; + int row_index = floor(index / texture->get_width()); + int column_index = index % texture->get_width(); - Color pcColor = image->get_pixel(row_index, column_index); - *(buffer + index) = image->get_pixel(row_index, column_index).to_argb32(); + *(buffer + index) = image->get_pixel(column_index, row_index).to_argb32(); } image->unlock(); // Using 4 channels, so 4 * 8 bits - HBITMAP bitmap = CreateBitmap(32, 32, 1, 4 * 8, buffer); + HBITMAP bitmap = CreateBitmap(texture->get_width(), texture->get_height(), 1, 4 * 8, buffer); COLORREF clrTransparent = -1; // Create the AND and XOR masks for the bitmap diff --git a/platform/x11/detect.py b/platform/x11/detect.py index da2b0701b6..5820a926e9 100644 --- a/platform/x11/detect.py +++ b/platform/x11/detect.py @@ -234,10 +234,10 @@ def configure(env): print("ALSA libraries not found, disabling driver") if env['pulseaudio']: - if (os.system("pkg-config --exists libpulse-simple") == 0): # 0 means found + if (os.system("pkg-config --exists libpulse") == 0): # 0 means found print("Enabling PulseAudio") env.Append(CPPFLAGS=["-DPULSEAUDIO_ENABLED"]) - env.ParseConfig('pkg-config --cflags --libs libpulse-simple') + env.ParseConfig('pkg-config --cflags --libs libpulse') else: print("PulseAudio development libraries not found, disabling driver") diff --git a/platform/x11/os_x11.cpp b/platform/x11/os_x11.cpp index c06c7516d0..338d13410f 100644 --- a/platform/x11/os_x11.cpp +++ b/platform/x11/os_x11.cpp @@ -2375,11 +2375,11 @@ void OS_X11::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, c Ref<Texture> texture = p_cursor; Ref<Image> image = texture->get_data(); - ERR_FAIL_COND(texture->get_width() != 32 || texture->get_height() != 32); + ERR_FAIL_COND(texture->get_width() > 256 || texture->get_height() > 256); // Create the cursor structure XcursorImage *cursor_image = XcursorImageCreate(texture->get_width(), texture->get_height()); - XcursorUInt image_size = 32 * 32; + XcursorUInt image_size = texture->get_width() * texture->get_height(); XcursorDim size = sizeof(XcursorPixel) * image_size; cursor_image->version = 1; @@ -2393,10 +2393,10 @@ void OS_X11::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, c image->lock(); for (XcursorPixel index = 0; index < image_size; index++) { - int column_index = floor(index / 32); - int row_index = index % 32; + int row_index = floor(index / texture->get_width()); + int column_index = index % texture->get_width(); - *(cursor_image->pixels + index) = image->get_pixel(row_index, column_index).to_argb32(); + *(cursor_image->pixels + index) = image->get_pixel(column_index, row_index).to_argb32(); } image->unlock(); diff --git a/scene/2d/canvas_item.cpp b/scene/2d/canvas_item.cpp index 5cca5705a0..2b89062181 100644 --- a/scene/2d/canvas_item.cpp +++ b/scene/2d/canvas_item.cpp @@ -412,7 +412,7 @@ void CanvasItem::_enter_canvas() { RID canvas; if (canvas_layer) - canvas = canvas_layer->get_world_2d()->get_canvas(); + canvas = canvas_layer->get_canvas(); else canvas = get_viewport()->find_world_2d()->get_canvas(); @@ -821,6 +821,12 @@ float CanvasItem::draw_char(const Ref<Font> &p_font, const Point2 &p_pos, const void CanvasItem::_notify_transform(CanvasItem *p_node) { + /* This check exists to avoid re-propagating the transform + * notification down the tree on dirty nodes. It provides + * optimization by avoiding redundancy (nodes are dirty, will get the + * notification anyway). + */ + if (/*p_node->xform_change.in_list() &&*/ p_node->global_invalid) { return; //nothing to do } @@ -854,7 +860,7 @@ RID CanvasItem::get_canvas() const { ERR_FAIL_COND_V(!is_inside_tree(), RID()); if (canvas_layer) - return canvas_layer->get_world_2d()->get_canvas(); + return canvas_layer->get_canvas(); else return get_viewport()->find_world_2d()->get_canvas(); } @@ -875,9 +881,7 @@ Ref<World2D> CanvasItem::get_world_2d() const { CanvasItem *tl = get_toplevel(); - if (tl->canvas_layer) { - return tl->canvas_layer->get_world_2d(); - } else if (tl->get_viewport()) { + if (tl->get_viewport()) { return tl->get_viewport()->find_world_2d(); } else { return Ref<World2D>(); diff --git a/scene/2d/parallax_layer.cpp b/scene/2d/parallax_layer.cpp index 050f98b02b..584c2f2c85 100644 --- a/scene/2d/parallax_layer.cpp +++ b/scene/2d/parallax_layer.cpp @@ -72,7 +72,7 @@ void ParallaxLayer::_update_mirroring() { ParallaxBackground *pb = Object::cast_to<ParallaxBackground>(get_parent()); if (pb) { - RID c = pb->get_world_2d()->get_canvas(); + RID c = pb->get_canvas(); RID ci = get_canvas_item(); Point2 mirrorScale = mirroring * get_scale(); VisualServer::get_singleton()->canvas_set_item_mirroring(c, ci, mirrorScale); diff --git a/scene/3d/particles.cpp b/scene/3d/particles.cpp index 8617bbc2f6..693b416f6d 100644 --- a/scene/3d/particles.cpp +++ b/scene/3d/particles.cpp @@ -849,9 +849,9 @@ void ParticlesMaterial::_update_shader() { code += " vec4(1.250, -1.050, -0.203, 0.0),\n"; code += " vec4(0.000, 0.000, 0.000, 0.0)) * hue_rot_s;\n"; if (color_ramp.is_valid()) { - code += " COLOR = textureLod(color_ramp,vec2(CUSTOM.y,0.0),0.0) * hue_rot_mat;\n"; + code += " COLOR = hue_rot_mat * textureLod(color_ramp,vec2(CUSTOM.y,0.0),0.0);\n"; } else { - code += " COLOR = color_value * hue_rot_mat;\n"; + code += " COLOR = hue_rot_mat * color_value;\n"; } if (emission_color_texture.is_valid() && emission_shape >= EMISSION_SHAPE_POINTS) { code += " COLOR*= texelFetch(emission_texture_color,emission_tex_ofs,0);\n"; diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index 232855c978..bc44c91f64 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -366,11 +366,17 @@ void Sprite3D::_draw() { final_rect.position * pixel_size, }; + + // Properly setup UVs for impostor textures (AtlasTexture). + RID texture_rid = texture->get_rid(); + Vector2 src_tsize = Vector2( + VS::get_singleton()->texture_get_width(texture_rid), + VS::get_singleton()->texture_get_height(texture_rid)); Vector2 uvs[4] = { - final_src_rect.position / tsize, - (final_src_rect.position + Vector2(final_src_rect.size.x, 0)) / tsize, - (final_src_rect.position + final_src_rect.size) / tsize, - (final_src_rect.position + Vector2(0, final_src_rect.size.y)) / tsize, + final_src_rect.position / src_tsize, + (final_src_rect.position + Vector2(final_src_rect.size.x, 0)) / src_tsize, + (final_src_rect.position + final_src_rect.size) / src_tsize, + (final_src_rect.position + Vector2(0, final_src_rect.size.y)) / src_tsize, }; if (is_flipped_h()) { @@ -649,18 +655,23 @@ void AnimatedSprite3D::_draw() { float pixel_size = get_pixel_size(); Vector2 vertices[4] = { - (final_rect.position + Vector2(0, final_rect.size.y)) * pixel_size, (final_rect.position + final_rect.size) * pixel_size, (final_rect.position + Vector2(final_rect.size.x, 0)) * pixel_size, final_rect.position * pixel_size, }; + + // Properly setup UVs for impostor textures (AtlasTexture). + RID texture_rid = texture->get_rid(); + Vector2 src_tsize = Vector2( + VS::get_singleton()->texture_get_width(texture_rid), + VS::get_singleton()->texture_get_height(texture_rid)); Vector2 uvs[4] = { - final_src_rect.position / tsize, - (final_src_rect.position + Vector2(final_src_rect.size.x, 0)) / tsize, - (final_src_rect.position + final_src_rect.size) / tsize, - (final_src_rect.position + Vector2(0, final_src_rect.size.y)) / tsize, + final_src_rect.position / src_tsize, + (final_src_rect.position + Vector2(final_src_rect.size.x, 0)) / src_tsize, + (final_src_rect.position + final_src_rect.size) / src_tsize, + (final_src_rect.position + Vector2(0, final_src_rect.size.y)) / src_tsize, }; if (is_flipped_h()) { diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index 2cf488ade4..b5bba885c4 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -1025,6 +1025,13 @@ float AnimationPlayer::get_speed_scale() const { return speed_scale; } +float AnimationPlayer::get_playing_speed() const { + + if (!playing) { + return 0; + } + return speed_scale * playback.current.speed_scale; +} void AnimationPlayer::seek(float p_time, bool p_update) { @@ -1316,6 +1323,7 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_speed_scale", "speed"), &AnimationPlayer::set_speed_scale); ClassDB::bind_method(D_METHOD("get_speed_scale"), &AnimationPlayer::get_speed_scale); + ClassDB::bind_method(D_METHOD("get_playing_speed"), &AnimationPlayer::get_playing_speed); ClassDB::bind_method(D_METHOD("set_autoplay", "name"), &AnimationPlayer::set_autoplay); ClassDB::bind_method(D_METHOD("get_autoplay"), &AnimationPlayer::get_autoplay); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index ef758bac44..af2022ddac 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -293,6 +293,7 @@ public: void set_speed_scale(float p_speed); float get_speed_scale() const; + float get_playing_speed() const; void set_autoplay(const String &p_name); String get_autoplay() const; diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index a5883863cd..c71320f207 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -1281,22 +1281,24 @@ void Control::_size_changed() { Size2 minimum_size = get_combined_minimum_size(); - if (data.h_grow == GROW_DIRECTION_BEGIN) { - if (minimum_size.width > new_size_cache.width) { - new_pos_cache.x = new_pos_cache.x + new_size_cache.width - minimum_size.width; - new_size_cache.width = minimum_size.width; + if (minimum_size.width > new_size_cache.width) { + if (data.h_grow == GROW_DIRECTION_BEGIN) { + new_pos_cache.x += new_size_cache.width - minimum_size.width; + } else if (data.h_grow == GROW_DIRECTION_BOTH) { + new_pos_cache.x += 0.5 * (new_size_cache.width - minimum_size.width); } - } else { - new_size_cache.width = MAX(minimum_size.width, new_size_cache.width); + + new_size_cache.width = minimum_size.width; } - if (data.v_grow == GROW_DIRECTION_BEGIN) { - if (minimum_size.height > new_size_cache.height) { - new_pos_cache.y = new_pos_cache.y + new_size_cache.height - minimum_size.height; - new_size_cache.height = minimum_size.height; + if (minimum_size.height > new_size_cache.height) { + if (data.v_grow == GROW_DIRECTION_BEGIN) { + new_pos_cache.y += new_size_cache.height - minimum_size.height; + } else if (data.v_grow == GROW_DIRECTION_BOTH) { + new_pos_cache.y += 0.5 * (new_size_cache.height - minimum_size.height); } - } else { - new_size_cache.height = MAX(minimum_size.height, new_size_cache.height); + + new_size_cache.height = minimum_size.height; } // We use a little workaround to avoid flickering when moving the pivot with _edit_set_pivot() @@ -2838,8 +2840,8 @@ void Control::_bind_methods() { ADD_PROPERTYINZ(PropertyInfo(Variant::INT, "margin_bottom", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_BOTTOM); ADD_GROUP("Grow Direction", "grow_"); - ADD_PROPERTYNO(PropertyInfo(Variant::INT, "grow_horizontal", PROPERTY_HINT_ENUM, "Begin,End"), "set_h_grow_direction", "get_h_grow_direction"); - ADD_PROPERTYNO(PropertyInfo(Variant::INT, "grow_vertical", PROPERTY_HINT_ENUM, "Begin,End"), "set_v_grow_direction", "get_v_grow_direction"); + ADD_PROPERTYNO(PropertyInfo(Variant::INT, "grow_horizontal", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_h_grow_direction", "get_h_grow_direction"); + ADD_PROPERTYNO(PropertyInfo(Variant::INT, "grow_vertical", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_v_grow_direction", "get_v_grow_direction"); ADD_GROUP("Rect", "rect_"); ADD_PROPERTYNZ(PropertyInfo(Variant::VECTOR2, "rect_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_position", "get_position"); @@ -2939,6 +2941,7 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(GROW_DIRECTION_BEGIN); BIND_ENUM_CONSTANT(GROW_DIRECTION_END); + BIND_ENUM_CONSTANT(GROW_DIRECTION_BOTH); BIND_ENUM_CONSTANT(ANCHOR_BEGIN); BIND_ENUM_CONSTANT(ANCHOR_END); diff --git a/scene/gui/control.h b/scene/gui/control.h index 51325f27b5..65f75c8a66 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -60,7 +60,8 @@ public: enum GrowDirection { GROW_DIRECTION_BEGIN, - GROW_DIRECTION_END + GROW_DIRECTION_END, + GROW_DIRECTION_BOTH }; enum FocusMode { diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index cc17e6bcd8..fa7c9e091e 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -37,6 +37,7 @@ void ItemList::add_item(const String &p_item, const Ref<Texture> &p_texture, boo Item item; item.icon = p_texture; item.icon_region = Rect2i(); + item.icon_modulate = Color(1, 1, 1, 1); item.text = p_item; item.selectable = p_selectable; item.selected = false; @@ -54,6 +55,7 @@ void ItemList::add_icon_item(const Ref<Texture> &p_item, bool p_selectable) { Item item; item.icon = p_item; item.icon_region = Rect2i(); + item.icon_modulate = Color(1, 1, 1, 1); //item.text=p_item; item.selectable = p_selectable; item.selected = false; @@ -138,6 +140,21 @@ Rect2 ItemList::get_item_icon_region(int p_idx) const { return items[p_idx].icon_region; } +void ItemList::set_item_icon_modulate(int p_idx, const Color &p_modulate) { + + ERR_FAIL_INDEX(p_idx, items.size()); + + items[p_idx].icon_modulate = p_modulate; + update(); +} + +Color ItemList::get_item_icon_modulate(int p_idx) const { + + ERR_FAIL_INDEX_V(p_idx, items.size(), Color()); + + return items[p_idx].icon_modulate; +} + void ItemList::set_item_custom_bg_color(int p_idx, const Color &p_custom_bg_color) { ERR_FAIL_INDEX(p_idx, items.size()); @@ -1045,7 +1062,7 @@ void ItemList::_notification(int p_what) { draw_rect.size = adj.size; } - Color modulate = Color(1, 1, 1, 1); + Color modulate = items[i].icon_modulate; if (items[i].disabled) modulate.a *= 0.5; @@ -1389,6 +1406,9 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_icon_region", "idx", "rect"), &ItemList::set_item_icon_region); ClassDB::bind_method(D_METHOD("get_item_icon_region", "idx"), &ItemList::get_item_icon_region); + ClassDB::bind_method(D_METHOD("set_item_icon_modulate", "idx", "modulate"), &ItemList::set_item_icon_modulate); + ClassDB::bind_method(D_METHOD("get_item_icon_modulate", "idx"), &ItemList::get_item_icon_modulate); + ClassDB::bind_method(D_METHOD("set_item_selectable", "idx", "selectable"), &ItemList::set_item_selectable); ClassDB::bind_method(D_METHOD("is_item_selectable", "idx"), &ItemList::is_item_selectable); diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 0fa0dd415b..58771c1777 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -54,6 +54,7 @@ private: Ref<Texture> icon; Rect2i icon_region; + Color icon_modulate; Ref<Texture> tag_icon; String text; bool selectable; @@ -135,6 +136,9 @@ public: void set_item_icon_region(int p_idx, const Rect2 &p_region); Rect2 get_item_icon_region(int p_idx) const; + void set_item_icon_modulate(int p_idx, const Color &p_modulate); + Color get_item_icon_modulate(int p_idx) const; + void set_item_selectable(int p_idx, bool p_selectable); bool is_item_selectable(int p_idx) const; diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 2e74faa61d..87cf4dc334 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -59,7 +59,6 @@ void MenuButton::pressed() { popup->set_size(Size2(size.width, 0)); popup->set_parent_rect(Rect2(Point2(gp - popup->get_global_position()), get_size())); popup->popup(); - popup->set_invalidate_click_until_motion(); } void MenuButton::_gui_input(Ref<InputEvent> p_event) { @@ -109,7 +108,6 @@ MenuButton::MenuButton() { add_child(popup); popup->set_as_toplevel(true); popup->set_pass_on_modal_close_click(false); - connect("button_up", popup, "call_deferred", make_binds("grab_click_focus")); set_process_unhandled_key_input(true); set_action_mode(ACTION_MODE_BUTTON_PRESS); } diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index 6e53f11b99..aaad10f579 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -118,7 +118,7 @@ void OptionButton::add_icon_item(const Ref<Texture> &p_icon, const String &p_lab } void OptionButton::add_item(const String &p_label, int p_ID) { - popup->add_check_item(p_label, p_ID); + popup->add_radio_check_item(p_label, p_ID); if (popup->get_item_count() == 1) select(0); } diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 747230e69f..9ff3bd6e81 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -55,7 +55,7 @@ Size2 PopupMenu::get_minimum_size() const { float max_w = 0; int font_h = font->get_height(); - int check_w = get_icon("checked")->get_width(); + int check_w = MAX(get_icon("checked")->get_width(), get_icon("radio_checked")->get_width()); int accel_max_w = 0; for (int i = 0; i < items.size(); i++) { @@ -74,7 +74,7 @@ Size2 PopupMenu::get_minimum_size() const { size.width += items[i].h_ofs; - if (items[i].checkable) { + if (items[i].checkable_type) { size.width += check_w + hseparation; } @@ -284,7 +284,8 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { if (b->is_pressed()) return; - switch (b->get_button_index()) { + int button_idx = b->get_button_index(); + switch (button_idx) { case BUTTON_WHEEL_DOWN: { @@ -298,30 +299,37 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { _scroll(b->get_factor(), b->get_position()); } } break; - case BUTTON_LEFT: { - - int over = _get_mouse_over(b->get_position()); - - if (invalidated_click) { - invalidated_click = false; - break; - } - if (over < 0) { - hide(); - break; //non-activable + default: { + // Allow activating item by releasing the LMB or any that was down when the popup appeared + if (button_idx == BUTTON_LEFT || (initial_button_mask & (1 << (button_idx - 1)))) { + + bool was_during_grabbed_click = during_grabbed_click; + during_grabbed_click = false; + + int over = _get_mouse_over(b->get_position()); + + if (invalidated_click) { + invalidated_click = false; + break; + } + if (over < 0) { + if (!was_during_grabbed_click) { + hide(); + } + break; //non-activable + } + + if (items[over].separator || items[over].disabled) + break; + + if (items[over].submenu != "") { + + _activate_submenu(over); + return; + } + activate_item(over); } - - if (items[over].separator || items[over].disabled) - break; - - if (items[over].submenu != "") { - - _activate_submenu(over); - return; - } - activate_item(over); - - } break; + } } //update(); @@ -408,8 +416,9 @@ void PopupMenu::_notification(int p_what) { Ref<StyleBox> style = get_stylebox("panel"); Ref<StyleBox> hover = get_stylebox("hover"); Ref<Font> font = get_font("font"); - Ref<Texture> check = get_icon("checked"); - Ref<Texture> uncheck = get_icon("unchecked"); + // In Item::checkable_type enum order (less the non-checkable member) + Ref<Texture> check[] = { get_icon("checked"), get_icon("radio_checked") }; + Ref<Texture> uncheck[] = { get_icon("unchecked"), get_icon("radio_unchecked") }; Ref<Texture> submenu = get_icon("submenu"); Ref<StyleBox> separator = get_stylebox("separator"); @@ -452,14 +461,10 @@ void PopupMenu::_notification(int p_what) { separator->draw(ci, Rect2(item_ofs + Point2(0, Math::floor((h - sep_h) / 2.0)), Size2(get_size().width - style->get_minimum_size().width, sep_h))); } - if (items[i].checkable) { - - if (items[i].checked) - check->draw(ci, item_ofs + Point2(0, Math::floor((h - check->get_height()) / 2.0))); - else - uncheck->draw(ci, item_ofs + Point2(0, Math::floor((h - check->get_height()) / 2.0))); - - item_ofs.x += check->get_width() + hseparation; + if (items[i].checkable_type) { + Texture *icon = (items[i].checked ? check[items[i].checkable_type - 1] : uncheck[items[i].checkable_type - 1]).ptr(); + icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0))); + item_ofs.x += icon->get_width() + hseparation; } if (!items[i].icon.is_null()) { @@ -503,6 +508,11 @@ void PopupMenu::_notification(int p_what) { update(); } } break; + case NOTIFICATION_POST_POPUP: { + + initial_button_mask = Input::get_singleton()->get_mouse_button_mask(); + during_grabbed_click = (bool)initial_button_mask; + } break; case NOTIFICATION_POPUP_HIDE: { if (mouse_over >= 0) { @@ -554,10 +564,11 @@ void PopupMenu::add_icon_check_item(const Ref<Texture> &p_icon, const String &p_ item.xl_text = tr(p_label); item.accel = p_accel; item.ID = p_ID; - item.checkable = true; + item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); update(); } + void PopupMenu::add_check_item(const String &p_label, int p_ID, uint32_t p_accel) { Item item; @@ -565,11 +576,18 @@ void PopupMenu::add_check_item(const String &p_label, int p_ID, uint32_t p_accel item.xl_text = tr(p_label); item.accel = p_accel; item.ID = p_ID; - item.checkable = true; + item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); update(); } +void PopupMenu::add_radio_check_item(const String &p_label, int p_ID, uint32_t p_accel) { + + add_check_item(p_label, p_ID, p_accel); + items[items.size() - 1].checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; + update(); +} + void PopupMenu::add_icon_shortcut(const Ref<Texture> &p_icon, const Ref<ShortCut> &p_shortcut, int p_ID, bool p_global) { ERR_FAIL_COND(p_shortcut.is_null()); @@ -598,6 +616,7 @@ void PopupMenu::add_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID, bool p_g items.push_back(item); update(); } + void PopupMenu::add_icon_check_shortcut(const Ref<Texture> &p_icon, const Ref<ShortCut> &p_shortcut, int p_ID, bool p_global) { ERR_FAIL_COND(p_shortcut.is_null()); @@ -607,7 +626,7 @@ void PopupMenu::add_icon_check_shortcut(const Ref<Texture> &p_icon, const Ref<Sh Item item; item.ID = p_ID; item.shortcut = p_shortcut; - item.checkable = true; + item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; item.icon = p_icon; item.shortcut_is_global = p_global; items.push_back(item); @@ -624,11 +643,18 @@ void PopupMenu::add_check_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID, bo item.ID = p_ID; item.shortcut = p_shortcut; item.shortcut_is_global = p_global; - item.checkable = true; + item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); update(); } +void PopupMenu::add_radio_check_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID, bool p_global) { + + add_check_shortcut(p_shortcut, p_ID, p_global); + items[items.size() - 1].checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; + update(); +} + void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int p_default_state, int p_ID, uint32_t p_accel) { Item item; @@ -636,7 +662,6 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int item.xl_text = tr(p_label); item.accel = p_accel; item.ID = p_ID; - item.checkable = false; item.max_states = p_max_states; item.state = p_default_state; items.push_back(item); @@ -811,7 +836,14 @@ bool PopupMenu::is_item_separator(int p_idx) const { void PopupMenu::set_item_as_checkable(int p_idx, bool p_checkable) { ERR_FAIL_INDEX(p_idx, items.size()); - items[p_idx].checkable = p_checkable; + items[p_idx].checkable_type = p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE; + update(); +} + +void PopupMenu::set_item_as_radio_checkable(int p_idx, bool p_radio_checkable) { + + ERR_FAIL_INDEX(p_idx, items.size()); + items[p_idx].checkable_type = p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE; update(); } @@ -867,7 +899,12 @@ void PopupMenu::toggle_item_multistate(int p_idx) { bool PopupMenu::is_item_checkable(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, items.size(), false); - return items[p_idx].checkable; + return items[p_idx].checkable_type; +} + +bool PopupMenu::is_item_radio_checkable(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), false); + return items[p_idx].checkable_type == Item::CHECKABLE_TYPE_RADIO_BUTTON; } int PopupMenu::get_item_count() const { @@ -941,7 +978,7 @@ void PopupMenu::activate_item(int p_item) { // We close all parents that are chained together, // with hide_on_item_selection enabled - if (items[p_item].checkable) { + if (items[p_item].checkable_type) { if (!hide_on_checkable_item_selection || !pop->is_hide_on_checkable_item_selection()) break; } else if (0 < items[p_item].max_states) { @@ -958,7 +995,7 @@ void PopupMenu::activate_item(int p_item) { // Hides popup by default; unless otherwise specified // by using set_hide_on_item_selection and set_hide_on_checkable_item_selection - if (items[p_item].checkable) { + if (items[p_item].checkable_type) { if (!hide_on_checkable_item_selection) return; } else if (0 < items[p_item].max_states) { @@ -1010,7 +1047,9 @@ Array PopupMenu::_get_items() const { items.push_back(get_item_text(i)); items.push_back(get_item_icon(i)); - items.push_back(is_item_checkable(i)); + // For compatibility, use false/true for no/checkbox and integers for other values + int ct = this->items[i].checkable_type; + items.push_back(Variant(ct <= Item::CHECKABLE_TYPE_CHECK_BOX ? is_item_checkable(i) : ct)); items.push_back(is_item_checked(i)); items.push_back(is_item_disabled(i)); @@ -1053,7 +1092,9 @@ void PopupMenu::_set_items(const Array &p_items) { String text = p_items[i + 0]; Ref<Texture> icon = p_items[i + 1]; + // For compatibility, use false/true for no/checkbox and integers for other values bool checkable = p_items[i + 2]; + bool radio_checkable = (int)p_items[i + 2] == Item::CHECKABLE_TYPE_RADIO_BUTTON; bool checked = p_items[i + 3]; bool disabled = p_items[i + 4]; @@ -1066,7 +1107,13 @@ void PopupMenu::_set_items(const Array &p_items) { int idx = get_item_count(); add_item(text, id); set_item_icon(idx, icon); - set_item_as_checkable(idx, checkable); + if (checkable) { + if (radio_checkable) { + set_item_as_radio_checkable(idx, true); + } else { + set_item_as_checkable(idx, true); + } + } set_item_checked(idx, checked); set_item_disabled(idx, disabled); set_item_id(idx, id); @@ -1147,12 +1194,14 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("add_item", "label", "id", "accel"), &PopupMenu::add_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_icon_check_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_check_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_check_item", "label", "id", "accel"), &PopupMenu::add_check_item, DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("add_radio_check_item", "label", "id", "accel"), &PopupMenu::add_radio_check_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_submenu_item", "label", "submenu", "id"), &PopupMenu::add_submenu_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("add_icon_shortcut", "texture", "shortcut", "id", "global"), &PopupMenu::add_icon_shortcut, DEFVAL(-1), DEFVAL(false)); ClassDB::bind_method(D_METHOD("add_shortcut", "shortcut", "id", "global"), &PopupMenu::add_shortcut, DEFVAL(-1), DEFVAL(false)); ClassDB::bind_method(D_METHOD("add_icon_check_shortcut", "texture", "shortcut", "id", "global"), &PopupMenu::add_icon_check_shortcut, DEFVAL(-1), DEFVAL(false)); ClassDB::bind_method(D_METHOD("add_check_shortcut", "shortcut", "id", "global"), &PopupMenu::add_check_shortcut, DEFVAL(-1), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("add_radio_check_shortcut", "shortcut", "id", "global"), &PopupMenu::add_radio_check_shortcut, DEFVAL(-1), DEFVAL(false)); ClassDB::bind_method(D_METHOD("set_item_text", "idx", "text"), &PopupMenu::set_item_text); ClassDB::bind_method(D_METHOD("set_item_icon", "idx", "icon"), &PopupMenu::set_item_icon); @@ -1164,6 +1213,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_submenu", "idx", "submenu"), &PopupMenu::set_item_submenu); ClassDB::bind_method(D_METHOD("set_item_as_separator", "idx", "enable"), &PopupMenu::set_item_as_separator); ClassDB::bind_method(D_METHOD("set_item_as_checkable", "idx", "enable"), &PopupMenu::set_item_as_checkable); + ClassDB::bind_method(D_METHOD("set_item_as_radio_checkable", "idx", "enable"), &PopupMenu::set_item_as_radio_checkable); ClassDB::bind_method(D_METHOD("set_item_tooltip", "idx", "tooltip"), &PopupMenu::set_item_tooltip); ClassDB::bind_method(D_METHOD("set_item_shortcut", "idx", "shortcut", "global"), &PopupMenu::set_item_shortcut, DEFVAL(false)); ClassDB::bind_method(D_METHOD("set_item_multistate", "idx", "state"), &PopupMenu::set_item_multistate); @@ -1182,6 +1232,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("get_item_submenu", "idx"), &PopupMenu::get_item_submenu); ClassDB::bind_method(D_METHOD("is_item_separator", "idx"), &PopupMenu::is_item_separator); ClassDB::bind_method(D_METHOD("is_item_checkable", "idx"), &PopupMenu::is_item_checkable); + ClassDB::bind_method(D_METHOD("is_item_radio_checkable", "idx"), &PopupMenu::is_item_radio_checkable); ClassDB::bind_method(D_METHOD("get_item_tooltip", "idx"), &PopupMenu::get_item_tooltip); ClassDB::bind_method(D_METHOD("get_item_shortcut", "idx"), &PopupMenu::get_item_shortcut); @@ -1216,15 +1267,20 @@ void PopupMenu::_bind_methods() { ADD_SIGNAL(MethodInfo("index_pressed", PropertyInfo(Variant::INT, "index"))); } -void PopupMenu::set_invalidate_click_until_motion() { +void PopupMenu::popup(const Rect2 &p_bounds) { + + grab_click_focus(); moved = Vector2(); invalidated_click = true; + Popup::popup(p_bounds); } PopupMenu::PopupMenu() { mouse_over = -1; submenu_over = -1; + initial_button_mask = 0; + during_grabbed_click = false; set_focus_mode(FOCUS_ALL); set_as_toplevel(true); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index 60f36e95ec..c7851969d0 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -46,7 +46,11 @@ class PopupMenu : public Popup { String text; String xl_text; bool checked; - bool checkable; + enum { + CHECKABLE_TYPE_NONE, + CHECKABLE_TYPE_CHECK_BOX, + CHECKABLE_TYPE_RADIO_BUTTON, + } checkable_type; int max_states; int state; bool separator; @@ -63,7 +67,7 @@ class PopupMenu : public Popup { Item() { checked = false; - checkable = false; + checkable_type = CHECKABLE_TYPE_NONE; separator = false; max_states = 0; state = 0; @@ -78,6 +82,8 @@ class PopupMenu : public Popup { Timer *submenu_timer; List<Rect2> autohide_areas; Vector<Item> items; + int initial_button_mask; + bool during_grabbed_click; int mouse_over; int submenu_over; Rect2 parent_rect; @@ -115,12 +121,14 @@ public: void add_item(const String &p_label, int p_ID = -1, uint32_t p_accel = 0); void add_icon_check_item(const Ref<Texture> &p_icon, const String &p_label, int p_ID = -1, uint32_t p_accel = 0); void add_check_item(const String &p_label, int p_ID = -1, uint32_t p_accel = 0); + void add_radio_check_item(const String &p_label, int p_ID = -1, uint32_t p_accel = 0); void add_submenu_item(const String &p_label, const String &p_submenu, int p_ID = -1); void add_icon_shortcut(const Ref<Texture> &p_icon, const Ref<ShortCut> &p_shortcut, int p_ID = -1, bool p_global = false); void add_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID = -1, bool p_global = false); void add_icon_check_shortcut(const Ref<Texture> &p_icon, const Ref<ShortCut> &p_shortcut, int p_ID = -1, bool p_global = false); void add_check_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID = -1, bool p_global = false); + void add_radio_check_shortcut(const Ref<ShortCut> &p_shortcut, int p_ID = -1, bool p_global = false); void add_multistate_item(const String &p_label, int p_max_states, int p_default_state, int p_ID = -1, uint32_t p_accel = 0); @@ -134,6 +142,7 @@ public: void set_item_submenu(int p_idx, const String &p_submenu); void set_item_as_separator(int p_idx, bool p_separator); void set_item_as_checkable(int p_idx, bool p_checkable); + void set_item_as_radio_checkable(int p_idx, bool p_radio_checkable); void set_item_tooltip(int p_idx, const String &p_tooltip); void set_item_shortcut(int p_idx, const Ref<ShortCut> &p_shortcut, bool p_global = false); void set_item_h_offset(int p_idx, int p_offset); @@ -154,6 +163,7 @@ public: String get_item_submenu(int p_idx) const; bool is_item_separator(int p_idx) const; bool is_item_checkable(int p_idx) const; + bool is_item_radio_checkable(int p_idx) const; String get_item_tooltip(int p_idx) const; Ref<ShortCut> get_item_shortcut(int p_idx) const; int get_item_state(int p_idx) const; @@ -178,7 +188,6 @@ public: void add_autohide_area(const Rect2 &p_area); void clear_autohide_areas(); - void set_invalidate_click_until_motion(); void set_hide_on_item_selection(bool p_enabled); bool is_hide_on_item_selection() const; @@ -188,6 +197,8 @@ public: void set_hide_on_multistate_item_selection(bool p_enabled); bool is_hide_on_multistate_item_selection() const; + virtual void popup(const Rect2 &p_bounds = Rect2()); + PopupMenu(); ~PopupMenu(); }; diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index ae07d5e671..6bfc4d4dee 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -516,6 +516,39 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int & table->total_width += table->columns[i].width + hseparation; } + //resize to max_width if needed and distribute the remaining space + bool table_need_fit = true; + while (table_need_fit) { + table_need_fit = false; + //fit slim + for (int i = 0; i < table->columns.size(); i++) { + if (!table->columns[i].expand) + continue; + int dif = table->columns[i].width - table->columns[i].max_width; + if (dif > 0) { + table_need_fit = true; + table->columns[i].width = table->columns[i].max_width; + table->total_width -= dif; + total_ratio -= table->columns[i].expand_ratio; + } + } + //grow + remaining_width = available_width - table->total_width; + if (remaining_width > 0 && total_ratio > 0) { + for (int i = 0; i < table->columns.size(); i++) { + if (table->columns[i].expand) { + int dif = table->columns[i].max_width - table->columns[i].width; + if (dif > 0) { + int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; + int incr = MIN(dif, slice); + table->columns[i].width += incr; + table->total_width += incr; + } + } + } + } + } + //compute caches properly again with the right width idx = 0; for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { diff --git a/scene/gui/texture_progress.cpp b/scene/gui/texture_progress.cpp index 4b3ba6df3c..82d983184b 100644 --- a/scene/gui/texture_progress.cpp +++ b/scene/gui/texture_progress.cpp @@ -106,6 +106,33 @@ Ref<Texture> TextureProgress::get_progress_texture() const { return progress; } +void TextureProgress::set_tint_under(const Color &p_tint) { + tint_under = p_tint; + update(); +} + +Color TextureProgress::get_tint_under() const { + return tint_under; +} + +void TextureProgress::set_tint_progress(const Color &p_tint) { + tint_progress = p_tint; + update(); +} + +Color TextureProgress::get_tint_progress() const { + return tint_progress; +} + +void TextureProgress::set_tint_over(const Color &p_tint) { + tint_over = p_tint; + update(); +} + +Color TextureProgress::get_tint_over() const { + return tint_over; +} + Point2 TextureProgress::unit_val_to_uv(float val) { if (progress.is_null()) return Point2(); @@ -170,7 +197,7 @@ Point2 TextureProgress::get_relative_center() { return p; } -void TextureProgress::draw_nine_patch_stretched(const Ref<Texture> &p_texture, FillMode p_mode, double p_ratio) { +void TextureProgress::draw_nine_patch_stretched(const Ref<Texture> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate) { Vector2 texture_size = p_texture->get_size(); Vector2 topleft = Vector2(stretch_margin[MARGIN_LEFT], stretch_margin[MARGIN_TOP]); Vector2 bottomright = Vector2(stretch_margin[MARGIN_RIGHT], stretch_margin[MARGIN_BOTTOM]); @@ -240,7 +267,7 @@ void TextureProgress::draw_nine_patch_stretched(const Ref<Texture> &p_texture, F } RID ci = get_canvas_item(); - VS::get_singleton()->canvas_item_add_nine_patch(ci, dst_rect, src_rect, p_texture->get_rid(), topleft, bottomright); + VS::get_singleton()->canvas_item_add_nine_patch(ci, dst_rect, src_rect, p_texture->get_rid(), topleft, bottomright, VS::NINE_PATCH_STRETCH, VS::NINE_PATCH_STRETCH, true, p_modulate); } void TextureProgress::_notification(int p_what) { @@ -251,42 +278,42 @@ void TextureProgress::_notification(int p_what) { if (nine_patch_stretch && (mode == FILL_LEFT_TO_RIGHT || mode == FILL_RIGHT_TO_LEFT || mode == FILL_TOP_TO_BOTTOM || mode == FILL_BOTTOM_TO_TOP)) { if (under.is_valid()) { - draw_nine_patch_stretched(under, FILL_LEFT_TO_RIGHT, 1.0); + draw_nine_patch_stretched(under, FILL_LEFT_TO_RIGHT, 1.0, tint_under); } if (progress.is_valid()) { - draw_nine_patch_stretched(progress, mode, get_as_ratio()); + draw_nine_patch_stretched(progress, mode, get_as_ratio(), tint_progress); } if (over.is_valid()) { - draw_nine_patch_stretched(over, FILL_LEFT_TO_RIGHT, 1.0); + draw_nine_patch_stretched(over, FILL_LEFT_TO_RIGHT, 1.0, tint_over); } } else { if (under.is_valid()) - draw_texture(under, Point2()); + draw_texture(under, Point2(), tint_under); if (progress.is_valid()) { Size2 s = progress->get_size(); switch (mode) { case FILL_LEFT_TO_RIGHT: { Rect2 region = Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)); - draw_texture_rect_region(progress, region, region); + draw_texture_rect_region(progress, region, region, tint_progress); } break; case FILL_RIGHT_TO_LEFT: { Rect2 region = Rect2(Point2(s.x - s.x * get_as_ratio(), 0), Size2(s.x * get_as_ratio(), s.y)); - draw_texture_rect_region(progress, region, region); + draw_texture_rect_region(progress, region, region, tint_progress); } break; case FILL_TOP_TO_BOTTOM: { Rect2 region = Rect2(Point2(), Size2(s.x, s.y * get_as_ratio())); - draw_texture_rect_region(progress, region, region); + draw_texture_rect_region(progress, region, region, tint_progress); } break; case FILL_BOTTOM_TO_TOP: { Rect2 region = Rect2(Point2(0, s.y - s.y * get_as_ratio()), Size2(s.x, s.y * get_as_ratio())); - draw_texture_rect_region(progress, region, region); + draw_texture_rect_region(progress, region, region, tint_progress); } break; case FILL_CLOCKWISE: case FILL_COUNTER_CLOCKWISE: { float val = get_as_ratio() * rad_max_degrees / 360; if (val == 1) { Rect2 region = Rect2(Point2(), s); - draw_texture_rect_region(progress, region, region); + draw_texture_rect_region(progress, region, region, tint_progress); } else if (val != 0) { Array pts; float direction = mode == FILL_CLOCKWISE ? 1 : -1; @@ -311,7 +338,9 @@ void TextureProgress::_notification(int p_what) { uvs.push_back(uv); points.push_back(Point2(uv.x * s.x, uv.y * s.y)); } - draw_polygon(points, Vector<Color>(), uvs, progress); + Vector<Color> colors; + colors.push_back(tint_progress); + draw_polygon(points, colors, uvs, progress); } if (Engine::get_singleton()->is_editor_hint()) { Point2 p = progress->get_size(); @@ -323,11 +352,11 @@ void TextureProgress::_notification(int p_what) { } } break; default: - draw_texture_rect_region(progress, Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y))); + draw_texture_rect_region(progress, Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), tint_progress); } } if (over.is_valid()) - draw_texture(over, Point2()); + draw_texture(over, Point2(), tint_over); } } break; @@ -389,6 +418,15 @@ void TextureProgress::_bind_methods() { ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &TextureProgress::set_fill_mode); ClassDB::bind_method(D_METHOD("get_fill_mode"), &TextureProgress::get_fill_mode); + ClassDB::bind_method(D_METHOD("set_tint_under", "tint"), &TextureProgress::set_tint_under); + ClassDB::bind_method(D_METHOD("get_tint_under"), &TextureProgress::get_tint_under); + + ClassDB::bind_method(D_METHOD("set_tint_progress", "tint"), &TextureProgress::set_tint_progress); + ClassDB::bind_method(D_METHOD("get_tint_progress"), &TextureProgress::get_tint_progress); + + ClassDB::bind_method(D_METHOD("set_tint_over", "tint"), &TextureProgress::set_tint_over); + ClassDB::bind_method(D_METHOD("get_tint_over"), &TextureProgress::get_tint_over); + ClassDB::bind_method(D_METHOD("set_radial_initial_angle", "mode"), &TextureProgress::set_radial_initial_angle); ClassDB::bind_method(D_METHOD("get_radial_initial_angle"), &TextureProgress::get_radial_initial_angle); @@ -409,6 +447,10 @@ void TextureProgress::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_over", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_over_texture", "get_over_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_progress", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_progress_texture", "get_progress_texture"); ADD_PROPERTYNZ(PropertyInfo(Variant::INT, "fill_mode", PROPERTY_HINT_ENUM, "Left to Right,Right to Left,Top to Bottom,Bottom to Top,Clockwise,Counter Clockwise"), "set_fill_mode", "get_fill_mode"); + ADD_GROUP("Tint", "tint_"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_under", PROPERTY_HINT_COLOR_NO_ALPHA), "set_tint_under", "get_tint_under"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_over", PROPERTY_HINT_COLOR_NO_ALPHA), "set_tint_over", "get_tint_over"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_progress", PROPERTY_HINT_COLOR_NO_ALPHA), "set_tint_progress", "get_tint_progress"); ADD_GROUP("Radial Fill", "radial_"); ADD_PROPERTYNZ(PropertyInfo(Variant::REAL, "radial_initial_angle", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider"), "set_radial_initial_angle", "get_radial_initial_angle"); ADD_PROPERTYNZ(PropertyInfo(Variant::REAL, "radial_fill_degrees", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider"), "set_fill_degrees", "get_fill_degrees"); @@ -440,4 +482,6 @@ TextureProgress::TextureProgress() { stretch_margin[MARGIN_RIGHT] = 0; stretch_margin[MARGIN_BOTTOM] = 0; stretch_margin[MARGIN_TOP] = 0; + + tint_under = tint_progress = tint_over = Color(1, 1, 1); } diff --git a/scene/gui/texture_progress.h b/scene/gui/texture_progress.h index 77c3980e29..34158b5db5 100644 --- a/scene/gui/texture_progress.h +++ b/scene/gui/texture_progress.h @@ -82,6 +82,15 @@ public: void set_nine_patch_stretch(bool p_stretch); bool get_nine_patch_stretch() const; + void set_tint_under(const Color &p_tint); + Color get_tint_under() const; + + void set_tint_progress(const Color &p_tint); + Color get_tint_progress() const; + + void set_tint_over(const Color &p_tint); + Color get_tint_over() const; + Size2 get_minimum_size() const; TextureProgress(); @@ -93,10 +102,11 @@ private: Point2 rad_center_off; bool nine_patch_stretch; int stretch_margin[4]; + Color tint_under, tint_progress, tint_over; Point2 unit_val_to_uv(float val); Point2 get_relative_center(); - void draw_nine_patch_stretched(const Ref<Texture> &p_texture, FillMode p_mode, double p_ratio); + void draw_nine_patch_stretched(const Ref<Texture> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate); }; VARIANT_ENUM_CAST(TextureProgress::FillMode); diff --git a/scene/main/canvas_layer.cpp b/scene/main/canvas_layer.cpp index 31d45d8e4c..8414210952 100644 --- a/scene/main/canvas_layer.cpp +++ b/scene/main/canvas_layer.cpp @@ -35,7 +35,7 @@ void CanvasLayer::set_layer(int p_xform) { layer = p_xform; if (viewport.is_valid()) - VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas->get_canvas(), layer); + VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas, layer); } int CanvasLayer::get_layer() const { @@ -48,7 +48,7 @@ void CanvasLayer::set_transform(const Transform2D &p_xform) { transform = p_xform; locrotscale_dirty = true; if (viewport.is_valid()) - VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas->get_canvas(), transform); + VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform); } Transform2D CanvasLayer::get_transform() const { @@ -61,7 +61,7 @@ void CanvasLayer::_update_xform() { transform.set_rotation_and_scale(rot, scale); transform.set_origin(ofs); if (viewport.is_valid()) - VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas->get_canvas(), transform); + VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform); } void CanvasLayer::_update_locrotscale() { @@ -133,11 +133,6 @@ Vector2 CanvasLayer::get_scale() const { return scale; } -Ref<World2D> CanvasLayer::get_world_2d() const { - - return canvas; -} - void CanvasLayer::_notification(int p_what) { switch (p_what) { @@ -153,14 +148,14 @@ void CanvasLayer::_notification(int p_what) { ERR_FAIL_COND(!vp); viewport = vp->get_viewport_rid(); - VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas->get_canvas()); - VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas->get_canvas(), layer); - VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas->get_canvas(), transform); + VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas); + VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas, layer); + VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform); } break; case NOTIFICATION_EXIT_TREE: { - VisualServer::get_singleton()->viewport_remove_canvas(viewport, canvas->get_canvas()); + VisualServer::get_singleton()->viewport_remove_canvas(viewport, canvas); viewport = RID(); } break; @@ -184,7 +179,7 @@ RID CanvasLayer::get_viewport() const { void CanvasLayer::set_custom_viewport(Node *p_viewport) { ERR_FAIL_NULL(p_viewport); if (is_inside_tree()) { - VisualServer::get_singleton()->viewport_remove_canvas(viewport, canvas->get_canvas()); + VisualServer::get_singleton()->viewport_remove_canvas(viewport, canvas); viewport = RID(); } @@ -205,9 +200,9 @@ void CanvasLayer::set_custom_viewport(Node *p_viewport) { viewport = vp->get_viewport_rid(); - VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas->get_canvas()); - VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas->get_canvas(), layer); - VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas->get_canvas(), transform); + VisualServer::get_singleton()->viewport_attach_canvas(viewport, canvas); + VisualServer::get_singleton()->viewport_set_canvas_layer(viewport, canvas, layer); + VisualServer::get_singleton()->viewport_set_canvas_transform(viewport, canvas, transform); } } @@ -225,6 +220,10 @@ int CanvasLayer::get_sort_index() { return sort_index++; } +RID CanvasLayer::get_canvas() const { + + return canvas; +} void CanvasLayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_layer", "layer"), &CanvasLayer::set_layer); @@ -248,7 +247,7 @@ void CanvasLayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_custom_viewport", "viewport"), &CanvasLayer::set_custom_viewport); ClassDB::bind_method(D_METHOD("get_custom_viewport"), &CanvasLayer::get_custom_viewport); - ClassDB::bind_method(D_METHOD("get_world_2d"), &CanvasLayer::get_world_2d); + ClassDB::bind_method(D_METHOD("get_canvas"), &CanvasLayer::get_canvas); //ClassDB::bind_method(D_METHOD("get_viewport"),&CanvasLayer::get_viewport); ADD_PROPERTY(PropertyInfo(Variant::INT, "layer", PROPERTY_HINT_RANGE, "-128,128,1"), "set_layer", "get_layer"); @@ -268,8 +267,13 @@ CanvasLayer::CanvasLayer() { rot = 0; locrotscale_dirty = false; layer = 1; - canvas = Ref<World2D>(memnew(World2D)); + canvas = VS::get_singleton()->canvas_create(); custom_viewport = NULL; custom_viewport_id = 0; sort_index = 0; } + +CanvasLayer::~CanvasLayer() { + + VS::get_singleton()->free(canvas); +} diff --git a/scene/main/canvas_layer.h b/scene/main/canvas_layer.h index c3352a6dba..aae23fbb12 100644 --- a/scene/main/canvas_layer.h +++ b/scene/main/canvas_layer.h @@ -45,7 +45,7 @@ class CanvasLayer : public Node { real_t rot; int layer; Transform2D transform; - Ref<World2D> canvas; + RID canvas; ObjectID custom_viewport_id; // to check validity Viewport *custom_viewport; @@ -81,8 +81,6 @@ public: void set_scale(const Size2 &p_scale); Size2 get_scale() const; - Ref<World2D> get_world_2d() const; - Size2 get_viewport_size() const; RID get_viewport() const; @@ -93,7 +91,10 @@ public: void reset_sort_index(); int get_sort_index(); + RID get_canvas() const; + CanvasLayer(); + ~CanvasLayer(); }; #endif // CANVAS_LAYER_H diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 28b4540573..ec01490ae5 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -477,7 +477,7 @@ bool Node::is_network_master() const { ERR_FAIL_COND_V(!is_inside_tree(), false); - return get_tree()->get_network_unique_id() == data.network_master; + return get_multiplayer_api()->get_network_unique_id() == data.network_master; } /***** RPC CONFIG ********/ @@ -667,200 +667,16 @@ Variant Node::_rpc_unreliable_id_bind(const Variant **p_args, int p_argcount, Va } void Node::rpcp(int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount) { - ERR_FAIL_COND(!is_inside_tree()); - - bool skip_rpc = false; - bool call_local_native = false; - bool call_local_script = false; - - if (p_peer_id == 0 || p_peer_id == get_tree()->get_network_unique_id() || (p_peer_id < 0 && p_peer_id != -get_tree()->get_network_unique_id())) { - //check that send mode can use local call - - Map<StringName, RPCMode>::Element *E = data.rpc_methods.find(p_method); - if (E) { - - switch (E->get()) { - - case RPC_MODE_DISABLED: { - //do nothing - } break; - case RPC_MODE_REMOTE: { - //do nothing also, no need to call local - } break; - case RPC_MODE_SYNC: { - //call it, sync always results in call - call_local_native = true; - } break; - case RPC_MODE_MASTER: { - call_local_native = is_network_master(); - if (call_local_native) { - skip_rpc = true; //no other master so.. - } - } break; - case RPC_MODE_SLAVE: { - call_local_native = !is_network_master(); - } break; - } - } - - if (call_local_native) { - // done below - } else if (get_script_instance()) { - //attempt with script - ScriptInstance::RPCMode rpc_mode = get_script_instance()->get_rpc_mode(p_method); - - switch (rpc_mode) { - - case ScriptInstance::RPC_MODE_DISABLED: { - //do nothing - } break; - case ScriptInstance::RPC_MODE_REMOTE: { - //do nothing also, no need to call local - } break; - case ScriptInstance::RPC_MODE_SYNC: { - //call it, sync always results in call - call_local_script = true; - } break; - case ScriptInstance::RPC_MODE_MASTER: { - call_local_script = is_network_master(); - if (call_local_script) { - skip_rpc = true; //no other master so.. - } - } break; - case ScriptInstance::RPC_MODE_SLAVE: { - call_local_script = !is_network_master(); - } break; - } - } - } - - if (!skip_rpc) { - get_tree()->_rpc(this, p_peer_id, p_unreliable, false, p_method, p_arg, p_argcount); - } - - if (call_local_native) { - Variant::CallError ce; - call(p_method, p_arg, p_argcount, ce); - if (ce.error != Variant::CallError::CALL_OK) { - String error = Variant::get_call_error_text(this, p_method, p_arg, p_argcount, ce); - error = "rpc() aborted in local call: - " + error; - ERR_PRINTS(error); - return; - } - } - - if (call_local_script) { - Variant::CallError ce; - ce.error = Variant::CallError::CALL_OK; - get_script_instance()->call(p_method, p_arg, p_argcount, ce); - if (ce.error != Variant::CallError::CALL_OK) { - String error = Variant::get_call_error_text(this, p_method, p_arg, p_argcount, ce); - error = "rpc() aborted in script local call: - " + error; - ERR_PRINTS(error); - return; - } - } + get_multiplayer_api()->rpcp(this, p_peer_id, p_unreliable, p_method, p_arg, p_argcount); } -/******** RSET *********/ - void Node::rsetp(int p_peer_id, bool p_unreliable, const StringName &p_property, const Variant &p_value) { - ERR_FAIL_COND(!is_inside_tree()); - - bool skip_rset = false; - - if (p_peer_id == 0 || p_peer_id == get_tree()->get_network_unique_id() || (p_peer_id < 0 && p_peer_id != -get_tree()->get_network_unique_id())) { - //check that send mode can use local call - - bool set_local = false; - - Map<StringName, RPCMode>::Element *E = data.rpc_properties.find(p_property); - if (E) { - - switch (E->get()) { - - case RPC_MODE_DISABLED: { - //do nothing - } break; - case RPC_MODE_REMOTE: { - //do nothing also, no need to call local - } break; - case RPC_MODE_SYNC: { - //call it, sync always results in call - set_local = true; - } break; - case RPC_MODE_MASTER: { - set_local = is_network_master(); - if (set_local) { - skip_rset = true; - } - - } break; - case RPC_MODE_SLAVE: { - set_local = !is_network_master(); - } break; - } - } - - if (set_local) { - bool valid; - set(p_property, p_value, &valid); - - if (!valid) { - String error = "rset() aborted in local set, property not found: - " + String(p_property); - ERR_PRINTS(error); - return; - } - } else if (get_script_instance()) { - //attempt with script - ScriptInstance::RPCMode rpc_mode = get_script_instance()->get_rset_mode(p_property); - - switch (rpc_mode) { - - case ScriptInstance::RPC_MODE_DISABLED: { - //do nothing - } break; - case ScriptInstance::RPC_MODE_REMOTE: { - //do nothing also, no need to call local - } break; - case ScriptInstance::RPC_MODE_SYNC: { - //call it, sync always results in call - set_local = true; - } break; - case ScriptInstance::RPC_MODE_MASTER: { - set_local = is_network_master(); - if (set_local) { - skip_rset = true; - } - } break; - case ScriptInstance::RPC_MODE_SLAVE: { - set_local = !is_network_master(); - } break; - } - - if (set_local) { - - bool valid = get_script_instance()->set(p_property, p_value); - - if (!valid) { - String error = "rset() aborted in local script set, property not found: - " + String(p_property); - ERR_PRINTS(error); - return; - } - } - } - } - - if (skip_rset) - return; - - const Variant *vptr = &p_value; - - get_tree()->_rpc(this, p_peer_id, p_unreliable, true, p_property, &vptr, 1); + get_multiplayer_api()->rsetp(this, p_peer_id, p_unreliable, p_property, p_value); } +/******** RSET *********/ void Node::rset(const StringName &p_property, const Variant &p_value) { rsetp(0, false, p_property, p_value); @@ -882,6 +698,30 @@ void Node::rset_unreliable_id(int p_peer_id, const StringName &p_property, const } //////////// end of rpc +Ref<MultiplayerAPI> Node::get_multiplayer_api() const { + if (multiplayer_api.is_valid()) + return multiplayer_api; + if (!is_inside_tree()) + return Ref<MultiplayerAPI>(); + return get_tree()->get_multiplayer_api(); +} + +Ref<MultiplayerAPI> Node::get_custom_multiplayer_api() const { + return multiplayer_api; +} + +void Node::set_custom_multiplayer_api(Ref<MultiplayerAPI> p_multiplayer_api) { + + multiplayer_api = p_multiplayer_api; +} + +const Map<StringName, Node::RPCMode>::Element *Node::get_node_rpc_mode(const StringName &p_method) { + return data.rpc_methods.find(p_method); +} + +const Map<StringName, Node::RPCMode>::Element *Node::get_node_rset_mode(const StringName &p_property) { + return data.rpc_properties.find(p_property); +} bool Node::can_call_rpc(const StringName &p_method, int p_from) const { @@ -2163,13 +2003,7 @@ Node *Node::_duplicate(int p_flags, Map<const Node *, Node *> *r_duplimap) const if (name == script_property_name) continue; - Variant value = N->get()->get(name); - // Duplicate dictionaries and arrays, mainly needed for __meta__ - if (value.get_type() == Variant::DICTIONARY) { - value = Dictionary(value).duplicate(); - } else if (value.get_type() == Variant::ARRAY) { - value = Array(value).duplicate(); - } + Variant value = N->get()->get(name).duplicate(true); if (E->get().usage & PROPERTY_USAGE_DO_NOT_SHARE_ON_DUPLICATE) { @@ -2313,13 +2147,7 @@ void Node::_duplicate_and_reown(Node *p_new_parent, const Map<Node *, Node *> &p continue; String name = E->get().name; - Variant value = get(name); - // Duplicate dictionaries and arrays, mainly needed for __meta__ - if (value.get_type() == Variant::DICTIONARY) { - value = Dictionary(value).duplicate(); - } else if (value.get_type() == Variant::ARRAY) { - value = Array(value).duplicate(); - } + Variant value = get(name).duplicate(true); node->set(name, value); } @@ -2889,6 +2717,9 @@ void Node::_bind_methods() { ClassDB::bind_method(D_METHOD("is_network_master"), &Node::is_network_master); + ClassDB::bind_method(D_METHOD("get_multiplayer_api"), &Node::get_multiplayer_api); + ClassDB::bind_method(D_METHOD("get_custom_multiplayer_api"), &Node::get_custom_multiplayer_api); + ClassDB::bind_method(D_METHOD("set_custom_multiplayer_api", "api"), &Node::set_custom_multiplayer_api); ClassDB::bind_method(D_METHOD("rpc_config", "method", "mode"), &Node::rpc_config); ClassDB::bind_method(D_METHOD("rset_config", "property", "mode"), &Node::rset_config); @@ -2970,6 +2801,8 @@ void Node::_bind_methods() { ADD_PROPERTYNZ(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "", 0), "set_name", "get_name"); ADD_PROPERTYNZ(PropertyInfo(Variant::STRING, "filename", PROPERTY_HINT_NONE, "", 0), "set_filename", "get_filename"); ADD_PROPERTYNZ(PropertyInfo(Variant::OBJECT, "owner", PROPERTY_HINT_RESOURCE_TYPE, "Node", 0), "set_owner", "get_owner"); + ADD_PROPERTYNZ(PropertyInfo(Variant::OBJECT, "multiplayer_api", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", 0), "", "get_multiplayer_api"); + ADD_PROPERTYNZ(PropertyInfo(Variant::OBJECT, "custom_multiplayer_api", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", 0), "set_custom_multiplayer_api", "get_custom_multiplayer_api"); BIND_VMETHOD(MethodInfo("_process", PropertyInfo(Variant::REAL, "delta"))); BIND_VMETHOD(MethodInfo("_physics_process", PropertyInfo(Variant::REAL, "delta"))); diff --git a/scene/main/node.h b/scene/main/node.h index 341869f7ba..2e8716cbd1 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -151,6 +151,8 @@ private: NAME_CASING_SNAKE_CASE }; + Ref<MultiplayerAPI> multiplayer_api; + void _print_tree(const Node *p_node); Node *_get_node(const NodePath &p_path) const; @@ -403,15 +405,20 @@ public: void rpc_id(int p_peer_id, const StringName &p_method, VARIANT_ARG_LIST); //rpc call, honors RPCMode void rpc_unreliable_id(int p_peer_id, const StringName &p_method, VARIANT_ARG_LIST); //rpc call, honors RPCMode - void rpcp(int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount); - void rset(const StringName &p_property, const Variant &p_value); //remote set call, honors RPCMode void rset_unreliable(const StringName &p_property, const Variant &p_value); //remote set call, honors RPCMode void rset_id(int p_peer_id, const StringName &p_property, const Variant &p_value); //remote set call, honors RPCMode void rset_unreliable_id(int p_peer_id, const StringName &p_property, const Variant &p_value); //remote set call, honors RPCMode + void rpcp(int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount); void rsetp(int p_peer_id, bool p_unreliable, const StringName &p_property, const Variant &p_value); + Ref<MultiplayerAPI> get_multiplayer_api() const; + Ref<MultiplayerAPI> get_custom_multiplayer_api() const; + void set_custom_multiplayer_api(Ref<MultiplayerAPI> p_multiplayer_api); + const Map<StringName, RPCMode>::Element *get_node_rpc_mode(const StringName &p_method); + const Map<StringName, RPCMode>::Element *get_node_rset_mode(const StringName &p_property); + bool can_call_rpc(const StringName &p_method, int p_from) const; bool can_call_rset(const StringName &p_property, int p_from) const; diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 037331dec1..4419dfe70f 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -484,7 +484,7 @@ bool SceneTree::idle(float p_time) { idle_process_time = p_time; - _network_poll(); + multiplayer_api->poll(); emit_signal("idle_frame"); @@ -1633,16 +1633,11 @@ Ref<SceneTreeTimer> SceneTree::create_timer(float p_delay_sec, bool p_process_pa void SceneTree::_network_peer_connected(int p_id) { - connected_peers.insert(p_id); - path_get_cache.insert(p_id, PathGetCache()); - emit_signal("network_peer_connected", p_id); } void SceneTree::_network_peer_disconnected(int p_id) { - connected_peers.erase(p_id); - path_get_cache.erase(p_id); //I no longer need your cache, sorry emit_signal("network_peer_disconnected", p_id); } @@ -1661,471 +1656,70 @@ void SceneTree::_server_disconnected() { emit_signal("server_disconnected"); } -void SceneTree::set_network_peer(const Ref<NetworkedMultiplayerPeer> &p_network_peer) { - if (network_peer.is_valid()) { - network_peer->disconnect("peer_connected", this, "_network_peer_connected"); - network_peer->disconnect("peer_disconnected", this, "_network_peer_disconnected"); - network_peer->disconnect("connection_succeeded", this, "_connected_to_server"); - network_peer->disconnect("connection_failed", this, "_connection_failed"); - network_peer->disconnect("server_disconnected", this, "_server_disconnected"); - connected_peers.clear(); - path_get_cache.clear(); - path_send_cache.clear(); - last_send_cache_id = 1; +Ref<MultiplayerAPI> SceneTree::get_multiplayer_api() const { + return multiplayer_api; +} + +void SceneTree::set_multiplayer_api(Ref<MultiplayerAPI> p_multiplayer_api) { + ERR_FAIL_COND(!p_multiplayer_api.is_valid()); + + if (multiplayer_api.is_valid()) { + multiplayer_api->disconnect("network_peer_connected", this, "_network_peer_connected"); + multiplayer_api->disconnect("network_peer_disconnected", this, "_network_peer_disconnected"); + multiplayer_api->disconnect("connected_to_server", this, "_connected_to_server"); + multiplayer_api->disconnect("connection_failed", this, "_connection_failed"); + multiplayer_api->disconnect("server_disconnected", this, "_server_disconnected"); } - ERR_EXPLAIN("Supplied NetworkedNetworkPeer must be connecting or connected."); - ERR_FAIL_COND(p_network_peer.is_valid() && p_network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED); + multiplayer_api = p_multiplayer_api; + multiplayer_api->set_root_node(root); - network_peer = p_network_peer; + multiplayer_api->connect("network_peer_connected", this, "_network_peer_connected"); + multiplayer_api->connect("network_peer_disconnected", this, "_network_peer_disconnected"); + multiplayer_api->connect("connected_to_server", this, "_connected_to_server"); + multiplayer_api->connect("connection_failed", this, "_connection_failed"); + multiplayer_api->connect("server_disconnected", this, "_server_disconnected"); +} - if (network_peer.is_valid()) { - network_peer->connect("peer_connected", this, "_network_peer_connected"); - network_peer->connect("peer_disconnected", this, "_network_peer_disconnected"); - network_peer->connect("connection_succeeded", this, "_connected_to_server"); - network_peer->connect("connection_failed", this, "_connection_failed"); - network_peer->connect("server_disconnected", this, "_server_disconnected"); - } +void SceneTree::set_network_peer(const Ref<NetworkedMultiplayerPeer> &p_network_peer) { + + multiplayer_api->set_network_peer(p_network_peer); } Ref<NetworkedMultiplayerPeer> SceneTree::get_network_peer() const { - return network_peer; + return multiplayer_api->get_network_peer(); } bool SceneTree::is_network_server() const { - ERR_FAIL_COND_V(!network_peer.is_valid(), false); - return network_peer->is_server(); + return multiplayer_api->is_network_server(); } bool SceneTree::has_network_peer() const { - return network_peer.is_valid(); + return multiplayer_api->has_network_peer(); } int SceneTree::get_network_unique_id() const { - ERR_FAIL_COND_V(!network_peer.is_valid(), 0); - return network_peer->get_unique_id(); + return multiplayer_api->get_network_unique_id(); } Vector<int> SceneTree::get_network_connected_peers() const { - ERR_FAIL_COND_V(!network_peer.is_valid(), Vector<int>()); - - Vector<int> ret; - for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { - ret.push_back(E->get()); - } - return ret; + return multiplayer_api->get_network_connected_peers(); } int SceneTree::get_rpc_sender_id() const { - return rpc_sender_id; + return multiplayer_api->get_rpc_sender_id(); } void SceneTree::set_refuse_new_network_connections(bool p_refuse) { - ERR_FAIL_COND(!network_peer.is_valid()); - network_peer->set_refuse_new_connections(p_refuse); + multiplayer_api->set_refuse_new_network_connections(p_refuse); } bool SceneTree::is_refusing_new_network_connections() const { - - ERR_FAIL_COND_V(!network_peer.is_valid(), false); - - return network_peer->is_refusing_new_connections(); -} - -void SceneTree::_rpc(Node *p_from, int p_to, bool p_unreliable, bool p_set, const StringName &p_name, const Variant **p_arg, int p_argcount) { - - if (network_peer.is_null()) { - ERR_EXPLAIN("Attempt to remote call/set when networking is not active in SceneTree."); - ERR_FAIL(); - } - - if (network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_CONNECTING) { - ERR_EXPLAIN("Attempt to remote call/set when networking is not connected yet in SceneTree."); - ERR_FAIL(); - } - - if (network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED) { - ERR_EXPLAIN("Attempt to remote call/set when networking is disconnected."); - ERR_FAIL(); - } - - if (p_argcount > 255) { - ERR_EXPLAIN("Too many arguments >255."); - ERR_FAIL(); - } - - if (p_to != 0 && !connected_peers.has(ABS(p_to))) { - if (p_to == get_network_unique_id()) { - ERR_EXPLAIN("Attempt to remote call/set yourself! unique ID: " + itos(get_network_unique_id())); - } else { - ERR_EXPLAIN("Attempt to remote call unexisting ID: " + itos(p_to)); - } - - ERR_FAIL(); - } - - NodePath from_path = p_from->get_path(); - ERR_FAIL_COND(from_path.is_empty()); - - //see if the path is cached - PathSentCache *psc = path_send_cache.getptr(from_path); - if (!psc) { - //path is not cached, create - path_send_cache[from_path] = PathSentCache(); - psc = path_send_cache.getptr(from_path); - psc->id = last_send_cache_id++; - } - - //create base packet, lots of hardcode because it must be tight - - int ofs = 0; - -#define MAKE_ROOM(m_amount) \ - if (packet_cache.size() < m_amount) packet_cache.resize(m_amount); - - //encode type - MAKE_ROOM(1); - packet_cache[0] = p_set ? NETWORK_COMMAND_REMOTE_SET : NETWORK_COMMAND_REMOTE_CALL; - ofs += 1; - - //encode ID - MAKE_ROOM(ofs + 4); - encode_uint32(psc->id, &packet_cache[ofs]); - ofs += 4; - - //encode function name - CharString name = String(p_name).utf8(); - int len = encode_cstring(name.get_data(), NULL); - MAKE_ROOM(ofs + len); - encode_cstring(name.get_data(), &packet_cache[ofs]); - ofs += len; - - if (p_set) { - //set argument - Error err = encode_variant(*p_arg[0], NULL, len); - ERR_FAIL_COND(err != OK); - MAKE_ROOM(ofs + len); - encode_variant(*p_arg[0], &packet_cache[ofs], len); - ofs += len; - - } else { - //call arguments - MAKE_ROOM(ofs + 1); - packet_cache[ofs] = p_argcount; - ofs += 1; - for (int i = 0; i < p_argcount; i++) { - Error err = encode_variant(*p_arg[i], NULL, len); - ERR_FAIL_COND(err != OK); - MAKE_ROOM(ofs + len); - encode_variant(*p_arg[i], &packet_cache[ofs], len); - ofs += len; - } - } - - //see if all peers have cached path (is so, call can be fast) - bool has_all_peers = true; - - List<int> peers_to_add; //if one is missing, take note to add it - - for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { - - if (p_to < 0 && E->get() == -p_to) - continue; //continue, excluded - - if (p_to > 0 && E->get() != p_to) - continue; //continue, not for this peer - - Map<int, bool>::Element *F = psc->confirmed_peers.find(E->get()); - - if (!F || F->get() == false) { - //path was not cached, or was cached but is unconfirmed - if (!F) { - //not cached at all, take note - peers_to_add.push_back(E->get()); - } - - has_all_peers = false; - } - } - - //those that need to be added, send a message for this - - for (List<int>::Element *E = peers_to_add.front(); E; E = E->next()) { - - //encode function name - CharString pname = String(from_path).utf8(); - int len = encode_cstring(pname.get_data(), NULL); - - Vector<uint8_t> packet; - - packet.resize(1 + 4 + len); - packet[0] = NETWORK_COMMAND_SIMPLIFY_PATH; - encode_uint32(psc->id, &packet[1]); - encode_cstring(pname.get_data(), &packet[5]); - - network_peer->set_target_peer(E->get()); //to all of you - network_peer->set_transfer_mode(NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); - network_peer->put_packet(packet.ptr(), packet.size()); - - psc->confirmed_peers.insert(E->get(), false); //insert into confirmed, but as false since it was not confirmed - } - - //take chance and set transfer mode, since all send methods will use it - network_peer->set_transfer_mode(p_unreliable ? NetworkedMultiplayerPeer::TRANSFER_MODE_UNRELIABLE : NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); - - if (has_all_peers) { - - //they all have verified paths, so send fast - network_peer->set_target_peer(p_to); //to all of you - network_peer->put_packet(packet_cache.ptr(), ofs); //a message with love - } else { - //not all verified path, so send one by one - - //apend path at the end, since we will need it for some packets - CharString pname = String(from_path).utf8(); - int path_len = encode_cstring(pname.get_data(), NULL); - MAKE_ROOM(ofs + path_len); - encode_cstring(pname.get_data(), &packet_cache[ofs]); - - for (Set<int>::Element *E = connected_peers.front(); E; E = E->next()) { - - if (p_to < 0 && E->get() == -p_to) - continue; //continue, excluded - - if (p_to > 0 && E->get() != p_to) - continue; //continue, not for this peer - - Map<int, bool>::Element *F = psc->confirmed_peers.find(E->get()); - ERR_CONTINUE(!F); //should never happen - - network_peer->set_target_peer(E->get()); //to this one specifically - - if (F->get() == true) { - //this one confirmed path, so use id - encode_uint32(psc->id, &packet_cache[1]); - network_peer->put_packet(packet_cache.ptr(), ofs); - } else { - //this one did not confirm path yet, so use entire path (sorry!) - encode_uint32(0x80000000 | ofs, &packet_cache[1]); //offset to path and flag - network_peer->put_packet(packet_cache.ptr(), ofs + path_len); - } - } - } -} - -void SceneTree::_network_process_packet(int p_from, const uint8_t *p_packet, int p_packet_len) { - - ERR_FAIL_COND(p_packet_len < 5); - - uint8_t packet_type = p_packet[0]; - - switch (packet_type) { - - case NETWORK_COMMAND_REMOTE_CALL: - case NETWORK_COMMAND_REMOTE_SET: { - - ERR_FAIL_COND(p_packet_len < 5); - uint32_t target = decode_uint32(&p_packet[1]); - - Node *node = NULL; - - if (target & 0x80000000) { - //use full path (not cached yet) - - int ofs = target & 0x7FFFFFFF; - ERR_FAIL_COND(ofs >= p_packet_len); - - String paths; - paths.parse_utf8((const char *)&p_packet[ofs], p_packet_len - ofs); - - NodePath np = paths; - - node = get_root()->get_node(np); - if (node == NULL) { - ERR_EXPLAIN("Failed to get path from RPC: " + String(np)); - ERR_FAIL_COND(node == NULL); - } - } else { - //use cached path - int id = target; - - Map<int, PathGetCache>::Element *E = path_get_cache.find(p_from); - ERR_FAIL_COND(!E); - - Map<int, PathGetCache::NodeInfo>::Element *F = E->get().nodes.find(id); - ERR_FAIL_COND(!F); - - PathGetCache::NodeInfo *ni = &F->get(); - //do proper caching later - - node = get_root()->get_node(ni->path); - if (node == NULL) { - ERR_EXPLAIN("Failed to get cached path from RPC: " + String(ni->path)); - ERR_FAIL_COND(node == NULL); - } - } - - ERR_FAIL_COND(p_packet_len < 6); - - //detect cstring end - int len_end = 5; - for (; len_end < p_packet_len; len_end++) { - if (p_packet[len_end] == 0) { - break; - } - } - - ERR_FAIL_COND(len_end >= p_packet_len); - - StringName name = String::utf8((const char *)&p_packet[5]); - - if (packet_type == NETWORK_COMMAND_REMOTE_CALL) { - - if (!node->can_call_rpc(name, p_from)) - return; - - int ofs = len_end + 1; - - ERR_FAIL_COND(ofs >= p_packet_len); - - int argc = p_packet[ofs]; - Vector<Variant> args; - Vector<const Variant *> argp; - args.resize(argc); - argp.resize(argc); - - ofs++; - - for (int i = 0; i < argc; i++) { - - ERR_FAIL_COND(ofs >= p_packet_len); - int vlen; - Error err = decode_variant(args[i], &p_packet[ofs], p_packet_len - ofs, &vlen); - ERR_FAIL_COND(err != OK); - //args[i]=p_packet[3+i]; - argp[i] = &args[i]; - ofs += vlen; - } - - Variant::CallError ce; - - node->call(name, (const Variant **)argp.ptr(), argc, ce); - if (ce.error != Variant::CallError::CALL_OK) { - String error = Variant::get_call_error_text(node, name, (const Variant **)argp.ptr(), argc, ce); - error = "RPC - " + error; - ERR_PRINTS(error); - } - - } else { - - if (!node->can_call_rset(name, p_from)) - return; - - int ofs = len_end + 1; - - ERR_FAIL_COND(ofs >= p_packet_len); - - Variant value; - decode_variant(value, &p_packet[ofs], p_packet_len - ofs); - - bool valid; - - node->set(name, value, &valid); - if (!valid) { - String error = "Error setting remote property '" + String(name) + "', not found in object of type " + node->get_class(); - ERR_PRINTS(error); - } - } - - } break; - case NETWORK_COMMAND_SIMPLIFY_PATH: { - - ERR_FAIL_COND(p_packet_len < 5); - int id = decode_uint32(&p_packet[1]); - - String paths; - paths.parse_utf8((const char *)&p_packet[5], p_packet_len - 5); - - NodePath path = paths; - - if (!path_get_cache.has(p_from)) { - path_get_cache[p_from] = PathGetCache(); - } - - PathGetCache::NodeInfo ni; - ni.path = path; - ni.instance = 0; - - path_get_cache[p_from].nodes[id] = ni; - - { - //send ack - - //encode path - CharString pname = String(path).utf8(); - int len = encode_cstring(pname.get_data(), NULL); - - Vector<uint8_t> packet; - - packet.resize(1 + len); - packet[0] = NETWORK_COMMAND_CONFIRM_PATH; - encode_cstring(pname.get_data(), &packet[1]); - - network_peer->set_transfer_mode(NetworkedMultiplayerPeer::TRANSFER_MODE_RELIABLE); - network_peer->set_target_peer(p_from); - network_peer->put_packet(packet.ptr(), packet.size()); - } - } break; - case NETWORK_COMMAND_CONFIRM_PATH: { - - String paths; - paths.parse_utf8((const char *)&p_packet[1], p_packet_len - 1); - - NodePath path = paths; - - PathSentCache *psc = path_send_cache.getptr(path); - ERR_FAIL_COND(!psc); - - Map<int, bool>::Element *E = psc->confirmed_peers.find(p_from); - ERR_FAIL_COND(!E); - E->get() = true; - } break; - } -} - -void SceneTree::_network_poll() { - - if (!network_peer.is_valid() || network_peer->get_connection_status() == NetworkedMultiplayerPeer::CONNECTION_DISCONNECTED) - return; - - network_peer->poll(); - - if (!network_peer.is_valid()) //it's possible that polling might have resulted in a disconnection, so check here - return; - - while (network_peer->get_available_packet_count()) { - - int sender = network_peer->get_packet_peer(); - const uint8_t *packet; - int len; - - Error err = network_peer->get_packet(&packet, len); - if (err != OK) { - ERR_PRINT("Error getting packet!"); - } - - rpc_sender_id = sender; - _network_process_packet(sender, packet, len); - rpc_sender_id = 0; - - if (!network_peer.is_valid()) { - break; //it's also possible that a packet or RPC caused a disconnection, so also check here - } - } + return multiplayer_api->is_refusing_new_network_connections(); } void SceneTree::_bind_methods() { @@ -2196,6 +1790,8 @@ void SceneTree::_bind_methods() { ClassDB::bind_method(D_METHOD("_change_scene"), &SceneTree::_change_scene); + ClassDB::bind_method(D_METHOD("set_multiplayer_api", "multiplayer_api"), &SceneTree::set_multiplayer_api); + ClassDB::bind_method(D_METHOD("get_multiplayer_api"), &SceneTree::get_multiplayer_api); ClassDB::bind_method(D_METHOD("set_network_peer", "peer"), &SceneTree::set_network_peer); ClassDB::bind_method(D_METHOD("get_network_peer"), &SceneTree::get_network_peer); ClassDB::bind_method(D_METHOD("is_network_server"), &SceneTree::is_network_server); @@ -2225,6 +1821,7 @@ void SceneTree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "current_scene", PROPERTY_HINT_RESOURCE_TYPE, "Node", 0), "set_current_scene", "get_current_scene"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "network_peer", PROPERTY_HINT_RESOURCE_TYPE, "NetworkedMultiplayerPeer", 0), "set_network_peer", "get_network_peer"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root", PROPERTY_HINT_RESOURCE_TYPE, "Node", 0), "", "get_root"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "multiplayer_api", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", 0), "set_multiplayer_api", "get_multiplayer_api"); ADD_SIGNAL(MethodInfo("tree_changed")); ADD_SIGNAL(MethodInfo("node_added", PropertyInfo(Variant::OBJECT, "node"))); @@ -2320,7 +1917,6 @@ SceneTree::SceneTree() { call_lock = 0; root_lock = 0; node_count = 0; - rpc_sender_id = 0; //create with mainloop @@ -2329,6 +1925,9 @@ SceneTree::SceneTree() { if (!root->get_world().is_valid()) root->set_world(Ref<World>(memnew(World))); + // Initialize network state + set_multiplayer_api(Ref<MultiplayerAPI>(memnew(MultiplayerAPI))); + //root->set_world_2d( Ref<World2D>( memnew( World2D ))); root->set_as_audio_listener(true); root->set_as_audio_listener_2d(true); @@ -2423,8 +2022,6 @@ SceneTree::SceneTree() { live_edit_root = NodePath("/root"); - last_send_cache_id = 1; - #endif use_font_oversampling = false; diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index c5357762ec..203c1d9c9c 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -31,7 +31,7 @@ #ifndef SCENE_MAIN_LOOP_H #define SCENE_MAIN_LOOP_H -#include "io/networked_multiplayer_peer.h" +#include "io/multiplayer_api.h" #include "os/main_loop.h" #include "os/thread_safe.h" #include "scene/resources/mesh.h" @@ -185,16 +185,8 @@ private: ///network/// - enum NetworkCommands { - NETWORK_COMMAND_REMOTE_CALL, - NETWORK_COMMAND_REMOTE_SET, - NETWORK_COMMAND_SIMPLIFY_PATH, - NETWORK_COMMAND_CONFIRM_PATH, - }; - - Ref<NetworkedMultiplayerPeer> network_peer; + Ref<MultiplayerAPI> multiplayer_api; - Set<int> connected_peers; void _network_peer_connected(int p_id); void _network_peer_disconnected(int p_id); @@ -202,39 +194,9 @@ private: void _connection_failed(); void _server_disconnected(); - int rpc_sender_id; - - //path sent caches - struct PathSentCache { - Map<int, bool> confirmed_peers; - int id; - }; - - HashMap<NodePath, PathSentCache> path_send_cache; - int last_send_cache_id; - - //path get caches - struct PathGetCache { - struct NodeInfo { - NodePath path; - ObjectID instance; - }; - - Map<int, NodeInfo> nodes; - }; - - Map<int, PathGetCache> path_get_cache; - - Vector<uint8_t> packet_cache; - - void _network_process_packet(int p_from, const uint8_t *p_packet, int p_packet_len); - void _network_poll(); - static SceneTree *singleton; friend class Node; - void _rpc(Node *p_from, int p_to, bool p_unreliable, bool p_set, const StringName &p_name, const Variant **p_arg, int p_argcount); - void tree_changed(); void node_added(Node *p_node); void node_removed(Node *p_node); @@ -450,6 +412,8 @@ public: //network API + Ref<MultiplayerAPI> get_multiplayer_api() const; + void set_multiplayer_api(Ref<MultiplayerAPI> p_multiplayer_api); void set_network_peer(const Ref<NetworkedMultiplayerPeer> &p_network_peer); Ref<NetworkedMultiplayerPeer> get_network_peer() const; bool is_network_server() const; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 08fbf44469..45a969eeda 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -181,6 +181,7 @@ public: Viewport::GUI::GUI() { mouse_focus = NULL; + mouse_click_grabber = NULL; mouse_focus_button = -1; key_focus = NULL; mouse_over = NULL; @@ -2278,7 +2279,7 @@ List<Control *>::Element *Viewport::_gui_show_modal(Control *p_control) { else p_control->_modal_set_prev_focus_owner(0); - if (gui.mouse_focus && !p_control->is_a_parent_of(gui.mouse_focus)) { + if (gui.mouse_focus && !p_control->is_a_parent_of(gui.mouse_focus) && !gui.mouse_click_grabber) { Ref<InputEventMouseButton> mb; mb.instance(); mb->set_position(gui.mouse_focus->get_local_mouse_position()); @@ -2300,9 +2301,22 @@ Control *Viewport::_gui_get_focus_owner() { void Viewport::_gui_grab_click_focus(Control *p_control) { + gui.mouse_click_grabber = p_control; + call_deferred("_post_gui_grab_click_focus"); +} + +void Viewport::_post_gui_grab_click_focus() { + + Control *focus_grabber = gui.mouse_click_grabber; + if (!focus_grabber) { + // Redundant grab requests were made + return; + } + gui.mouse_click_grabber = NULL; + if (gui.mouse_focus) { - if (gui.mouse_focus == p_control) + if (gui.mouse_focus == focus_grabber) return; Ref<InputEventMouseButton> mb; mb.instance(); @@ -2313,9 +2327,9 @@ void Viewport::_gui_grab_click_focus(Control *p_control) { mb->set_position(click); mb->set_button_index(gui.mouse_focus_button); mb->set_pressed(false); - gui.mouse_focus->call_deferred(SceneStringNames::get_singleton()->_gui_input, mb); + gui.mouse_focus->call_multilevel(SceneStringNames::get_singleton()->_gui_input, mb); - gui.mouse_focus = p_control; + gui.mouse_focus = focus_grabber; gui.focus_inv_xform = gui.mouse_focus->get_global_transform_with_canvas().affine_inverse(); click = gui.mouse_focus->get_global_transform_with_canvas().affine_inverse().xform(gui.last_mouse_pos); mb->set_position(click); @@ -2648,6 +2662,7 @@ void Viewport::_bind_methods() { ClassDB::bind_method(D_METHOD("_gui_show_tooltip"), &Viewport::_gui_show_tooltip); ClassDB::bind_method(D_METHOD("_gui_remove_focus"), &Viewport::_gui_remove_focus); + ClassDB::bind_method(D_METHOD("_post_gui_grab_click_focus"), &Viewport::_post_gui_grab_click_focus); ClassDB::bind_method(D_METHOD("set_shadow_atlas_size", "size"), &Viewport::set_shadow_atlas_size); ClassDB::bind_method(D_METHOD("get_shadow_atlas_size"), &Viewport::get_shadow_atlas_size); diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 07bbd3f1fa..94e49033e0 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -248,6 +248,7 @@ private: bool key_event_accepted; Control *mouse_focus; + Control *mouse_click_grabber; int mouse_focus_button; Control *key_focus; Control *mouse_over; @@ -323,6 +324,7 @@ private: bool _gui_control_has_focus(const Control *p_control); void _gui_control_grab_focus(Control *p_control); void _gui_grab_click_focus(Control *p_control); + void _post_gui_grab_click_focus(); void _gui_accept_event(); Control *_gui_get_focus_owner(); diff --git a/scene/resources/default_theme/default_theme.cpp b/scene/resources/default_theme/default_theme.cpp index 3e244aa8f8..ea70797530 100644 --- a/scene/resources/default_theme/default_theme.cpp +++ b/scene/resources/default_theme/default_theme.cpp @@ -582,6 +582,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const theme->set_icon("checked", "PopupMenu", make_icon(checked_png)); theme->set_icon("unchecked", "PopupMenu", make_icon(unchecked_png)); + theme->set_icon("radio_checked", "PopupMenu", make_icon(radio_checked_png)); + theme->set_icon("radio_unchecked", "PopupMenu", make_icon(radio_unchecked_png)); theme->set_icon("submenu", "PopupMenu", make_icon(submenu_png)); theme->set_font("font", "PopupMenu", default_font); diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 3df9285bb6..846f6e356e 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -249,6 +249,8 @@ Node *SceneState::instance(GenEditState p_edit_state) const { //must make a copy, because this res is local to scene } } + } else if (p_edit_state == GEN_EDIT_STATE_INSTANCE) { + value = value.duplicate(true); // Duplicate arrays and dictionaries for the editor } node->set(snames[nprops[j].name], value, &valid); } diff --git a/scene/resources/scene_format_text.cpp b/scene/resources/scene_format_text.cpp index 030b822f3b..597866eb74 100644 --- a/scene/resources/scene_format_text.cpp +++ b/scene/resources/scene_format_text.cpp @@ -1672,7 +1672,7 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const RES &p_r f->store_string(vars); } - f->store_line("]\n"); + f->store_line("]"); for (int j = 0; j < state->get_node_property_count(i); j++) { @@ -1682,10 +1682,7 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const RES &p_r f->store_string(_valprop(String(state->get_node_property_name(i, j))) + " = " + vars + "\n"); } - if (state->get_node_property_count(i)) { - //add space - f->store_line(String()); - } + f->store_line(String()); } for (int i = 0; i < state->get_connection_count(); i++) { @@ -1708,14 +1705,12 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const RES &p_r f->store_string(" binds= " + vars); } - f->store_line("]\n"); + f->store_line("]"); } - f->store_line(String()); - Vector<NodePath> editable_instances = state->get_editable_instances(); for (int i = 0; i < editable_instances.size(); i++) { - f->store_line("[editable path=\"" + editable_instances[i].operator String() + "\"]"); + f->store_line("\n[editable path=\"" + editable_instances[i].operator String() + "\"]"); } } diff --git a/scene/resources/surface_tool.cpp b/scene/resources/surface_tool.cpp index 07c1036a10..5a42873d79 100644 --- a/scene/resources/surface_tool.cpp +++ b/scene/resources/surface_tool.cpp @@ -861,7 +861,7 @@ void SurfaceTool::generate_tangents() { } } -void SurfaceTool::generate_normals() { +void SurfaceTool::generate_normals(bool p_flip) { ERR_FAIL_COND(primitive != Mesh::PRIMITIVE_TRIANGLES); @@ -887,7 +887,11 @@ void SurfaceTool::generate_normals() { ERR_FAIL_COND(!v[2]); E = v[2]->next(); - Vector3 normal = Plane(v[0]->get().vertex, v[1]->get().vertex, v[2]->get().vertex).normal; + Vector3 normal; + if (!p_flip) + normal = Plane(v[0]->get().vertex, v[1]->get().vertex, v[2]->get().vertex).normal; + else + normal = Plane(v[2]->get().vertex, v[1]->get().vertex, v[0]->get().vertex).normal; if (smooth) { @@ -980,7 +984,7 @@ void SurfaceTool::_bind_methods() { ClassDB::bind_method(D_METHOD("index"), &SurfaceTool::index); ClassDB::bind_method(D_METHOD("deindex"), &SurfaceTool::deindex); - ClassDB::bind_method(D_METHOD("generate_normals"), &SurfaceTool::generate_normals); + ClassDB::bind_method(D_METHOD("generate_normals", "flip"), &SurfaceTool::generate_normals, DEFVAL(false)); ClassDB::bind_method(D_METHOD("generate_tangents"), &SurfaceTool::generate_tangents); ClassDB::bind_method(D_METHOD("add_to_format", "flags"), &SurfaceTool::add_to_format); diff --git a/scene/resources/surface_tool.h b/scene/resources/surface_tool.h index 7a9aa349bb..459d399380 100644 --- a/scene/resources/surface_tool.h +++ b/scene/resources/surface_tool.h @@ -116,7 +116,7 @@ public: void index(); void deindex(); - void generate_normals(); + void generate_normals(bool p_flip = false); void generate_tangents(); void add_to_format(int p_flags) { format |= p_flags; } diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp index 4463f98b07..bebbf6e238 100644 --- a/scene/resources/tile_set.cpp +++ b/scene/resources/tile_set.cpp @@ -347,6 +347,7 @@ void TileSet::tile_set_modulate(int p_id, const Color &p_modulate) { ERR_FAIL_COND(!tile_map.has(p_id)); tile_map[p_id].modulate = p_modulate; emit_changed(); + _change_notify("modulate"); } Color TileSet::tile_get_modulate(int p_id) const { @@ -918,6 +919,8 @@ void TileSet::_bind_methods() { ClassDB::bind_method(D_METHOD("tile_get_shape_count", "id"), &TileSet::tile_get_shape_count); ClassDB::bind_method(D_METHOD("tile_set_shapes", "id", "shapes"), &TileSet::_tile_set_shapes); ClassDB::bind_method(D_METHOD("tile_get_shapes", "id"), &TileSet::_tile_get_shapes); + ClassDB::bind_method(D_METHOD("tile_set_tile_mode", "id", "tilemode"), &TileSet::tile_set_tile_mode); + ClassDB::bind_method(D_METHOD("tile_get_tile_mode", "id"), &TileSet::tile_get_tile_mode); ClassDB::bind_method(D_METHOD("tile_set_navigation_polygon", "id", "navigation_polygon"), &TileSet::tile_set_navigation_polygon); ClassDB::bind_method(D_METHOD("tile_get_navigation_polygon", "id"), &TileSet::tile_get_navigation_polygon); ClassDB::bind_method(D_METHOD("tile_set_navigation_polygon_offset", "id", "navigation_polygon_offset"), &TileSet::tile_set_navigation_polygon_offset); @@ -947,6 +950,10 @@ void TileSet::_bind_methods() { BIND_ENUM_CONSTANT(BIND_BOTTOMLEFT); BIND_ENUM_CONSTANT(BIND_BOTTOM); BIND_ENUM_CONSTANT(BIND_BOTTOMRIGHT); + + BIND_ENUM_CONSTANT(SINGLE_TILE); + BIND_ENUM_CONSTANT(AUTO_TILE); + BIND_ENUM_CONSTANT(ANIMATED_TILE); } TileSet::TileSet() { diff --git a/scene/resources/tile_set.h b/scene/resources/tile_set.h index 46f34b6252..706d04998f 100644 --- a/scene/resources/tile_set.h +++ b/scene/resources/tile_set.h @@ -238,5 +238,6 @@ public: VARIANT_ENUM_CAST(TileSet::AutotileBindings); VARIANT_ENUM_CAST(TileSet::BitmaskMode); +VARIANT_ENUM_CAST(TileSet::TileMode); #endif // TILE_SET_H diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index c3b0de6d9a..b08e41301a 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -101,6 +101,18 @@ int AudioDriver::get_total_channels_by_speaker_mode(AudioDriver::SpeakerMode p_m ERR_FAIL_V(2); } +Array AudioDriver::get_device_list() { + Array list; + + list.push_back("Default"); + + return list; +} + +String AudioDriver::get_device() { + return "Default"; +} + AudioDriver::AudioDriver() { _last_mix_time = 0; @@ -1108,6 +1120,21 @@ Ref<AudioBusLayout> AudioServer::generate_bus_layout() const { return state; } +Array AudioServer::get_device_list() { + + return AudioDriver::get_singleton()->get_device_list(); +} + +String AudioServer::get_device() { + + return AudioDriver::get_singleton()->get_device(); +} + +void AudioServer::set_device(String device) { + + AudioDriver::get_singleton()->set_device(device); +} + void AudioServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_bus_count", "amount"), &AudioServer::set_bus_count); @@ -1154,6 +1181,9 @@ void AudioServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_speaker_mode"), &AudioServer::get_speaker_mode); ClassDB::bind_method(D_METHOD("get_mix_rate"), &AudioServer::get_mix_rate); + ClassDB::bind_method(D_METHOD("get_device_list"), &AudioServer::get_device_list); + ClassDB::bind_method(D_METHOD("get_device"), &AudioServer::get_device); + ClassDB::bind_method(D_METHOD("set_device"), &AudioServer::set_device); ClassDB::bind_method(D_METHOD("set_bus_layout", "bus_layout"), &AudioServer::set_bus_layout); ClassDB::bind_method(D_METHOD("generate_bus_layout"), &AudioServer::generate_bus_layout); diff --git a/servers/audio_server.h b/servers/audio_server.h index a8be48b4c3..af2668b69e 100644 --- a/servers/audio_server.h +++ b/servers/audio_server.h @@ -70,6 +70,9 @@ public: virtual void start() = 0; virtual int get_mix_rate() const = 0; virtual SpeakerMode get_speaker_mode() const = 0; + virtual Array get_device_list(); + virtual String get_device(); + virtual void set_device(String device) {} virtual void lock() = 0; virtual void unlock() = 0; virtual void finish() = 0; @@ -300,6 +303,10 @@ public: void set_bus_layout(const Ref<AudioBusLayout> &p_bus_layout); Ref<AudioBusLayout> generate_bus_layout() const; + Array get_device_list(); + String get_device(); + void set_device(String device); + AudioServer(); virtual ~AudioServer(); }; diff --git a/servers/physics/collision_object_sw.h b/servers/physics/collision_object_sw.h index f5d32e56a0..dee28bb6df 100644 --- a/servers/physics/collision_object_sw.h +++ b/servers/physics/collision_object_sw.h @@ -37,7 +37,8 @@ #include "shape_sw.h" #ifdef DEBUG_ENABLED -#define MAX_OBJECT_DISTANCE 10000000.0 +#define MAX_OBJECT_DISTANCE 3.1622776601683791e+18 + #define MAX_OBJECT_DISTANCE_X2 (MAX_OBJECT_DISTANCE * MAX_OBJECT_DISTANCE) #endif diff --git a/servers/physics_2d/collision_object_2d_sw.cpp b/servers/physics_2d/collision_object_2d_sw.cpp index ce06aa9a2b..23084a4241 100644 --- a/servers/physics_2d/collision_object_2d_sw.cpp +++ b/servers/physics_2d/collision_object_2d_sw.cpp @@ -73,6 +73,27 @@ void CollisionObject2DSW::set_shape_transform(int p_index, const Transform2D &p_ _shapes_changed(); } +void CollisionObject2DSW::set_shape_as_disabled(int p_idx, bool p_disabled) { + ERR_FAIL_INDEX(p_idx, shapes.size()); + + CollisionObject2DSW::Shape &shape = shapes[p_idx]; + if (shape.disabled == p_disabled) + return; + + shape.disabled = p_disabled; + + if (!space) + return; + + if (p_disabled && shape.bpid != 0) { + space->get_broadphase()->remove(shape.bpid); + shape.bpid = 0; + _update_shapes(); + } else if (!p_disabled && shape.bpid == 0) { + _update_shapes(); // automatically adds shape with bpid == 0 + } +} + void CollisionObject2DSW::remove_shape(Shape2DSW *p_shape) { //remove a shape, all the times it appears @@ -139,6 +160,10 @@ void CollisionObject2DSW::_update_shapes() { for (int i = 0; i < shapes.size(); i++) { Shape &s = shapes[i]; + + if (s.disabled) + continue; + if (s.bpid == 0) { s.bpid = space->get_broadphase()->create(this, i); space->get_broadphase()->set_static(s.bpid, _static); @@ -163,6 +188,9 @@ void CollisionObject2DSW::_update_shapes_with_motion(const Vector2 &p_motion) { for (int i = 0; i < shapes.size(); i++) { Shape &s = shapes[i]; + if (s.disabled) + continue; + if (s.bpid == 0) { s.bpid = space->get_broadphase()->create(this, i); space->get_broadphase()->set_static(s.bpid, _static); diff --git a/servers/physics_2d/collision_object_2d_sw.h b/servers/physics_2d/collision_object_2d_sw.h index 5f25c27158..ab3e219ac0 100644 --- a/servers/physics_2d/collision_object_2d_sw.h +++ b/servers/physics_2d/collision_object_2d_sw.h @@ -136,10 +136,7 @@ public: _FORCE_INLINE_ Transform2D get_inv_transform() const { return inv_transform; } _FORCE_INLINE_ Space2DSW *get_space() const { return space; } - _FORCE_INLINE_ void set_shape_as_disabled(int p_idx, bool p_disabled) { - ERR_FAIL_INDEX(p_idx, shapes.size()); - shapes[p_idx].disabled = p_disabled; - } + void set_shape_as_disabled(int p_idx, bool p_disabled); _FORCE_INLINE_ bool is_shape_set_as_disabled(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, shapes.size(), false); return shapes[p_idx].disabled; diff --git a/servers/visual/shader_types.cpp b/servers/visual/shader_types.cpp index 9042649337..2ab52d13b8 100644 --- a/servers/visual/shader_types.cpp +++ b/servers/visual/shader_types.cpp @@ -218,9 +218,8 @@ ShaderTypes::ShaderTypes() { shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["LIGHT_HEIGHT"] = ShaderLanguage::TYPE_FLOAT; shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["LIGHT_COLOR"] = ShaderLanguage::TYPE_VEC4; shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["LIGHT_UV"] = ShaderLanguage::TYPE_VEC2; - shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["LIGHT_SHADOW"] = ShaderLanguage::TYPE_VEC4; shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["LIGHT"] = ShaderLanguage::TYPE_VEC4; - shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["SHADOW"] = ShaderLanguage::TYPE_VEC4; + shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["SHADOW_COLOR"] = ShaderLanguage::TYPE_VEC4; shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["POINT_COORD"] = constt(ShaderLanguage::TYPE_VEC2); shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].built_ins["TIME"] = constt(ShaderLanguage::TYPE_FLOAT); shader_modes[VS::SHADER_CANVAS_ITEM].functions["light"].can_discard = true; @@ -254,7 +253,6 @@ ShaderTypes::ShaderTypes() { shader_modes[VS::SHADER_PARTICLES].functions["vertex"].built_ins["RANDOM_SEED"] = constt(ShaderLanguage::TYPE_UINT); shader_modes[VS::SHADER_PARTICLES].functions["vertex"].can_discard = false; - shader_modes[VS::SHADER_PARTICLES].modes.insert("billboard"); shader_modes[VS::SHADER_PARTICLES].modes.insert("disable_force"); shader_modes[VS::SHADER_PARTICLES].modes.insert("disable_velocity"); shader_modes[VS::SHADER_PARTICLES].modes.insert("keep_data"); |