diff options
Diffstat (limited to 'modules')
142 files changed, 4317 insertions, 3241 deletions
diff --git a/modules/basis_universal/register_types.cpp b/modules/basis_universal/register_types.cpp index e80d453df7..155f7809b0 100644 --- a/modules/basis_universal/register_types.cpp +++ b/modules/basis_universal/register_types.cpp @@ -253,7 +253,7 @@ static Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size }; image.instantiate(); - image->create(info.m_width, info.m_height, info.m_total_levels > 1, imgfmt, gpudata); + image->set_data(info.m_width, info.m_height, info.m_total_levels > 1, imgfmt, gpudata); return image; } diff --git a/modules/bmp/image_loader_bmp.cpp b/modules/bmp/image_loader_bmp.cpp index cc21ed28e8..681b3e74bc 100644 --- a/modules/bmp/image_loader_bmp.cpp +++ b/modules/bmp/image_loader_bmp.cpp @@ -156,7 +156,7 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, if (p_color_buffer == nullptr || color_table_size == 0) { // regular pixels - p_image->create(width, height, false, Image::FORMAT_RGBA8, data); + p_image->set_data(width, height, false, Image::FORMAT_RGBA8, data); } else { // data is in indexed format, extend it @@ -194,7 +194,7 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, dest += 4; } - p_image->create(width, height, false, Image::FORMAT_RGBA8, extended_data); + p_image->set_data(width, height, false, Image::FORMAT_RGBA8, extended_data); } } return err; diff --git a/modules/camera/camera_macos.mm b/modules/camera/camera_macos.mm index 0b9696a3e9..0e61dde8e9 100644 --- a/modules/camera/camera_macos.mm +++ b/modules/camera/camera_macos.mm @@ -158,7 +158,7 @@ memcpy(w, dataY, new_width * new_height); img[0].instantiate(); - img[0]->create(new_width, new_height, 0, Image::FORMAT_R8, img_data[0]); + img[0]->set_data(new_width, new_height, 0, Image::FORMAT_R8, img_data[0]); } { @@ -177,7 +177,7 @@ ///TODO OpenGL doesn't support FORMAT_RG8, need to do some form of conversion img[1].instantiate(); - img[1]->create(new_width, new_height, 0, Image::FORMAT_RG8, img_data[1]); + img[1]->set_data(new_width, new_height, 0, Image::FORMAT_RG8, img_data[1]); } // set our texture... diff --git a/modules/csg/doc_classes/CSGShape3D.xml b/modules/csg/doc_classes/CSGShape3D.xml index 7e92d667b3..06f8f5a403 100644 --- a/modules/csg/doc_classes/CSGShape3D.xml +++ b/modules/csg/doc_classes/CSGShape3D.xml @@ -64,9 +64,10 @@ A contact is detected if object A is in any of the layers that object B scans, or object B is in any layer scanned by object A. See [url=$DOCS_URL/tutorials/physics/physics_introduction.html#collision-layers-and-masks]Collision layers and masks[/url] in the documentation for more information. </member> <member name="collision_mask" type="int" setter="set_collision_mask" getter="get_collision_mask" default="1"> - The physics layers this CSG shape scans for collisions. See [url=$DOCS_URL/tutorials/physics/physics_introduction.html#collision-layers-and-masks]Collision layers and masks[/url] in the documentation for more information. + The physics layers this CSG shape scans for collisions. Only effective if [member use_collision] is [code]true[/code]. See [url=$DOCS_URL/tutorials/physics/physics_introduction.html#collision-layers-and-masks]Collision layers and masks[/url] in the documentation for more information. </member> <member name="collision_priority" type="float" setter="set_collision_priority" getter="get_collision_priority" default="1.0"> + The priority used to solve colliding when occurring penetration. Only effective if [member use_collision] is [code]true[/code]. The higher the priority is, the lower the penetration into the object will be. This can for example be used to prevent the player from breaking through the boundaries of a level. </member> <member name="operation" type="int" setter="set_operation" getter="get_operation" enum="CSGShape3D.Operation" default="0"> The operation that is performed on this shape. This is ignored for the first CSG child node as the operation is between this node and the previous child of this nodes parent. @@ -75,7 +76,7 @@ Snap makes the mesh snap to a given distance so that the faces of two meshes can be perfectly aligned. A lower value results in greater precision but may be harder to adjust. </member> <member name="use_collision" type="bool" setter="set_use_collision" getter="is_using_collision" default="false"> - Adds a collision shape to the physics engine for our CSG shape. This will always act like a static body. Note that the collision shape is still active even if the CSG shape itself is hidden. + Adds a collision shape to the physics engine for our CSG shape. This will always act like a static body. Note that the collision shape is still active even if the CSG shape itself is hidden. See also [member collision_mask] and [member collision_priority]. </member> </members> <constants> diff --git a/modules/cvtt/image_compress_cvtt.cpp b/modules/cvtt/image_compress_cvtt.cpp index 3322ff0a1b..89705e4ee0 100644 --- a/modules/cvtt/image_compress_cvtt.cpp +++ b/modules/cvtt/image_compress_cvtt.cpp @@ -230,7 +230,7 @@ void image_compress_cvtt(Image *p_image, float p_lossy_quality, Image::UsedChann h = MAX(h / 2, 1); } - p_image->create(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); + p_image->set_data(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); } void image_decompress_cvtt(Image *p_image) { @@ -339,5 +339,5 @@ void image_decompress_cvtt(Image *p_image) { w >>= 1; h >>= 1; } - p_image->create(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); + p_image->set_data(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); } diff --git a/modules/denoise/lightmap_denoiser.cpp b/modules/denoise/lightmap_denoiser.cpp index a0dbd07b10..2b9221bc43 100644 --- a/modules/denoise/lightmap_denoiser.cpp +++ b/modules/denoise/lightmap_denoiser.cpp @@ -51,7 +51,7 @@ Ref<Image> LightmapDenoiserOIDN::denoise_image(const Ref<Image> &p_image) { return p_image; } - img->create(img->get_width(), img->get_height(), false, img->get_format(), data); + img->set_data(img->get_width(), img->get_height(), false, img->get_format(), data); return img; } diff --git a/modules/enet/doc_classes/ENetMultiplayerPeer.xml b/modules/enet/doc_classes/ENetMultiplayerPeer.xml index 5181ae76ce..f02fc893fd 100644 --- a/modules/enet/doc_classes/ENetMultiplayerPeer.xml +++ b/modules/enet/doc_classes/ENetMultiplayerPeer.xml @@ -77,8 +77,5 @@ <member name="host" type="ENetConnection" setter="" getter="get_host"> The underlying [ENetConnection] created after [method create_client] and [method create_server]. </member> - <member name="server_relay" type="bool" setter="set_server_relay_enabled" getter="is_server_relay_enabled" default="true"> - Enable or disable the server feature that notifies clients of other peers' connection/disconnection, and relays messages between them. When this option is [code]false[/code], clients won't be automatically notified of other peers and won't be able to send them packets through the server. - </member> </members> </class> diff --git a/modules/enet/doc_classes/ENetPacketPeer.xml b/modules/enet/doc_classes/ENetPacketPeer.xml index 67760ba5e8..52f45b2338 100644 --- a/modules/enet/doc_classes/ENetPacketPeer.xml +++ b/modules/enet/doc_classes/ENetPacketPeer.xml @@ -80,7 +80,7 @@ <return type="void" /> <param index="0" name="ping_interval" type="int" /> <description> - Sets the [code]ping_interval[/code] in milliseconds at which pings will be sent to a peer. Pings are used both to monitor the liveness of the connection and also to dynamically adjust the throttle during periods of low traffic so that the throttle has reasonable responsiveness during traffic spikes. + Sets the [code]ping_interval[/code] in milliseconds at which pings will be sent to a peer. Pings are used both to monitor the liveness of the connection and also to dynamically adjust the throttle during periods of low traffic so that the throttle has reasonable responsiveness during traffic spikes. The default ping interval is [code]500[/code] milliseconds. </description> </method> <method name="reset"> @@ -115,33 +115,43 @@ <param index="2" name="deceleration" type="int" /> <description> Configures throttle parameter for a peer. - Unreliable packets are dropped by ENet in response to the varying conditions of the Internet connection to the peer. The throttle represents a probability that an unreliable packet should not be dropped and thus sent by ENet to the peer. By measuring fluctuations in round trip times of reliable packets over the specified [code]interval[/code], ENet will either increase the probably by the amount specified in the [code]acceleration[/code] parameter, or decrease it by the amount specified in the [code]deceleration[/code] parameter (both are ratios to [constant PACKET_THROTTLE_SCALE]). + Unreliable packets are dropped by ENet in response to the varying conditions of the Internet connection to the peer. The throttle represents a probability that an unreliable packet should not be dropped and thus sent by ENet to the peer. By measuring fluctuations in round trip times of reliable packets over the specified [code]interval[/code], ENet will either increase the probability by the amount specified in the [code]acceleration[/code] parameter, or decrease it by the amount specified in the [code]deceleration[/code] parameter (both are ratios to [constant PACKET_THROTTLE_SCALE]). When the throttle has a value of [constant PACKET_THROTTLE_SCALE], no unreliable packets are dropped by ENet, and so 100% of all unreliable packets will be sent. - When the throttle has a value of 0, all unreliable packets are dropped by ENet, and so 0% of all unreliable packets will be sent. + When the throttle has a value of [code]0[/code], all unreliable packets are dropped by ENet, and so 0% of all unreliable packets will be sent. Intermediate values for the throttle represent intermediate probabilities between 0% and 100% of unreliable packets being sent. The bandwidth limits of the local and foreign hosts are taken into account to determine a sensible limit for the throttle probability above which it should not raise even in the best of conditions. </description> </method> </methods> <constants> <constant name="STATE_DISCONNECTED" value="0" enum="PeerState"> + The peer is disconnected. </constant> <constant name="STATE_CONNECTING" value="1" enum="PeerState"> + The peer is currently attempting to connect. </constant> <constant name="STATE_ACKNOWLEDGING_CONNECT" value="2" enum="PeerState"> + The peer has acknowledged the connection request. </constant> <constant name="STATE_CONNECTION_PENDING" value="3" enum="PeerState"> + The peer is currently connecting. </constant> <constant name="STATE_CONNECTION_SUCCEEDED" value="4" enum="PeerState"> + The peer has successfully connected, but is not ready to communicate with yet ([constant STATE_CONNECTED]). </constant> <constant name="STATE_CONNECTED" value="5" enum="PeerState"> + The peer is currently connected and ready to communicate with. </constant> <constant name="STATE_DISCONNECT_LATER" value="6" enum="PeerState"> + The peer is slated to disconnect after it has no more outgoing packets to send. </constant> <constant name="STATE_DISCONNECTING" value="7" enum="PeerState"> + The peer is currently disconnecting. </constant> <constant name="STATE_ACKNOWLEDGING_DISCONNECT" value="8" enum="PeerState"> + The peer has acknowledged the disconnection request. </constant> <constant name="STATE_ZOMBIE" value="9" enum="PeerState"> + The peer has lost connection, but is not considered truly disconnected (as the peer didn't acknowledge the disconnection request). </constant> <constant name="PEER_PACKET_LOSS" value="0" enum="PeerStatistic"> Mean packet loss of reliable packets as a ratio with respect to the [constant PACKET_LOSS_SCALE]. @@ -150,6 +160,7 @@ Packet loss variance. </constant> <constant name="PEER_PACKET_LOSS_EPOCH" value="2" enum="PeerStatistic"> + The time at which packet loss statistics were last updated (in milliseconds since the connection started). The interval for packet loss statistics updates is 10 seconds, and at least one packet must have been sent since the last statistics update. </constant> <constant name="PEER_ROUND_TRIP_TIME" value="3" enum="PeerStatistic"> Mean packet round trip time for reliable packets. @@ -164,24 +175,31 @@ Variance of the last trip time recorded. </constant> <constant name="PEER_PACKET_THROTTLE" value="7" enum="PeerStatistic"> + The peer's current throttle status. </constant> <constant name="PEER_PACKET_THROTTLE_LIMIT" value="8" enum="PeerStatistic"> + The maximum number of unreliable packets that should not be dropped. This value is always greater than or equal to [code]1[/code]. The initial value is equal to [constant PACKET_THROTTLE_SCALE]. </constant> <constant name="PEER_PACKET_THROTTLE_COUNTER" value="9" enum="PeerStatistic"> + Internal value used to increment the packet throttle counter. The value is hardcoded to [code]7[/code] and cannot be changed. You probably want to look at [constant PEER_PACKET_THROTTLE_ACCELERATION] instead. </constant> <constant name="PEER_PACKET_THROTTLE_EPOCH" value="10" enum="PeerStatistic"> + The time at which throttle statistics were last updated (in milliseconds since the connection started). The interval for throttle statistics updates is [constant PEER_PACKET_THROTTLE_INTERVAL]. </constant> <constant name="PEER_PACKET_THROTTLE_ACCELERATION" value="11" enum="PeerStatistic"> + The throttle's acceleration factor. Higher values will make ENet adapt to fluctuating network conditions faster, causing unrelaible packets to be sent [i]more[/i] often. The default value is [code]2[/code]. </constant> <constant name="PEER_PACKET_THROTTLE_DECELERATION" value="12" enum="PeerStatistic"> + The throttle's deceleration factor. Higher values will make ENet adapt to fluctuating network conditions faster, causing unrelaible packets to be sent [i]less[/i] often. The default value is [code]2[/code]. </constant> <constant name="PEER_PACKET_THROTTLE_INTERVAL" value="13" enum="PeerStatistic"> + The interval over which the lowest mean round trip time should be measured for use by the throttle mechanism (in milliseconds). The default value is [code]5000[/code]. </constant> <constant name="PACKET_LOSS_SCALE" value="65536"> The reference scale for packet loss. See [method get_statistic] and [constant PEER_PACKET_LOSS]. </constant> <constant name="PACKET_THROTTLE_SCALE" value="32"> - The reference value for throttle configuration. See [method throttle_configure]. + The reference value for throttle configuration. The default value is [code]32[/code]. See [method throttle_configure]. </constant> <constant name="FLAG_RELIABLE" value="1"> Mark the packet to be sent as reliable. diff --git a/modules/enet/enet_multiplayer_peer.cpp b/modules/enet/enet_multiplayer_peer.cpp index e7728f4aec..c93db3b972 100644 --- a/modules/enet/enet_multiplayer_peer.cpp +++ b/modules/enet/enet_multiplayer_peer.cpp @@ -44,6 +44,22 @@ int ENetMultiplayerPeer::get_packet_peer() const { return incoming_packets.front()->get().from; } +MultiplayerPeer::TransferMode ENetMultiplayerPeer::get_packet_mode() const { + ERR_FAIL_COND_V_MSG(!_is_active(), TRANSFER_MODE_RELIABLE, "The multiplayer instance isn't currently active."); + ERR_FAIL_COND_V(incoming_packets.size() == 0, TRANSFER_MODE_RELIABLE); + return incoming_packets.front()->get().transfer_mode; +} + +int ENetMultiplayerPeer::get_packet_channel() const { + ERR_FAIL_COND_V_MSG(!_is_active(), 1, "The multiplayer instance isn't currently active."); + ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); + int ch = incoming_packets.front()->get().channel; + if (ch >= SYSCH_MAX) { // First 2 channels are reserved. + return ch - SYSCH_MAX + 1; + } + return 0; +} + Error ENetMultiplayerPeer::create_server(int p_port, int p_max_clients, int p_max_channels, int p_in_bandwidth, int p_out_bandwidth) { ERR_FAIL_COND_V_MSG(_is_active(), ERR_ALREADY_IN_USE, "The multiplayer instance is already active."); set_refuse_new_connections(false); @@ -114,6 +130,22 @@ Error ENetMultiplayerPeer::add_mesh_peer(int p_id, Ref<ENetConnection> p_host) { return OK; } +void ENetMultiplayerPeer::_store_packet(int32_t p_source, ENetConnection::Event &p_event) { + Packet packet; + packet.packet = p_event.packet; + packet.channel = p_event.channel_id; + packet.from = p_source; + if (p_event.packet->flags & ENET_PACKET_FLAG_RELIABLE) { + packet.transfer_mode = TRANSFER_MODE_RELIABLE; + } else if (p_event.packet->flags & ENET_PACKET_FLAG_UNSEQUENCED) { + packet.transfer_mode = TRANSFER_MODE_UNRELIABLE; + } else { + packet.transfer_mode = TRANSFER_MODE_UNRELIABLE_ORDERED; + } + packet.packet->referenceCount++; + incoming_packets.push_back(packet); +} + bool ENetMultiplayerPeer::_parse_server_event(ENetConnection::EventType p_type, ENetConnection::Event &p_event) { switch (p_type) { case ENetConnection::EVENT_CONNECT: { @@ -131,9 +163,6 @@ bool ENetMultiplayerPeer::_parse_server_event(ENetConnection::EventType p_type, peers[id] = p_event.peer; emit_signal(SNAME("peer_connected"), id); - if (server_relay) { - _notify_peers(id, true); - } return false; } case ENetConnection::EVENT_DISCONNECT: { @@ -145,50 +174,11 @@ bool ENetMultiplayerPeer::_parse_server_event(ENetConnection::EventType p_type, emit_signal(SNAME("peer_disconnected"), id); peers.erase(id); - if (server_relay) { - _notify_peers(id, false); - } return false; } case ENetConnection::EVENT_RECEIVE: { - if (p_event.channel_id == SYSCH_CONFIG) { - _destroy_unused(p_event.packet); - ERR_FAIL_V_MSG(false, "Only server can send config messages"); - } else { - if (p_event.packet->dataLength < 8) { - _destroy_unused(p_event.packet); - ERR_FAIL_V_MSG(false, "Invalid packet size"); - } - - uint32_t source = decode_uint32(&p_event.packet->data[0]); - int target = decode_uint32(&p_event.packet->data[4]); - - uint32_t id = p_event.peer->get_meta(SNAME("_net_id")); - // Someone is cheating and trying to fake the source! - if (source != id) { - _destroy_unused(p_event.packet); - ERR_FAIL_V_MSG(false, "Someone is cheating and trying to fake the source!"); - } - - Packet packet; - packet.packet = p_event.packet; - packet.channel = p_event.channel_id; - packet.from = id; - - // Even if relaying is disabled, these targets are valid as incoming packets. - if (target == 1 || target == 0 || target < -1) { - packet.packet->referenceCount++; - incoming_packets.push_back(packet); - } - - if (server_relay && target != 1) { - packet.packet->referenceCount++; - _relay(source, target, p_event.channel_id, p_event.packet); - packet.packet->referenceCount--; - _destroy_unused(p_event.packet); - } - // Destroy packet later - } + int32_t source = p_event.peer->get_meta(SNAME("_net_id")); + _store_packet(source, p_event); return false; } default: @@ -215,44 +205,7 @@ bool ENetMultiplayerPeer::_parse_client_event(ENetConnection::EventType p_type, return true; } case ENetConnection::EVENT_RECEIVE: { - if (p_event.channel_id == SYSCH_CONFIG) { - // Config message - if (p_event.packet->dataLength != 8) { - _destroy_unused(p_event.packet); - ERR_FAIL_V(false); - } - - int msg = decode_uint32(&p_event.packet->data[0]); - int id = decode_uint32(&p_event.packet->data[4]); - - switch (msg) { - case SYSMSG_ADD_PEER: { - peers[id] = Ref<ENetPacketPeer>(); - emit_signal(SNAME("peer_connected"), id); - - } break; - case SYSMSG_REMOVE_PEER: { - peers.erase(id); - emit_signal(SNAME("peer_disconnected"), id); - } break; - } - _destroy_unused(p_event.packet); - } else { - if (p_event.packet->dataLength < 8) { - _destroy_unused(p_event.packet); - ERR_FAIL_V_MSG(false, "Invalid packet size"); - } - - uint32_t source = decode_uint32(&p_event.packet->data[0]); - Packet packet; - packet.packet = p_event.packet; - packet.from = source; - packet.channel = p_event.channel_id; - - packet.packet->referenceCount++; - incoming_packets.push_back(packet); - // Destroy packet later - } + _store_packet(1, p_event); return false; } default: @@ -273,18 +226,7 @@ bool ENetMultiplayerPeer::_parse_mesh_event(ENetConnection::EventType p_type, EN hosts.erase(p_peer_id); return true; case ENetConnection::EVENT_RECEIVE: { - if (p_event.packet->dataLength < 8) { - _destroy_unused(p_event.packet); - ERR_FAIL_V_MSG(false, "Invalid packet size"); - } - - Packet packet; - packet.packet = p_event.packet; - packet.from = p_peer_id; - packet.channel = p_event.channel_id; - - packet.packet->referenceCount++; - incoming_packets.push_back(packet); + _store_packet(p_peer_id, p_event); return false; } break; default: @@ -376,6 +318,10 @@ bool ENetMultiplayerPeer::is_server() const { return active_mode == MODE_SERVER; } +bool ENetMultiplayerPeer::is_server_relay_supported() const { + return active_mode == MODE_SERVER || active_mode == MODE_CLIENT; +} + void ENetMultiplayerPeer::close_connection(uint32_t wait_usec) { if (!_is_active()) { return; @@ -422,8 +368,8 @@ Error ENetMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_si current_packet = incoming_packets.front()->get(); incoming_packets.pop_front(); - *r_buffer = (const uint8_t *)(¤t_packet.packet->data[8]); - r_buffer_size = current_packet.packet->dataLength - 8; + *r_buffer = (const uint8_t *)(current_packet.packet->data); + r_buffer_size = current_packet.packet->dataLength; return OK; } @@ -457,15 +403,13 @@ Error ENetMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size } #ifdef DEBUG_ENABLED - if ((packet_flags & ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT) && p_buffer_size + 8 > ENET_HOST_DEFAULT_MTU) { - WARN_PRINT_ONCE(vformat("Sending %d bytes unrealiably which is above the MTU (%d), this will result in higher packet loss", p_buffer_size + 8, ENET_HOST_DEFAULT_MTU)); + if ((packet_flags & ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT) && p_buffer_size > ENET_HOST_DEFAULT_MTU) { + WARN_PRINT_ONCE(vformat("Sending %d bytes unrealiably which is above the MTU (%d), this will result in higher packet loss", p_buffer_size, ENET_HOST_DEFAULT_MTU)); } #endif - ENetPacket *packet = enet_packet_create(nullptr, p_buffer_size + 8, packet_flags); - encode_uint32(unique_id, &packet->data[0]); // Source ID - encode_uint32(target_peer, &packet->data[4]); // Dest ID - memcpy(&packet->data[8], p_buffer, p_buffer_size); + ENetPacket *packet = enet_packet_create(nullptr, p_buffer_size, packet_flags); + memcpy(&packet->data[0], p_buffer, p_buffer_size); if (is_server()) { if (target_peer == 0) { @@ -548,16 +492,6 @@ void ENetMultiplayerPeer::set_refuse_new_connections(bool p_enabled) { MultiplayerPeer::set_refuse_new_connections(p_enabled); } -void ENetMultiplayerPeer::set_server_relay_enabled(bool p_enabled) { - ERR_FAIL_COND_MSG(_is_active(), "Server relaying can't be toggled while the multiplayer instance is active."); - - server_relay = p_enabled; -} - -bool ENetMultiplayerPeer::is_server_relay_enabled() const { - return server_relay; -} - Ref<ENetConnection> ENetMultiplayerPeer::get_host() const { ERR_FAIL_COND_V(!_is_active(), nullptr); ERR_FAIL_COND_V(active_mode == MODE_MESH, nullptr); @@ -577,70 +511,6 @@ void ENetMultiplayerPeer::_destroy_unused(ENetPacket *p_packet) { } } -void ENetMultiplayerPeer::_relay(int p_from, int p_to, enet_uint8 p_channel, ENetPacket *p_packet) { - if (p_to == 0) { - // Re-send to everyone but sender :| - for (KeyValue<int, Ref<ENetPacketPeer>> &E : peers) { - if (E.key == p_from) { - continue; - } - - E.value->send(p_channel, p_packet); - } - } else if (p_to < 0) { - // Re-send to everyone but excluded and sender. - for (KeyValue<int, Ref<ENetPacketPeer>> &E : peers) { - if (E.key == p_from || E.key == -p_to) { // Do not resend to self, also do not send to excluded - continue; - } - - E.value->send(p_channel, p_packet); - } - } else { - // To someone else, specifically - ERR_FAIL_COND(!peers.has(p_to)); - ENetPacket *packet = enet_packet_create(p_packet->data, p_packet->dataLength, p_packet->flags); - peers[p_to]->send(p_channel, packet); - } -} - -void ENetMultiplayerPeer::_notify_peers(int p_id, bool p_connected) { - if (p_connected) { - ERR_FAIL_COND(!peers.has(p_id)); - // Someone connected, notify all the peers available. - Ref<ENetPacketPeer> peer = peers[p_id]; - ENetPacket *packet = enet_packet_create(nullptr, 8, ENET_PACKET_FLAG_RELIABLE); - encode_uint32(SYSMSG_ADD_PEER, &packet->data[0]); - encode_uint32(p_id, &packet->data[4]); - for (KeyValue<int, Ref<ENetPacketPeer>> &E : peers) { - if (E.key == p_id) { - continue; - } - // Send new peer to existing peer. - E.value->send(SYSCH_CONFIG, packet); - // Send existing peer to new peer. - // This packet will be automatically destroyed by ENet after send. - ENetPacket *packet2 = enet_packet_create(nullptr, 8, ENET_PACKET_FLAG_RELIABLE); - encode_uint32(SYSMSG_ADD_PEER, &packet2->data[0]); - encode_uint32(E.key, &packet2->data[4]); - peer->send(SYSCH_CONFIG, packet2); - } - _destroy_unused(packet); - } else { - // Server just received a client disconnect and is in relay mode, notify everyone else. - ENetPacket *packet = enet_packet_create(nullptr, 8, ENET_PACKET_FLAG_RELIABLE); - encode_uint32(SYSMSG_REMOVE_PEER, &packet->data[0]); - encode_uint32(p_id, &packet->data[4]); - for (KeyValue<int, Ref<ENetPacketPeer>> &E : peers) { - if (E.key == p_id) { - continue; - } - E.value->send(SYSCH_CONFIG, packet); - } - _destroy_unused(packet); - } -} - void ENetMultiplayerPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("create_server", "port", "max_clients", "max_channels", "in_bandwidth", "out_bandwidth"), &ENetMultiplayerPeer::create_server, DEFVAL(32), DEFVAL(0), DEFVAL(0), DEFVAL(0)); ClassDB::bind_method(D_METHOD("create_client", "address", "port", "channel_count", "in_bandwidth", "out_bandwidth", "local_port"), &ENetMultiplayerPeer::create_client, DEFVAL(0), DEFVAL(0), DEFVAL(0), DEFVAL(0)); @@ -649,12 +519,9 @@ void ENetMultiplayerPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("close_connection", "wait_usec"), &ENetMultiplayerPeer::close_connection, DEFVAL(100)); ClassDB::bind_method(D_METHOD("set_bind_ip", "ip"), &ENetMultiplayerPeer::set_bind_ip); - ClassDB::bind_method(D_METHOD("set_server_relay_enabled", "enabled"), &ENetMultiplayerPeer::set_server_relay_enabled); - ClassDB::bind_method(D_METHOD("is_server_relay_enabled"), &ENetMultiplayerPeer::is_server_relay_enabled); ClassDB::bind_method(D_METHOD("get_host"), &ENetMultiplayerPeer::get_host); ClassDB::bind_method(D_METHOD("get_peer", "id"), &ENetMultiplayerPeer::get_peer); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "server_relay"), "set_server_relay_enabled", "is_server_relay_enabled"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "host", PROPERTY_HINT_RESOURCE_TYPE, "ENetConnection", PROPERTY_USAGE_NONE), "", "get_host"); } diff --git a/modules/enet/enet_multiplayer_peer.h b/modules/enet/enet_multiplayer_peer.h index 3152068d46..85692e8eee 100644 --- a/modules/enet/enet_multiplayer_peer.h +++ b/modules/enet/enet_multiplayer_peer.h @@ -47,10 +47,9 @@ private: }; enum { - SYSCH_CONFIG = 0, - SYSCH_RELIABLE = 1, - SYSCH_UNRELIABLE = 2, - SYSCH_MAX = 3 + SYSCH_RELIABLE = 0, + SYSCH_UNRELIABLE = 1, + SYSCH_MAX = 2 }; enum Mode { @@ -66,8 +65,6 @@ private: int target_peer = 0; - bool server_relay = true; - ConnectionStatus connection_status = CONNECTION_DISCONNECTED; HashMap<int, Ref<ENetConnection>> hosts; @@ -77,18 +74,18 @@ private: ENetPacket *packet = nullptr; int from = 0; int channel = 0; + TransferMode transfer_mode = TRANSFER_MODE_RELIABLE; }; List<Packet> incoming_packets; Packet current_packet; + void _store_packet(int32_t p_source, ENetConnection::Event &p_event); void _pop_current_packet(); bool _parse_server_event(ENetConnection::EventType p_event_type, ENetConnection::Event &p_event); bool _parse_client_event(ENetConnection::EventType p_event_type, ENetConnection::Event &p_event); bool _parse_mesh_event(ENetConnection::EventType p_event_type, ENetConnection::Event &p_event, int p_peer_id); - void _relay(int p_from, int p_to, enet_uint8 p_channel, ENetPacket *p_packet); - void _notify_peers(int p_id, bool p_connected); void _destroy_unused(ENetPacket *p_packet); _FORCE_INLINE_ bool _is_active() const { return active_mode != MODE_NONE; } @@ -99,10 +96,15 @@ protected: public: virtual void set_target_peer(int p_peer) override; + virtual int get_packet_peer() const override; + virtual TransferMode get_packet_mode() const override; + virtual int get_packet_channel() const override; virtual void poll() override; virtual bool is_server() const override; + virtual bool is_server_relay_supported() const override; + // Overridden so we can instrument the DTLSServer when needed. virtual void set_refuse_new_connections(bool p_enabled) override; @@ -125,8 +127,6 @@ public: void disconnect_peer(int p_peer, bool now = false); void set_bind_ip(const IPAddress &p_ip); - void set_server_relay_enabled(bool p_enabled); - bool is_server_relay_enabled() const; Ref<ENetConnection> get_host() const; Ref<ENetPacketPeer> get_peer(int p_id) const; diff --git a/modules/etcpak/image_compress_etcpak.cpp b/modules/etcpak/image_compress_etcpak.cpp index 7d5557d197..3d66b27556 100644 --- a/modules/etcpak/image_compress_etcpak.cpp +++ b/modules/etcpak/image_compress_etcpak.cpp @@ -233,7 +233,7 @@ void _compress_etcpak(EtcpakType p_compresstype, Image *r_img, float p_lossy_qua } // Replace original image with compressed one. - r_img->create(width, height, mipmaps, target_format, dest_data); + r_img->set_data(width, height, mipmaps, target_format, dest_data); print_verbose(vformat("ETCPAK encode took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time))); } diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index bc44479f93..c8eda53a2d 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -18,13 +18,11 @@ <param index="2" name="b8" type="int" /> <param index="3" name="a8" type="int" default="255" /> <description> - Returns a color constructed from integer red, green, blue, and alpha channels. Each channel should have 8 bits of information ranging from 0 to 255. - [code]r8[/code] red channel - [code]g8[/code] green channel - [code]b8[/code] blue channel - [code]a8[/code] alpha channel + Returns a [Color] constructed from red ([param r8]), green ([param g8]), blue ([param b8]), and optionally alpha ([param a8]) integer channels, each divided by [code]255.0[/code] for their final value. [codeblock] - red = Color8(255, 0, 0) + var red = Color8(255, 0, 0) # Same as Color(1, 0, 0) + var dark_blue = Color8(0, 0, 51) # Same as Color(0, 0, 0.2). + var my_color = Color8(306, 255, 0, 102) # Same as Color(1.2, 1, 0, 0.4). [/codeblock] </description> </method> @@ -33,9 +31,9 @@ <param index="0" name="condition" type="bool" /> <param index="1" name="message" type="String" default="""" /> <description> - Asserts that the [code]condition[/code] is [code]true[/code]. If the [code]condition[/code] is [code]false[/code], an error is generated. When running from the editor, the running project will also be paused until you resume it. This can be used as a stronger form of [method @GlobalScope.push_error] for reporting errors to project developers or add-on users. - [b]Note:[/b] For performance reasons, the code inside [method assert] is only executed in debug builds or when running the project from the editor. Don't include code that has side effects in an [method assert] call. Otherwise, the project will behave differently when exported in release mode. - The optional [code]message[/code] argument, if given, is shown in addition to the generic "Assertion failed" message. It must be a static string, so format strings can't be used. You can use this to provide additional details about why the assertion failed. + Asserts that the [param condition] is [code]true[/code]. If the [param condition] is [code]false[/code], an error is generated. When running from the editor, the running project will also be paused until you resume it. This can be used as a stronger form of [method @GlobalScope.push_error] for reporting errors to project developers or add-on users. + An optional [param message] can be shown in addition to the generic "Assertion failed" message. You can use this to provide additional details about why the assertion failed. + [b]Warning:[/b] For performance reasons, the code inside [method assert] is only executed in debug builds or when running the project from the editor. Don't include code that has side effects in an [method assert] call. Otherwise, the project will behave differently when exported in release mode. [codeblock] # Imagine we always want speed to be between 0 and 20. var speed = -10 @@ -50,7 +48,7 @@ <return type="String" /> <param index="0" name="char" type="int" /> <description> - Returns a character as a String of the given Unicode code point (which is compatible with ASCII code). + Returns a single character (as a [String]) of the given Unicode code point (which is compatible with ASCII code). [codeblock] a = char(65) # a is "A" a = char(65 + 32) # a is "a" @@ -63,14 +61,14 @@ <param index="0" name="what" type="Variant" /> <param index="1" name="type" type="int" /> <description> - Converts from a type to another in the best way possible. The [code]type[/code] parameter uses the [enum Variant.Type] values. + Converts [param what] to [param type] in the best way possible. The [param type] uses the [enum Variant.Type] values. [codeblock] - a = Vector2(1, 0) - # Prints 1 - print(a.length()) - a = convert(a, TYPE_STRING) - # Prints 6 as "(1, 0)" is 6 characters - print(a.length()) + var a = [4, 2.5, 1.2] + print(a is Array) # Prints true + + var b = convert(a, TYPE_PACKED_BYTE_ARRAY) + print(b) # Prints [4, 2, 1] + print(b is Array) # Prints false [/codeblock] </description> </method> @@ -78,7 +76,7 @@ <return type="Object" /> <param index="0" name="dictionary" type="Dictionary" /> <description> - Converts a [param dictionary] (previously created with [method inst_to_dict]) back to an Object instance. Useful for deserializing. + Converts a [param dictionary] (created with [method inst_to_dict]) back to an Object instance. Can be useful for deserializing. </description> </method> <method name="get_stack"> @@ -95,19 +93,19 @@ func bar(): print(get_stack()) [/codeblock] - would print + Starting from [code]_ready()[/code], [code]bar()[/code] would print: [codeblock] [{function:bar, line:12, source:res://script.gd}, {function:foo, line:9, source:res://script.gd}, {function:_ready, line:6, source:res://script.gd}] [/codeblock] - [b]Note:[/b] [method get_stack] only works if the running instance is connected to a debugging server (i.e. an editor instance). [method get_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. - [b]Note:[/b] Not supported for calling from threads. Instead, this will return an empty array. + [b]Note:[/b] This function only works if the running instance is connected to a debugging server (i.e. an editor instance). [method get_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. + [b]Note:[/b] Calling this function from a [Thread] is not supported. Doing so will return an empty array. </description> </method> <method name="inst_to_dict"> <return type="Dictionary" /> <param index="0" name="instance" type="Object" /> <description> - Returns the passed [param instance] converted to a Dictionary (useful for serializing). + Returns the passed [param instance] converted to a Dictionary. Can be useful for serializing. [codeblock] var foo = "bar" func _ready(): @@ -126,11 +124,13 @@ <return type="int" /> <param index="0" name="var" type="Variant" /> <description> - Returns length of Variant [code]var[/code]. Length is the character count of String, element count of Array, size of Dictionary, etc. - [b]Note:[/b] Generates a fatal error if Variant can not provide a length. + Returns the length of the given Variant [param var]. The length can be the character count of a [String], the element count of any array type or the size of a [Dictionary]. For every other Variant type, a run-time error is generated and execution is stopped. [codeblock] a = [1, 2, 3, 4] len(a) # Returns 4 + + b = "Hello!" + len(b) # Returns 6 [/codeblock] </description> </method> @@ -138,25 +138,25 @@ <return type="Resource" /> <param index="0" name="path" type="String" /> <description> - Loads a resource from the filesystem located at [code]path[/code]. The resource is loaded on the method call (unless it's referenced already elsewhere, e.g. in another script or in the scene), which might cause slight delay, especially when loading scenes. To avoid unnecessary delays when loading something multiple times, either store the resource in a variable or use [method preload]. + Returns a [Resource] from the filesystem located at the absolute [param path]. Unless it's already referenced elsewhere (such as in another script or in the scene), the resource is loaded from disk on function call, which might cause a slight delay, especially when loading large scenes. To avoid unnecessary delays when loading something multiple times, either store the resource in a variable or use [method preload]. [b]Note:[/b] Resource paths can be obtained by right-clicking on a resource in the FileSystem dock and choosing "Copy Path" or by dragging the file from the FileSystem dock into the script. [codeblock] - # Load a scene called main located in the root of the project directory and cache it in a variable. + # Load a scene called "main" located in the root of the project directory and cache it in a variable. var main = load("res://main.tscn") # main will contain a PackedScene resource. [/codeblock] - [b]Important:[/b] The path must be absolute, a local path will just return [code]null[/code]. - This method is a simplified version of [method ResourceLoader.load], which can be used for more advanced scenarios. - [b]Note:[/b] You have to import the files into the engine first to load them using [method load]. If you want to load [Image]s at run-time, you may use [method Image.load]. If you want to import audio files, you can use the snippet described in [member AudioStreamMP3.data]. + [b]Important:[/b] The path must be absolute. A relative path will always return [code]null[/code]. + This function is a simplified version of [method ResourceLoader.load], which can be used for more advanced scenarios. + [b]Note:[/b] Files have to be imported into the engine first to load them using this function. If you want to load [Image]s at run-time, you may use [method Image.load]. If you want to import audio files, you can use the snippet described in [member AudioStreamMP3.data]. </description> </method> <method name="preload"> <return type="Resource" /> <param index="0" name="path" type="String" /> <description> - Returns a [Resource] from the filesystem located at [code]path[/code]. The resource is loaded during script parsing, i.e. is loaded with the script and [method preload] effectively acts as a reference to that resource. Note that the method requires a constant path. If you want to load a resource from a dynamic/variable path, use [method load]. + Returns a [Resource] from the filesystem located at [param path]. During run-time, the resource is loaded when the script is being parsed. This function effectively acts as a reference to that resource. Note that this function requires [param path] to be a constant [String]. If you want to load a resource from a dynamic/variable path, use [method load]. [b]Note:[/b] Resource paths can be obtained by right clicking on a resource in the Assets Panel and choosing "Copy Path" or by dragging the file from the FileSystem dock into the script. [codeblock] - # Instance a scene. + # Create instance of a scene. var diamond = preload("res://diamond.tscn").instantiate() [/codeblock] </description> @@ -165,24 +165,24 @@ <return type="void" /> <description> Like [method @GlobalScope.print], but includes the current stack frame when running with the debugger turned on. - Output in the console would look something like this: + The output in the console may look like the following: [codeblock] Test print - At: res://test.gd:15:_process() + At: res://test.gd:15:_process() [/codeblock] - [b]Note:[/b] Not supported for calling from threads. Instead of the stack frame, this will print the thread ID. + [b]Note:[/b] Calling this function from a [Thread] is not supported. Doing so will instead print the thread ID. </description> </method> <method name="print_stack"> <return type="void" /> <description> Prints a stack trace at the current code location. See also [method get_stack]. - Output in the console would look something like this: + The output in the console may look like the following: [codeblock] Frame 0 - res://test.gd:16 in function '_process' [/codeblock] - [b]Note:[/b] [method print_stack] only works if the running instance is connected to a debugging server (i.e. an editor instance). [method print_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. - [b]Note:[/b] Not supported for calling from threads. Instead of the stack trace, this will print the thread ID. + [b]Note:[/b] This function only works if the running instance is connected to a debugging server (i.e. an editor instance). [method print_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. + [b]Note:[/b] Calling this function from a [Thread] is not supported. Doing so will instead print the thread ID. </description> </method> <method name="range" qualifiers="vararg"> @@ -229,7 +229,7 @@ <method name="str" qualifiers="vararg"> <return type="String" /> <description> - Converts one or more arguments to string in the best way possible. + Converts one or more arguments to a [String] in the best way possible. [codeblock] var a = [10, 20, 30] var b = str(a); @@ -242,7 +242,7 @@ <return type="bool" /> <param index="0" name="type" type="StringName" /> <description> - Returns whether the given [Object]-derived class exists in [ClassDB]. Note that [Variant] data types are not registered in [ClassDB]. + Returns [code]true[/code] if the given [Object]-derived class exists in [ClassDB]. Note that [Variant] data types are not registered in [ClassDB]. [codeblock] type_exists("Sprite2D") # Returns true type_exists("NonExistentClass") # Returns false @@ -259,11 +259,11 @@ </constant> <constant name="INF" value="inf"> Positive floating-point infinity. This is the result of floating-point division when the divisor is [code]0.0[/code]. For negative infinity, use [code]-INF[/code]. Dividing by [code]-0.0[/code] will result in negative infinity if the numerator is positive, so dividing by [code]0.0[/code] is not the same as dividing by [code]-0.0[/code] (despite [code]0.0 == -0.0[/code] returning [code]true[/code]). - [b]Note:[/b] Numeric infinity is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer number by [code]0[/code] will not result in [constant INF] and will result in a run-time error instead. + [b]Warning:[/b] Numeric infinity is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer number by [code]0[/code] will not result in [constant INF] and will result in a run-time error instead. </constant> <constant name="NAN" value="nan"> "Not a Number", an invalid floating-point value. [constant NAN] has special properties, including that it is not equal to itself ([code]NAN == NAN[/code] returns [code]false[/code]). It is output by some invalid operations, such as dividing floating-point [code]0.0[/code] by [code]0.0[/code]. - [b]Note:[/b] "Not a Number" is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer [code]0[/code] by [code]0[/code] will not result in [constant NAN] and will result in a run-time error instead. + [b]Warning:[/b] "Not a Number" is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer [code]0[/code] by [code]0[/code] will not result in [constant NAN] and will result in a run-time error instead. </constant> </constants> <annotations> @@ -294,7 +294,7 @@ <annotation name="@export_color_no_alpha"> <return type="void" /> <description> - Export a [Color] property without an alpha (fixed as [code]1.0[/code]). + Export a [Color] property without transparency (its alpha fixed as [code]1.0[/code]). See also [constant PROPERTY_HINT_COLOR_NO_ALPHA]. [codeblock] @export_color_no_alpha var modulate_color: Color @@ -320,7 +320,7 @@ [codeblock] @export_enum("Rebecca", "Mary", "Leah") var character_name: String @export_enum("Warrior", "Magician", "Thief") var character_class: int - @export_enum("Walking:30", "Running:60", "Riding:200") var character_speed: int + @export_enum("Slow:30", "Average:60", "Very Fast:200") var character_speed: int [/codeblock] </description> </annotation> @@ -451,7 +451,7 @@ <description> Define a new group for the following exported properties. This helps to organize properties in the Inspector dock. Groups can be added with an optional [param prefix], which would make group to only consider properties that have this prefix. The grouping will break on the first property that doesn't have a prefix. The prefix is also removed from the property's name in the Inspector dock. If no [param prefix] is provided, the every following property is added to the group. The group ends when then next group or category is defined. You can also force end a group by using this annotation with empty strings for parameters, [code]@export_group("", "")[/code]. - Groups cannot be nested, use [annotation @export_subgroup] to add subgroups to your groups. + Groups cannot be nested, use [annotation @export_subgroup] to add subgroups within groups. See also [constant PROPERTY_USAGE_GROUP]. [codeblock] @export_group("My Properties") @@ -473,7 +473,7 @@ Export a [String] property with a large [TextEdit] widget instead of a [LineEdit]. This adds support for multiline content and makes it easier to edit large amount of text stored in the property. See also [constant PROPERTY_HINT_MULTILINE_TEXT]. [codeblock] - @export_multiline var character_bio + @export_multiline var character_biography [/codeblock] </description> </annotation> @@ -547,11 +547,11 @@ <return type="void" /> <param index="0" name="icon_path" type="String" /> <description> - Add a custom icon to the current script. The icon is displayed in the Scene dock for every node that the script is attached to. For named classes the icon is also displayed in various editor dialogs. + Add a custom icon to the current script. After loading an icon at [param icon_path], the icon is displayed in the Scene dock for every node that the script is attached to. For named classes, the icon is also displayed in various editor dialogs. [codeblock] @icon("res://path/to/class/icon.svg") [/codeblock] - [b]Note:[/b] Only the script can have a custom icon. Inner classes are not supported yet. + [b]Note:[/b] Only the script can have a custom icon. Inner classes are not supported. </description> </annotation> <annotation name="@onready"> @@ -590,7 +590,7 @@ <return type="void" /> <param index="0" name="warning" type="String" /> <description> - Mark the following statement to ignore the specified warning. See [url=$DOCS_URL/tutorials/scripting/gdscript/warning_system.html]GDScript warning system[/url]. + Mark the following statement to ignore the specified [param warning]. See [url=$DOCS_URL/tutorials/scripting/gdscript/warning_system.html]GDScript warning system[/url]. [codeblock] func test(): print("hello") diff --git a/modules/gdscript/doc_classes/GDScript.xml b/modules/gdscript/doc_classes/GDScript.xml index 578e7a34f3..8246c96c15 100644 --- a/modules/gdscript/doc_classes/GDScript.xml +++ b/modules/gdscript/doc_classes/GDScript.xml @@ -4,7 +4,7 @@ A script implemented in the GDScript programming language. </brief_description> <description> - A script implemented in the GDScript programming language. The script extends the functionality of all objects that instance it. + A script implemented in the GDScript programming language. The script extends the functionality of all objects that instantiate it. [method new] creates a new instance of the script. [method Object.set_script] extends an existing object, if that object's class matches one of the script's base classes. </description> <tutorials> diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index 8645aa6f15..996d323a7f 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -256,7 +256,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } String word = str.substr(from + 1, to - from); // Keywords need to be exceptions, except for keywords that represent a value. - if (word == "true" || word == "false" || word == "null" || word == "PI" || word == "TAU" || word == "INF" || word == "NAN" || word == "self" || word == "super" || !keywords.has(word)) { + if (word == "true" || word == "false" || word == "null" || word == "PI" || word == "TAU" || word == "INF" || word == "NAN" || word == "self" || word == "super" || !reserved_keywords.has(word)) { if (!is_symbol(str[to]) || str[to] == '"' || str[to] == '\'' || str[to] == ')' || str[to] == ']' || str[to] == '}') { is_binary_op = true; } @@ -338,8 +338,10 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l col = global_function_color; } } - } else if (keywords.has(word)) { - col = keywords[word]; + } else if (class_names.has(word)) { + col = class_names[word]; + } else if (reserved_keywords.has(word)) { + col = reserved_keywords[word]; } else if (member_keywords.has(word)) { col = member_keywords[word]; } @@ -563,7 +565,8 @@ PackedStringArray GDScriptSyntaxHighlighter::_get_supported_languages() const { } void GDScriptSyntaxHighlighter::_update_cache() { - keywords.clear(); + class_names.clear(); + reserved_keywords.clear(); member_keywords.clear(); global_functions.clear(); color_regions.clear(); @@ -580,7 +583,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { List<StringName> types; ClassDB::get_class_list(&types); for (const StringName &E : types) { - keywords[E] = types_color; + class_names[E] = types_color; } /* User types. */ @@ -588,14 +591,14 @@ void GDScriptSyntaxHighlighter::_update_cache() { List<StringName> global_classes; ScriptServer::get_global_class_list(&global_classes); for (const StringName &E : global_classes) { - keywords[E] = usertype_color; + class_names[E] = usertype_color; } /* Autoloads. */ for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) { const ProjectSettings::AutoloadInfo &info = E.value; if (info.is_singleton) { - keywords[info.name] = usertype_color; + class_names[info.name] = usertype_color; } } @@ -606,7 +609,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { List<String> core_types; gdscript->get_core_type_words(&core_types); for (const String &E : core_types) { - keywords[StringName(E)] = basetype_color; + class_names[StringName(E)] = basetype_color; } /* Reserved words. */ @@ -616,9 +619,9 @@ void GDScriptSyntaxHighlighter::_update_cache() { gdscript->get_reserved_words(&keyword_list); for (const String &E : keyword_list) { if (gdscript->is_control_flow_keyword(E)) { - keywords[StringName(E)] = control_flow_keyword_color; + reserved_keywords[StringName(E)] = control_flow_keyword_color; } else { - keywords[StringName(E)] = keyword_color; + reserved_keywords[StringName(E)] = keyword_color; } } diff --git a/modules/gdscript/editor/gdscript_highlighter.h b/modules/gdscript/editor/gdscript_highlighter.h index 60b5b092d4..7c22eb30b1 100644 --- a/modules/gdscript/editor/gdscript_highlighter.h +++ b/modules/gdscript/editor/gdscript_highlighter.h @@ -47,7 +47,8 @@ private: Vector<ColorRegion> color_regions; HashMap<int, int> color_region_cache; - HashMap<StringName, Color> keywords; + HashMap<StringName, Color> class_names; + HashMap<StringName, Color> reserved_keywords; HashMap<StringName, Color> member_keywords; HashSet<StringName> global_functions; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 0a9dad04c7..b4da9c1224 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -1050,6 +1050,11 @@ Error GDScript::load_byte_code(const String &p_path) { return ERR_COMPILATION_FAILED; } +void GDScript::set_path(const String &p_path, bool p_take_over) { + Script::set_path(p_path, p_take_over); + this->path = p_path; +} + Error GDScript::load_source_code(const String &p_path) { Vector<uint8_t> sourcef; Error err; diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 0a010e5ad0..72ad890fbc 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -222,6 +222,7 @@ public: virtual Error reload(bool p_keep_state = false) override; + virtual void set_path(const String &p_path, bool p_take_over = false) override; void set_script_path(const String &p_path) { path = p_path; } //because subclasses need a path too... Error load_source_code(const String &p_path); Error load_byte_code(const String &p_path); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index e1beb2f374..1401e4b94b 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1041,7 +1041,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class) { } } -void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node) { +void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root) { ERR_FAIL_COND_MSG(p_node == nullptr, "Trying to resolve type of a null node."); switch (p_node->type) { @@ -1114,7 +1114,7 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node) { case GDScriptParser::Node::SUBSCRIPT: case GDScriptParser::Node::TERNARY_OPERATOR: case GDScriptParser::Node::UNARY_OPERATOR: - reduce_expression(static_cast<GDScriptParser::ExpressionNode *>(p_node), true); + reduce_expression(static_cast<GDScriptParser::ExpressionNode *>(p_node), p_is_root); break; case GDScriptParser::Node::BREAK: case GDScriptParser::Node::BREAKPOINT: @@ -1411,7 +1411,7 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { variable_type.builtin_type = Variant::INT; // Can this ever be a float or something else? p_for->variable->set_datatype(variable_type); } else if (p_for->list) { - resolve_node(p_for->list); + resolve_node(p_for->list, false); if (p_for->list->datatype.has_container_element_type()) { variable_type = p_for->list->datatype.get_container_element_type(); variable_type.type_source = GDScriptParser::DataType::ANNOTATED_INFERRED; @@ -1439,7 +1439,7 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { } void GDScriptAnalyzer::resolve_while(GDScriptParser::WhileNode *p_while) { - resolve_node(p_while->condition); + resolve_node(p_while->condition, false); resolve_suite(p_while->loop); p_while->set_datatype(p_while->loop->get_datatype()); @@ -1602,8 +1602,8 @@ void GDScriptAnalyzer::resolve_assert(GDScriptParser::AssertNode *p_assert) { reduce_expression(p_assert->condition); if (p_assert->message != nullptr) { reduce_expression(p_assert->message); - if (!p_assert->message->is_constant || p_assert->message->reduced_value.get_type() != Variant::STRING) { - push_error(R"(Expected constant string for assert error message.)", p_assert->message); + if (!p_assert->message->get_datatype().has_no_type() && (p_assert->message->get_datatype().kind != GDScriptParser::DataType::BUILTIN || p_assert->message->get_datatype().builtin_type != Variant::STRING)) { + push_error(R"(Expected string for assert error message.)", p_assert->message); } } @@ -1824,7 +1824,7 @@ void GDScriptAnalyzer::reduce_expression(GDScriptParser::ExpressionNode *p_expre reduce_binary_op(static_cast<GDScriptParser::BinaryOpNode *>(p_expression)); break; case GDScriptParser::Node::CALL: - reduce_call(static_cast<GDScriptParser::CallNode *>(p_expression), p_is_root); + reduce_call(static_cast<GDScriptParser::CallNode *>(p_expression), false, p_is_root); break; case GDScriptParser::Node::CAST: reduce_cast(static_cast<GDScriptParser::CastNode *>(p_expression)); @@ -2425,9 +2425,15 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a switch (err.error) { case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: { - PropertyInfo wrong_arg = function_info.arguments[err.argument]; + String expected_type_name; + if (err.argument < function_info.arguments.size()) { + expected_type_name = type_from_property(function_info.arguments[err.argument]).to_string(); + } else { + expected_type_name = Variant::get_type_name((Variant::Type)err.expected); + } + push_error(vformat(R"*(Invalid argument for "%s()" function: argument %d should be %s but is %s.)*", function_name, err.argument + 1, - type_from_property(wrong_arg).to_string(), p_call->arguments[err.argument]->get_datatype().to_string()), + expected_type_name, p_call->arguments[err.argument]->get_datatype().to_string()), p_call->arguments[err.argument]); } break; case Callable::CallError::CALL_ERROR_INVALID_METHOD: @@ -2548,6 +2554,16 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a mark_lambda_use_self(); } +#ifdef DEBUG_ENABLED + if (p_is_root && return_type.kind != GDScriptParser::DataType::UNRESOLVED && return_type.builtin_type != Variant::NIL) { + parser->push_warning(p_call, GDScriptWarning::RETURN_VALUE_DISCARDED, p_call->function_name); + } + + if (is_static && !base_type.is_meta_type && !(callee_type != GDScriptParser::Node::SUBSCRIPT && parser->current_function != nullptr && parser->current_function->is_static)) { + parser->push_warning(p_call, GDScriptWarning::STATIC_CALLED_ON_INSTANCE, p_call->function_name, base_type.to_string()); + } +#endif // DEBUG_ENABLED + call_type = return_type; } else { bool found = false; @@ -3216,7 +3232,13 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { } p_preload->resolved_path = p_preload->resolved_path.simplify_path(); if (!ResourceLoader::exists(p_preload->resolved_path)) { - push_error(vformat(R"(Preload file "%s" does not exist.)", p_preload->resolved_path), p_preload->path); + Ref<FileAccess> file_check = FileAccess::create(FileAccess::ACCESS_RESOURCES); + + if (file_check->file_exists(p_preload->resolved_path)) { + push_error(vformat(R"(Preload file "%s" has no resource loaders (unrecognized file extension).)", p_preload->resolved_path), p_preload->path); + } else { + push_error(vformat(R"(Preload file "%s" does not exist.)", p_preload->resolved_path), p_preload->path); + } } else { // TODO: Don't load if validating: use completion cache. p_preload->resource = ResourceLoader::load(p_preload->resolved_path); diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index 3966b81b6e..217a856ce0 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -63,7 +63,7 @@ class GDScriptAnalyzer { void resolve_class_body(GDScriptParser::ClassNode *p_class); void resolve_function_signature(GDScriptParser::FunctionNode *p_function); void resolve_function_body(GDScriptParser::FunctionNode *p_function); - void resolve_node(GDScriptParser::Node *p_node); + void resolve_node(GDScriptParser::Node *p_node, bool p_is_root = true); void resolve_suite(GDScriptParser::SuiteNode *p_suite); void resolve_if(GDScriptParser::IfNode *p_if); void resolve_for(GDScriptParser::ForNode *p_for); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index bdf6fb35b6..4279edf394 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3765,13 +3765,19 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node break; case GDScriptParser::DataType::CLASS: // Can assume type is a global GDScript class. - if (!ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) { - push_error(R"(Exported script type must extend Resource.)"); + if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) { + variable->export_info.type = Variant::OBJECT; + variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; + variable->export_info.hint_string = export_type.class_type->identifier->name; + } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) { + variable->export_info.type = Variant::OBJECT; + variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; + variable->export_info.hint_string = export_type.class_type->identifier->name; + } else { + push_error(R"(Export type can only be built-in, a resource, a node or an enum.)", variable); return false; } - variable->export_info.type = Variant::OBJECT; - variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; - variable->export_info.hint_string = export_type.class_type->identifier->name; + break; case GDScriptParser::DataType::SCRIPT: { StringName class_name; diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 1cae7bdfac..a0c107aa53 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -155,6 +155,10 @@ String GDScriptWarning::get_message() const { case INT_ASSIGNED_TO_ENUM: { return "Integer used when an enum value is expected. If this is intended cast the integer to the enum type."; } + case STATIC_CALLED_ON_INSTANCE: { + CHECK_SYMBOLS(2); + return vformat(R"(The function '%s()' is a static function but was called from an instance. Instead, it should be directly called from the type: '%s.%s()'.)", symbols[0], symbols[1], symbols[0]); + } case WARNING_MAX: break; // Can't happen, but silences warning } @@ -215,6 +219,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "EMPTY_FILE", "SHADOWED_GLOBAL_IDENTIFIER", "INT_ASSIGNED_TO_ENUM", + "STATIC_CALLED_ON_INSTANCE", }; static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index a639e7b44e..7e4e975510 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -78,6 +78,7 @@ public: EMPTY_FILE, // A script file is empty. SHADOWED_GLOBAL_IDENTIFIER, // A global class or function has the same name as variable. INT_ASSIGNED_TO_ENUM, // An integer value was assigned to an enum-typed variable without casting. + STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. WARNING_MAX, }; diff --git a/modules/gdscript/tests/scripts/parser/features/class.gd b/modules/gdscript/tests/scripts/parser/features/class.gd index 6652f85ad9..af24b32322 100644 --- a/modules/gdscript/tests/scripts/parser/features/class.gd +++ b/modules/gdscript/tests/scripts/parser/features/class.gd @@ -21,5 +21,5 @@ func test(): assert(test_sub.number == 25) # From Test. assert(test_sub.other_string == "bye") # From TestSub. - TestConstructor.new() - TestConstructor.new(500) + var _test_constructor = TestConstructor.new() + _test_constructor = TestConstructor.new(500) diff --git a/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out b/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out index d73c5eb7cd..13f759dd46 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out +++ b/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out @@ -1 +1,5 @@ GDTEST_OK +>> WARNING +>> Line: 6 +>> RETURN_VALUE_DISCARDED +>> The function 'i_return_int()' returns a value, but this value is never used. diff --git a/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.gd b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.gd new file mode 100644 index 0000000000..29d8501b78 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.gd @@ -0,0 +1,11 @@ +class Player: + var x = 3 + +func test(): + # These should not emit a warning. + var _player = Player.new() + print(String.num_uint64(8589934592)) # 2 ^ 33 + + # This should emit a warning. + var some_string = String() + print(some_string.num_uint64(8589934592)) # 2 ^ 33 diff --git a/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out new file mode 100644 index 0000000000..3933a35178 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out @@ -0,0 +1,7 @@ +GDTEST_OK +>> WARNING +>> Line: 11 +>> STATIC_CALLED_ON_INSTANCE +>> The function 'num_uint64()' is a static function but was called from an instance. Instead, it should be directly called from the type: 'String.num_uint64()'. +8589934592 +8589934592 diff --git a/modules/glslang/register_types.cpp b/modules/glslang/register_types.cpp index b1c2140039..53ec0fde34 100644 --- a/modules/glslang/register_types.cpp +++ b/modules/glslang/register_types.cpp @@ -53,7 +53,6 @@ static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage }; int ClientInputSemanticsVersion = 100; // maps to, say, #define VULKAN 100 - bool check_subgroup_support = true; // assume we support subgroups glslang::EShTargetClientVersion ClientVersion = glslang::EShTargetVulkan_1_2; glslang::EShTargetLanguageVersion TargetVersion = glslang::EShTargetSpv_1_5; @@ -63,7 +62,6 @@ static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage if (capabilities->version_major == 1 && capabilities->version_minor == 0) { ClientVersion = glslang::EShTargetVulkan_1_0; TargetVersion = glslang::EShTargetSpv_1_0; - check_subgroup_support = false; // subgroups are not supported in Vulkan 1.0 } else if (capabilities->version_major == 1 && capabilities->version_minor == 1) { ClientVersion = glslang::EShTargetVulkan_1_1; TargetVersion = glslang::EShTargetSpv_1_3; @@ -88,7 +86,7 @@ static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage shader.setEnvClient(glslang::EShClientVulkan, ClientVersion); shader.setEnvTarget(glslang::EShTargetSpv, TargetVersion); - if (check_subgroup_support) { + { uint32_t stage_bit = 1 << p_stage; uint32_t subgroup_in_shaders = uint32_t(p_render_device->limit_get(RD::LIMIT_SUBGROUP_IN_SHADERS)); diff --git a/modules/gltf/doc_classes/GLTFNode.xml b/modules/gltf/doc_classes/GLTFNode.xml index 4d1aa89ac9..8e48066623 100644 --- a/modules/gltf/doc_classes/GLTFNode.xml +++ b/modules/gltf/doc_classes/GLTFNode.xml @@ -9,6 +9,25 @@ <tutorials> <link title="GLTF scene and node spec">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_004_ScenesNodes.md"</link> </tutorials> + <methods> + <method name="get_additional_data"> + <return type="Variant" /> + <param index="0" name="extension_name" type="StringName" /> + <description> + Gets additional arbitrary data in this [GLTFNode] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the return value can be anything you set. If nothing was set, the return value is null. + </description> + </method> + <method name="set_additional_data"> + <return type="void" /> + <param index="0" name="extension_name" type="StringName" /> + <param index="1" name="additional_data" type="Variant" /> + <description> + Sets additional arbitrary data in this [GLTFNode] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The first argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the second argument can be anything you want. + </description> + </method> + </methods> <members> <member name="camera" type="int" setter="set_camera" getter="get_camera" default="-1"> </member> diff --git a/modules/gltf/doc_classes/GLTFState.xml b/modules/gltf/doc_classes/GLTFState.xml index 56f3a70631..d0740cf7ca 100644 --- a/modules/gltf/doc_classes/GLTFState.xml +++ b/modules/gltf/doc_classes/GLTFState.xml @@ -20,6 +20,14 @@ <description> </description> </method> + <method name="get_additional_data"> + <return type="Variant" /> + <param index="0" name="extension_name" type="StringName" /> + <description> + Gets additional arbitrary data in this [GLTFState] instance. This can be used to keep per-file state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the return value can be anything you set. If nothing was set, the return value is null. + </description> + </method> <method name="get_animation_player"> <return type="AnimationPlayer" /> <param index="0" name="idx" type="int" /> @@ -120,6 +128,15 @@ <description> </description> </method> + <method name="set_additional_data"> + <return type="void" /> + <param index="0" name="extension_name" type="StringName" /> + <param index="1" name="additional_data" type="Variant" /> + <description> + Sets additional arbitrary data in this [GLTFState] instance. This can be used to keep per-file state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The first argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the second argument can be anything you want. + </description> + </method> <method name="set_animations"> <return type="void" /> <param index="0" name="animations" type="GLTFAnimation[]" /> diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 6700b6de0a..cb148463a7 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -3484,7 +3484,7 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> state) { height = albedo_texture->get_height(); width = albedo_texture->get_width(); } - orm_image->create(width, height, false, Image::FORMAT_RGBA8); + orm_image->initialize_data(width, height, false, Image::FORMAT_RGBA8); if (ao_image.is_valid() && ao_image->get_size() != Vector2(width, height)) { ao_image->resize(width, height, Image::INTERPOLATE_LANCZOS); } @@ -3860,13 +3860,11 @@ void GLTFDocument::spec_gloss_to_rough_metal(Ref<GLTFSpecGloss> r_spec_gloss, Re if (r_spec_gloss->diffuse_img.is_null()) { return; } - Ref<Image> rm_img; - rm_img.instantiate(); bool has_roughness = false; bool has_metal = false; p_material->set_roughness(1.0f); p_material->set_metallic(1.0f); - rm_img->create(r_spec_gloss->spec_gloss_img->get_width(), r_spec_gloss->spec_gloss_img->get_height(), false, Image::FORMAT_RGBA8); + Ref<Image> rm_img = Image::create_empty(r_spec_gloss->spec_gloss_img->get_width(), r_spec_gloss->spec_gloss_img->get_height(), false, Image::FORMAT_RGBA8); r_spec_gloss->spec_gloss_img->decompress(); if (r_spec_gloss->diffuse_img.is_valid()) { r_spec_gloss->diffuse_img->decompress(); diff --git a/modules/gltf/gltf_state.cpp b/modules/gltf/gltf_state.cpp index 60192c67e6..ac5665e396 100644 --- a/modules/gltf/gltf_state.cpp +++ b/modules/gltf/gltf_state.cpp @@ -87,6 +87,8 @@ void GLTFState::_bind_methods() { ClassDB::bind_method(D_METHOD("get_animations"), &GLTFState::get_animations); ClassDB::bind_method(D_METHOD("set_animations", "animations"), &GLTFState::set_animations); ClassDB::bind_method(D_METHOD("get_scene_node", "idx"), &GLTFState::get_scene_node); + ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFState::get_additional_data); + ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFState::set_additional_data); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "json"), "set_json", "get_json"); // Dictionary ADD_PROPERTY(PropertyInfo(Variant::INT, "major_version"), "set_major_version", "get_major_version"); // int @@ -358,3 +360,11 @@ String GLTFState::get_base_path() { void GLTFState::set_base_path(String p_base_path) { base_path = p_base_path; } + +Variant GLTFState::get_additional_data(const StringName &p_extension_name) { + return additional_data[p_extension_name]; +} + +void GLTFState::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { + additional_data[p_extension_name] = p_additional_data; +} diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index afe7e82010..e24017b0fd 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -97,6 +97,7 @@ class GLTFState : public Resource { HashMap<ObjectID, GLTFSkeletonIndex> skeleton3d_to_gltf_skeleton; HashMap<ObjectID, HashMap<ObjectID, GLTFSkinIndex>> skin_and_skeleton3d_to_gltf_skin; + Dictionary additional_data; protected: static void _bind_methods(); @@ -191,6 +192,9 @@ public: AnimationPlayer *get_animation_player(int idx); + Variant get_additional_data(const StringName &p_extension_name); + void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); + //void set_scene_nodes(RBMap<GLTFNodeIndex, Node *> p_scene_nodes) { // this->scene_nodes = p_scene_nodes; //} diff --git a/modules/gltf/structures/gltf_node.cpp b/modules/gltf/structures/gltf_node.cpp index 86280603fa..6fd36f93b7 100644 --- a/modules/gltf/structures/gltf_node.cpp +++ b/modules/gltf/structures/gltf_node.cpp @@ -57,6 +57,8 @@ void GLTFNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_children", "children"), &GLTFNode::set_children); ClassDB::bind_method(D_METHOD("get_light"), &GLTFNode::get_light); ClassDB::bind_method(D_METHOD("set_light", "light"), &GLTFNode::set_light); + ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFNode::get_additional_data); + ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFNode::set_additional_data); ADD_PROPERTY(PropertyInfo(Variant::INT, "parent"), "set_parent", "get_parent"); // GLTFNodeIndex ADD_PROPERTY(PropertyInfo(Variant::INT, "height"), "set_height", "get_height"); // int @@ -176,3 +178,11 @@ GLTFLightIndex GLTFNode::get_light() { void GLTFNode::set_light(GLTFLightIndex p_light) { light = p_light; } + +Variant GLTFNode::get_additional_data(const StringName &p_extension_name) { + return additional_data[p_extension_name]; +} + +void GLTFNode::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { + additional_data[p_extension_name] = p_additional_data; +} diff --git a/modules/gltf/structures/gltf_node.h b/modules/gltf/structures/gltf_node.h index 1a57ea32e2..90a4fa99ed 100644 --- a/modules/gltf/structures/gltf_node.h +++ b/modules/gltf/structures/gltf_node.h @@ -53,6 +53,7 @@ private: Vector3 scale = Vector3(1, 1, 1); Vector<int> children; GLTFLightIndex light = -1; + Dictionary additional_data; protected: static void _bind_methods(); @@ -96,6 +97,9 @@ public: GLTFLightIndex get_light(); void set_light(GLTFLightIndex p_light); + + Variant get_additional_data(const StringName &p_extension_name); + void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); }; #endif // GLTF_NODE_H diff --git a/modules/hdr/image_loader_hdr.cpp b/modules/hdr/image_loader_hdr.cpp index 6f0bc16a26..457b263e16 100644 --- a/modules/hdr/image_loader_hdr.cpp +++ b/modules/hdr/image_loader_hdr.cpp @@ -140,7 +140,7 @@ Error ImageLoaderHDR::load_image(Ref<Image> p_image, Ref<FileAccess> f, BitField } } - p_image->create(width, height, false, Image::FORMAT_RGBE9995, imgdata); + p_image->set_data(width, height, false, Image::FORMAT_RGBE9995, imgdata); return OK; } diff --git a/modules/jpg/image_loader_jpegd.cpp b/modules/jpg/image_loader_jpegd.cpp index ce20ac9060..9bbe467352 100644 --- a/modules/jpg/image_loader_jpegd.cpp +++ b/modules/jpg/image_loader_jpegd.cpp @@ -33,8 +33,8 @@ #include "core/os/os.h" #include "core/string/print_string.h" -#include "thirdparty/jpeg-compressor/jpgd.h" -#include "thirdparty/jpeg-compressor/jpge.h" +#include <jpgd.h> +#include <jpge.h> #include <string.h> Error jpeg_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p_buffer_len) { @@ -99,7 +99,7 @@ Error jpeg_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p fmt = Image::FORMAT_RGB8; } - p_image->create(image_width, image_height, false, fmt, data); + p_image->set_data(image_width, image_height, false, fmt, data); return OK; } @@ -132,10 +132,6 @@ static Ref<Image> _jpegd_mem_loader_func(const uint8_t *p_png, int p_size) { return img; } -static Error _jpgd_save_func(const String &p_path, const Ref<Image> &p_img, float p_quality) { - return OK; -} - class ImageLoaderJPGOSFile : public jpge::output_stream { public: Ref<FileAccess> f; @@ -157,21 +153,18 @@ public: } }; -static Vector<uint8_t> _jpgd_buffer_save_func(const Ref<Image> &p_img, float p_quality) { - ERR_FAIL_COND_V(p_img.is_null() || p_img->is_empty(), Vector<uint8_t>()); +static Error _jpgd_save_to_output_stream(jpge::output_stream *p_output_stream, const Ref<Image> &p_img, float p_quality) { + ERR_FAIL_COND_V(p_img.is_null() || p_img->is_empty(), ERR_INVALID_PARAMETER); Ref<Image> image = p_img; if (image->get_format() != Image::FORMAT_RGB8) { - image->convert(Image::FORMAT_ETC2_RGB8); + image->convert(Image::FORMAT_RGB8); } jpge::params p; p.m_quality = CLAMP(p_quality * 100, 1, 100); - Vector<uint8_t> output; - ImageLoaderJPGOSBuffer ob; - ob.buffer = &output; jpge::jpeg_encoder enc; - enc.init(&ob, image->get_width(), image->get_height(), 3, p); + enc.init(p_output_stream, image->get_width(), image->get_height(), 3, p); const uint8_t *src_data = image->get_data().ptr(); for (int i = 0; i < image->get_height(); i++) { @@ -180,9 +173,28 @@ static Vector<uint8_t> _jpgd_buffer_save_func(const Ref<Image> &p_img, float p_q enc.process_scanline(nullptr); + return OK; +} + +static Vector<uint8_t> _jpgd_buffer_save_func(const Ref<Image> &p_img, float p_quality) { + Vector<uint8_t> output; + ImageLoaderJPGOSBuffer ob; + ob.buffer = &output; + if (_jpgd_save_to_output_stream(&ob, p_img, p_quality) != OK) { + return Vector<uint8_t>(); + } return output; } +static Error _jpgd_save_func(const String &p_path, const Ref<Image> &p_img, float p_quality) { + Error err; + Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err); + ERR_FAIL_COND_V_MSG(err, err, vformat("Can't save JPG at path: '%s'.", p_path)); + ImageLoaderJPGOSFile ob; + ob.f = file; + return _jpgd_save_to_output_stream(&ob, p_img, p_quality); +} + ImageLoaderJPG::ImageLoaderJPG() { Image::_jpg_mem_loader_func = _jpegd_mem_loader_func; Image::save_jpg_func = _jpgd_save_func; diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index 8785f327db..9111827c1b 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -251,15 +251,11 @@ Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_ } for (int i = 0; i < atlas_slices; i++) { - Ref<Image> albedo; - albedo.instantiate(); - albedo->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBA8); + Ref<Image> albedo = Image::create_empty(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBA8); albedo->set_as_black(); albedo_images.write[i] = albedo; - Ref<Image> emission; - emission.instantiate(); - emission->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH); + Ref<Image> emission = Image::create_empty(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH); emission->set_as_black(); emission_images.write[i] = emission; } @@ -478,9 +474,7 @@ void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i grid_usage.write[j] = count > 0 ? 255 : 0; } - Ref<Image> img; - img.instantiate(); - img->create(grid_size, grid_size, false, Image::FORMAT_L8, grid_usage); + Ref<Image> img = Image::create_from_data(grid_size, grid_size, false, Image::FORMAT_L8, grid_usage); img->save_png("res://grid_layer_" + itos(1000 + i).substr(1, 3) + ".png"); } #endif @@ -660,9 +654,7 @@ LightmapperRD::BakeError LightmapperRD::_dilate(RenderingDevice *rd, Ref<RDShade #ifdef DEBUG_TEXTURES for (int i = 0; i < atlas_slices; i++) { Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->convert(Image::FORMAT_RGBA8); img->save_png("res://5_dilated_" + itos(i) + ".png"); } @@ -778,7 +770,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d panorama_tex->convert(Image::FORMAT_RGBAF); } else { panorama_tex.instantiate(); - panorama_tex->create(8, 8, false, Image::FORMAT_RGBAF); + panorama_tex->initialize_data(8, 8, false, Image::FORMAT_RGBAF); panorama_tex->fill(Color(0, 0, 0, 1)); } @@ -953,13 +945,11 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d for (int i = 0; i < atlas_slices; i++) { Vector<uint8_t> s = rd->texture_get_data(position_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAF, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAF, s); img->save_exr("res://1_position_" + itos(i) + ".exr", false); s = rd->texture_get_data(normal_tex, i); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + img->set_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->save_exr("res://1_normal_" + itos(i) + ".exr", false); } #endif @@ -1182,9 +1172,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d for (int i = 0; i < atlas_slices; i++) { Vector<uint8_t> s = rd->texture_get_data(light_source_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->save_exr("res://2_light_primary_" + itos(i) + ".exr", false); } #endif @@ -1415,14 +1403,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d #if 0 for (int i = 0; i < probe_positions.size(); i++) { - Ref<Image> img; - img.instantiate(); - img->create(6, 4, false, Image::FORMAT_RGB8); + Ref<Image> img = Image::create_empty(6, 4, false, Image::FORMAT_RGB8); for (int j = 0; j < 6; j++) { Vector<uint8_t> s = rd->texture_get_data(lightprobe_tex, i * 6 + j); - Ref<Image> img2; - img2.instantiate(); - img2->create(2, 2, false, Image::FORMAT_RGBAF, s); + Ref<Image> img2 = Image::create_from_data(2, 2, false, Image::FORMAT_RGBAF, s); img2->convert(Image::FORMAT_RGB8); img->blit_rect(img2, Rect2i(0, 0, 2, 2), Point2i((j % 3) * 2, (j / 3) * 2)); } @@ -1449,9 +1433,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d if (denoiser.is_valid()) { for (int i = 0; i < atlas_slices * (p_bake_sh ? 4 : 1); i++) { Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); Ref<Image> denoised = denoiser->denoise_image(img); if (denoised != img) { @@ -1484,9 +1466,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d for (int i = 0; i < atlas_slices * (p_bake_sh ? 4 : 1); i++) { Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->save_exr("res://4_light_secondary_" + itos(i) + ".exr", false); } #endif @@ -1640,9 +1620,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d for (int i = 0; i < atlas_slices * (p_bake_sh ? 4 : 1); i++) { Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->save_exr("res://5_blendseams" + itos(i) + ".exr", false); } #endif @@ -1652,9 +1630,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d for (int i = 0; i < atlas_slices * (p_bake_sh ? 4 : 1); i++) { Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); - Ref<Image> img; - img.instantiate(); - img->create(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); + Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->convert(Image::FORMAT_RGBH); //remove alpha bake_textures.push_back(img); } @@ -1667,9 +1643,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d #ifdef DEBUG_TEXTURES { - Ref<Image> img2; - img2.instantiate(); - img2->create(probe_values.size(), 1, false, Image::FORMAT_RGBAF, probe_data); + Ref<Image> img2 = Image::create_from_data(probe_values.size(), 1, false, Image::FORMAT_RGBAF, probe_data); img2->save_exr("res://6_lightprobes.exr", false); } #endif diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs index 7ec3f88e5d..19fdd51dab 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs @@ -56,7 +56,7 @@ namespace GodotPlugins.Game } "; - context.AddSource("GodotPlugins.Game_Generated", + context.AddSource("GodotPlugins.Game.generated", SourceText.From(source, Encoding.UTF8)); } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index 1ee31eb1a9..d5d80df643 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -87,7 +87,7 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptMethods_Generated"; + + "_ScriptMethods.generated"; var source = new StringBuilder(); @@ -188,14 +188,14 @@ namespace Godot.SourceGenerators if (godotClassMethods.Length > 0) { source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, "); - source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n {\n"); + source.Append("NativeVariantPtrArgs args, out godot_variant ret)\n {\n"); foreach (var method in godotClassMethods) { GenerateMethodInvoker(method, source); } - source.Append(" return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n"); + source.Append(" return base.InvokeGodotClassMethod(method, args, out ret);\n"); source.Append(" }\n"); } @@ -364,7 +364,7 @@ namespace Godot.SourceGenerators source.Append(" if (method == MethodName."); source.Append(methodName); - source.Append(" && argCount == "); + source.Append(" && args.Count == "); source.Append(method.ParamTypes.Length); source.Append(") {\n"); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index e8a9e28d0c..ccfb405d26 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -96,8 +96,8 @@ namespace Godot.SourceGenerators string.Empty; bool hasNamespace = classNs.Length != 0; - var uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptPath_Generated"; + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptPath.generated"; var source = new StringBuilder(); @@ -126,7 +126,7 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueHint.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); } private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context, @@ -157,7 +157,7 @@ namespace Godot.SourceGenerators sourceBuilder.Append("})]\n"); - context.AddSource("AssemblyScriptTypes_Generated", + context.AddSource("AssemblyScriptTypes.generated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index b331e2e794..1198c633d9 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -73,7 +73,7 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptProperties_Generated"; + + "_ScriptProperties.generated"; var source = new StringBuilder(); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs index 65dadcb801..98b9745c16 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -73,7 +73,7 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptPropertyDefVal_Generated"; + + "_ScriptPropertyDefVal.generated"; var source = new StringBuilder(); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs index a40220565f..11e0a6fa21 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs @@ -73,7 +73,7 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptSerialization_Generated"; + + "_ScriptSerialization.generated"; var source = new StringBuilder(); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index ea7cc3fe38..50196b84f0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -82,7 +82,7 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() - + "_ScriptSignals_Generated"; + + "_ScriptSignals.generated"; var source = new StringBuilder(); @@ -167,6 +167,7 @@ namespace Godot.SourceGenerators Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol); } } + continue; } @@ -257,14 +258,14 @@ namespace Godot.SourceGenerators { source.Append( " protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, "); - source.Append("NativeVariantPtrArgs args, int argCount)\n {\n"); + source.Append("NativeVariantPtrArgs args)\n {\n"); foreach (var signal in godotSignalDelegates) { GenerateSignalEventInvoker(signal, source); } - source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n"); + source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args);\n"); source.Append(" }\n"); } @@ -404,7 +405,7 @@ namespace Godot.SourceGenerators source.Append(" if (signal == SignalName."); source.Append(signalName); - source.Append(" && argCount == "); + source.Append(" && args.Count == "); source.Append(invokeMethodData.ParamTypes.Length); source.Append(") {\n"); source.Append(" backing_"); diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 0d2bea2363..745a8b73f8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -17,6 +17,8 @@ namespace GodotTools.Export { public partial class ExportPlugin : EditorExportPlugin { + private List<string> _tempFolders = new List<string>(); + public void RegisterExportSettings() { // TODO: These would be better as export preset options, but that doesn't seem to be supported yet @@ -111,62 +113,78 @@ namespace GodotTools.Export string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; - // TODO: This works for now, as we only implemented support for x86 family desktop so far, but it needs to be fixed - string arch = features.Contains("x86_64") ? "x86_64" : "x86"; - - string ridOS = DetermineRuntimeIdentifierOS(platform); - string ridArch = DetermineRuntimeIdentifierArch(arch); - string runtimeIdentifier = $"{ridOS}-{ridArch}"; - - // Create temporary publish output directory - - string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", - $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); - - if (!Directory.Exists(publishOutputTempDir)) - Directory.CreateDirectory(publishOutputTempDir); - - // Execute dotnet publish - - if (!BuildManager.PublishProjectBlocking(buildConfig, platform, - runtimeIdentifier, publishOutputTempDir)) + var archs = new List<string>(); + if (features.Contains("x86_64")) { - throw new InvalidOperationException("Failed to build project."); + archs.Add("x86_64"); } - - string soExt = ridOS switch + else if (features.Contains("x86_32")) { - OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", - OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", - _ => "so" - }; - - if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll")) - // NativeAOT shared library output - && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"))) + archs.Add("x86_32"); + } + else if (features.Contains("arm64")) { - throw new NotSupportedException( - "Publish succeeded but project assembly not found in the output directory"); + archs.Add("arm64"); } - - // Copy all files from the dotnet publish output directory to - // a data directory next to the Godot output executable. - - string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject()); - - if (Directory.Exists(outputDataDir)) - Directory.Delete(outputDataDir, recursive: true); // Clean first - - Directory.CreateDirectory(outputDataDir); - - foreach (string dir in Directory.GetDirectories(publishOutputTempDir, "*", SearchOption.AllDirectories)) + else if (features.Contains("universal")) { - Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(publishOutputTempDir.Length + 1))); + if (platform == OS.Platforms.MacOS) + { + archs.Add("x86_64"); + archs.Add("arm64"); + } } - foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) + foreach (var arch in archs) { - File.Copy(file, Path.Combine(outputDataDir, file.Substring(publishOutputTempDir.Length + 1))); + string ridOS = DetermineRuntimeIdentifierOS(platform); + string ridArch = DetermineRuntimeIdentifierArch(arch); + string runtimeIdentifier = $"{ridOS}-{ridArch}"; + string projectDataDirName = $"{DetermineDataDirNameForProject()}_{arch}"; + if (platform == OS.Platforms.MacOS) + { + projectDataDirName = Path.Combine("Contents", "Resources", projectDataDirName); + } + + // Create temporary publish output directory + + string publishOutputTempDir = Path.Combine(Path.GetTempPath(), "godot-publish-dotnet", + $"{Process.GetCurrentProcess().Id}-{buildConfig}-{runtimeIdentifier}"); + + _tempFolders.Add(publishOutputTempDir); + + if (!Directory.Exists(publishOutputTempDir)) + Directory.CreateDirectory(publishOutputTempDir); + + // Execute dotnet publish + + if (!BuildManager.PublishProjectBlocking(buildConfig, platform, + runtimeIdentifier, publishOutputTempDir)) + { + throw new InvalidOperationException("Failed to build project."); + } + + string soExt = ridOS switch + { + OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", + OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", + _ => "so" + }; + + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.dll")) + // NativeAOT shared library output + && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpDirs.ProjectAssemblyName}.{soExt}"))) + { + throw new NotSupportedException( + "Publish succeeded but project assembly not found in the output directory"); + } + + // Add to the exported project shared object list. + + foreach (string file in Directory.GetFiles(publishOutputTempDir, "*", SearchOption.AllDirectories)) + { + AddSharedObject(file, tags: null, projectDataDirName); + } } } @@ -198,6 +216,12 @@ namespace GodotTools.Export if (Directory.Exists(aotTempDir)) Directory.Delete(aotTempDir, recursive: true); + foreach (string folder in _tempFolders) + { + Directory.Delete(folder, recursive: true); + } + _tempFolders.Clear(); + // TODO: The following is just a workaround until the export plugins can be made to abort with errors // We check for empty as well, because it's set to empty after hot-reloading diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index d29e0d69ab..b90321b586 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -1614,7 +1614,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output << MEMBER_BEGIN "protected internal " << (is_derived_type ? "override" : "virtual") << " bool " CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(in godot_string_name method, " - << "NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n" + << "NativeVariantPtrArgs args, out godot_variant ret)\n" << INDENT1 "{\n"; for (const MethodInterface &imethod : itype.methods) { @@ -1630,7 +1630,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // We check both native names (snake_case) and proxy names (PascalCase) output << INDENT2 "if ((method == " << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << " || method == MethodName." << imethod.proxy_name - << ") && argCount == " << itos(imethod.arguments.size()) + << ") && args.Count == " << itos(imethod.arguments.size()) << " && " << CS_METHOD_HAS_GODOT_CLASS_METHOD << "((godot_string_name)" << CS_STATIC_FIELD_METHOD_PROXY_NAME_PREFIX << imethod.name << ".NativeValue))\n" << INDENT2 "{\n"; @@ -1682,7 +1682,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } if (is_derived_type) { - output << INDENT2 "return base." CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(method, args, argCount, out ret);\n"; + output << INDENT2 "return base." CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(method, args, out ret);\n"; } else { output << INDENT2 "ret = default;\n" << INDENT2 "return false;\n"; @@ -2200,6 +2200,11 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::SignalInterface &p_isignal, StringBuilder &p_output) { String arguments_sig; + String delegate_type_params; + + if (!p_isignal.arguments.is_empty()) { + delegate_type_params += "<"; + } // Retrieve information from the arguments const ArgumentInterface &first = p_isignal.arguments.front()->get(); @@ -2220,11 +2225,18 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf if (&iarg != &first) { arguments_sig += ", "; + delegate_type_params += ", "; } arguments_sig += arg_type->cs_type; arguments_sig += " "; arguments_sig += iarg.name; + + delegate_type_params += arg_type->cs_type; + } + + if (!p_isignal.arguments.is_empty()) { + delegate_type_params += ">"; } // Generate signal @@ -2248,15 +2260,46 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("\")]"); } - String delegate_name = p_isignal.proxy_name; - delegate_name += "EventHandler"; // Delegate name is [SignalName]EventHandler + bool is_parameterless = p_isignal.arguments.size() == 0; - // Generate delegate - p_output.append(MEMBER_BEGIN "public delegate void "); - p_output.append(delegate_name); - p_output.append("("); - p_output.append(arguments_sig); - p_output.append(");\n"); + // Delegate name is [SignalName]EventHandler + String delegate_name = is_parameterless ? "Action" : p_isignal.proxy_name + "EventHandler"; + + if (!is_parameterless) { + // Generate delegate + p_output.append(MEMBER_BEGIN "public delegate void "); + p_output.append(delegate_name); + p_output.append("("); + p_output.append(arguments_sig); + p_output.append(");\n"); + + // Generate Callable trampoline for the delegate + p_output << MEMBER_BEGIN "private static unsafe void " << p_isignal.proxy_name << "Trampoline" + << "(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret)\n" + << INDENT1 "{\n" + << INDENT2 "Callable.ThrowIfArgCountMismatch(args, " << itos(p_isignal.arguments.size()) << ");\n" + << INDENT2 "((" << delegate_name << ")delegateObj)("; + + int idx = 0; + for (const ArgumentInterface &iarg : p_isignal.arguments) { + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found + + if (idx != 0) { + p_output << ","; + } + + // TODO: We don't need to use VariantConversionCallbacks. We have the type information so we can use [cs_variant_to_managed] and [cs_managed_to_variant]. + p_output << "\n" INDENT3 "VariantConversionCallbacks.GetToManagedCallback<" + << arg_type->cs_type << ">()(args[" << itos(idx) << "])"; + + idx++; + } + + p_output << ");\n" + << INDENT2 "ret = default;\n" + << INDENT1 "}\n"; + } if (p_isignal.method_doc && p_isignal.method_doc->description.size()) { String xml_summary = bbcode_to_xml(fix_doc_description(p_isignal.method_doc->description), &p_itype); @@ -2292,6 +2335,11 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("static "); } + if (!is_parameterless) { + // `unsafe` is needed for taking the trampoline's function pointer + p_output << "unsafe "; + } + p_output.append("event "); p_output.append(delegate_name); p_output.append(" "); @@ -2304,8 +2352,13 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append("add => Connect(SignalName."); } - p_output.append(p_isignal.proxy_name); - p_output.append(", new Callable(value));\n"); + if (is_parameterless) { + // Delegate type is Action. No need for custom trampoline. + p_output << p_isignal.proxy_name << ", Callable.From(value));\n"; + } else { + p_output << p_isignal.proxy_name + << ", Callable.CreateWithUnsafeTrampoline(value, &" << p_isignal.proxy_name << "Trampoline));\n"; + } if (p_itype.is_singleton) { p_output.append(INDENT2 "remove => " CS_PROPERTY_SINGLETON ".Disconnect(SignalName."); @@ -2313,8 +2366,14 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(INDENT2 "remove => Disconnect(SignalName."); } - p_output.append(p_isignal.proxy_name); - p_output.append(", new Callable(value));\n"); + if (is_parameterless) { + // Delegate type is Action. No need for custom trampoline. + p_output << p_isignal.proxy_name << ", Callable.From(value));\n"; + } else { + p_output << p_isignal.proxy_name + << ", Callable.CreateWithUnsafeTrampoline(value, &" << p_isignal.proxy_name << "Trampoline));\n"; + } + p_output.append(CLOSE_BLOCK_L1); } @@ -3119,9 +3178,10 @@ bool BindingsGenerator::_populate_object_type_interfaces() { for (const KeyValue<StringName, ClassDB::ClassInfo::EnumInfo> &E : enum_map) { StringName enum_proxy_cname = E.key; String enum_proxy_name = enum_proxy_cname.operator String(); - if (itype.find_property_by_proxy_name(enum_proxy_cname)) { - // We have several conflicts between enums and PascalCase properties, - // so we append 'Enum' to the enum name in those cases. + if (itype.find_property_by_proxy_name(enum_proxy_name) || itype.find_method_by_proxy_name(enum_proxy_name) || itype.find_signal_by_proxy_name(enum_proxy_name)) { + // In case the enum name conflicts with other PascalCase members, + // we append 'Enum' to the enum name in those cases. + // We have several conflicts between enums and PascalCase properties. enum_proxy_name += "Enum"; enum_proxy_cname = StringName(enum_proxy_name); } @@ -3170,7 +3230,15 @@ bool BindingsGenerator::_populate_object_type_interfaces() { int64_t *value = class_info->constant_map.getptr(StringName(constant_name)); ERR_FAIL_NULL_V(value, false); - ConstantInterface iconstant(constant_name, snake_to_pascal_case(constant_name, true), *value); + String constant_proxy_name = snake_to_pascal_case(constant_name, true); + + if (itype.find_property_by_proxy_name(constant_proxy_name) || itype.find_method_by_proxy_name(constant_proxy_name) || itype.find_signal_by_proxy_name(constant_proxy_name)) { + // In case the constant name conflicts with other PascalCase members, + // we append 'Constant' to the constant name in those cases. + constant_proxy_name += "Constant"; + } + + ConstantInterface iconstant(constant_name, constant_proxy_name, *value); iconstant.const_doc = nullptr; for (int i = 0; i < itype.class_doc->constants.size(); i++) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index 1c98dfcdf6..f1b46e293b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -489,25 +489,37 @@ namespace Godot.Collections ICollection<T>, IEnumerable<T> { + private static godot_variant ToVariantFunc(in Array<T> godotArray) => + VariantUtils.CreateFromArray(godotArray); + + private static Array<T> FromVariantFunc(in godot_variant variant) => + VariantUtils.ConvertToArrayObject<T>(variant); + // ReSharper disable StaticMemberInGenericType // Warning is about unique static fields being created for each generic type combination: // https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html // In our case this is exactly what we want. - private static unsafe delegate* managed<in T, godot_variant> _convertToVariantCallback; - private static unsafe delegate* managed<in godot_variant, T> _convertToManagedCallback; + private static readonly unsafe delegate* managed<in T, godot_variant> ConvertToVariantCallback; + private static readonly unsafe delegate* managed<in godot_variant, T> ConvertToManagedCallback; // ReSharper restore StaticMemberInGenericType static unsafe Array() { - _convertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<T>(); - _convertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<T>(); + VariantConversionCallbacks.GenericConversionCallbacks[typeof(Array<T>)] = + ( + (IntPtr)(delegate* managed<in Array<T>, godot_variant>)&ToVariantFunc, + (IntPtr)(delegate* managed<in godot_variant, Array<T>>)&FromVariantFunc + ); + + ConvertToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<T>(); + ConvertToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<T>(); } private static unsafe void ValidateVariantConversionCallbacks() { - if (_convertToVariantCallback == null || _convertToManagedCallback == null) + if (ConvertToVariantCallback == null || ConvertToManagedCallback == null) { throw new InvalidOperationException( $"The array element type is not supported for conversion to Variant: '{typeof(T).FullName}'."); @@ -653,7 +665,7 @@ namespace Godot.Collections get { _underlyingArray.GetVariantBorrowElementAt(index, out godot_variant borrowElem); - return _convertToManagedCallback(borrowElem); + return ConvertToManagedCallback(borrowElem); } set { @@ -663,7 +675,7 @@ namespace Godot.Collections godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self); godot_variant* itemPtr = &ptrw[index]; (*itemPtr).Dispose(); - *itemPtr = _convertToVariantCallback(value); + *itemPtr = ConvertToVariantCallback(value); } } @@ -675,7 +687,7 @@ namespace Godot.Collections /// <returns>The index of the item, or -1 if not found.</returns> public unsafe int IndexOf(T item) { - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; return NativeFuncs.godotsharp_array_index_of(ref self, variantValue); } @@ -693,7 +705,7 @@ namespace Godot.Collections if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; NativeFuncs.godotsharp_array_insert(ref self, index, variantValue); } @@ -726,7 +738,7 @@ namespace Godot.Collections /// <returns>The new size after adding the item.</returns> public unsafe void Add(T item) { - using var variantValue = _convertToVariantCallback(item); + using var variantValue = ConvertToVariantCallback(item); var self = (godot_array)_underlyingArray.NativeValue; _ = NativeFuncs.godotsharp_array_add(ref self, variantValue); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index ae44f8f4ba..354212da1b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -22,8 +22,7 @@ namespace Godot.Bridge } bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), - new NativeVariantPtrArgs(args), - argCount, out godot_variant retValue); + new NativeVariantPtrArgs(args, argCount), out godot_variant retValue); if (!methodInvoked) { @@ -102,7 +101,7 @@ namespace Godot.Bridge return godot_bool.False; } - *outRet = Marshaling.ConvertManagedObjectToVariant(ret); + *outRet = ret.CopyNativeVariant(); return godot_bool.True; } catch (Exception e) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index 57240624bc..44ea8fc83d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -9,7 +9,7 @@ namespace Godot.Bridge { // @formatter:off public delegate* unmanaged<IntPtr, godot_variant**, int, godot_bool*, void> SignalAwaiter_SignalCallback; - public delegate* unmanaged<IntPtr, godot_variant**, uint, godot_variant*, void> DelegateUtils_InvokeWithVariantArgs; + public delegate* unmanaged<IntPtr, void*, godot_variant**, int, godot_variant*, void> DelegateUtils_InvokeWithVariantArgs; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> DelegateUtils_DelegateEquals; public delegate* unmanaged<IntPtr, godot_array*, godot_bool> DelegateUtils_TrySerializeDelegateWithGCHandle; public delegate* unmanaged<godot_array*, IntPtr*, godot_bool> DelegateUtils_TryDeserializeDelegateWithGCHandle; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 092724a6b1..d83cf43eb2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -339,7 +339,7 @@ namespace Godot.Bridge *outOwnerIsNull = godot_bool.False; owner.RaiseGodotClassSignalCallbacks(CustomUnsafe.AsRef(eventSignalName), - new NativeVariantPtrArgs(args), argCount); + new NativeVariantPtrArgs(args, argCount)); } catch (Exception e) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs index bdedd2e87a..f9309ca13e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.cs @@ -26,11 +26,12 @@ namespace Godot /// } /// </code> /// </example> - public readonly struct Callable + public readonly partial struct Callable { private readonly Object _target; private readonly StringName _method; private readonly Delegate _delegate; + private readonly unsafe delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void> _trampoline; /// <summary> /// Object that contains the method. @@ -48,10 +49,10 @@ namespace Godot public Delegate Delegate => _delegate; /// <summary> - /// Converts a <see cref="Delegate"/> to a <see cref="Callable"/>. + /// Trampoline function pointer for dynamically invoking <see cref="Callable.Delegate"/>. /// </summary> - /// <param name="delegate">The delegate to convert.</param> - public static implicit operator Callable(Delegate @delegate) => new Callable(@delegate); + public unsafe delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void> Trampoline + => _trampoline; /// <summary> /// Constructs a new <see cref="Callable"/> for the method called <paramref name="method"/> @@ -59,22 +60,21 @@ namespace Godot /// </summary> /// <param name="target">Object that contains the method.</param> /// <param name="method">Name of the method that will be called.</param> - public Callable(Object target, StringName method) + public unsafe Callable(Object target, StringName method) { _target = target; _method = method; _delegate = null; + _trampoline = null; } - /// <summary> - /// Constructs a new <see cref="Callable"/> for the given <paramref name="delegate"/>. - /// </summary> - /// <param name="delegate">Delegate method that will be called.</param> - public Callable(Delegate @delegate) + private unsafe Callable(Delegate @delegate, + delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void> trampoline) { _target = @delegate?.Target as Object; _method = null; _delegate = @delegate; + _trampoline = trampoline; } private const int VarArgsSpanThreshold = 5; @@ -149,5 +149,59 @@ namespace Godot NativeFuncs.godotsharp_callable_call_deferred(callable, (godot_variant**)argsPtr, argc); } } + + /// <summary> + /// <para> + /// Constructs a new <see cref="Callable"/> using the <paramref name="trampoline"/> + /// function pointer to dynamically invoke the given <paramref name="delegate"/>. + /// </para> + /// <para> + /// The parameters passed to the <paramref name="trampoline"/> function are: + /// </para> + /// <list type="number"> + /// <item> + /// <term>delegateObj</term> + /// <description>The given <paramref name="delegate"/>, upcast to <see cref="object"/>.</description> + /// </item> + /// <item> + /// <term>args</term> + /// <description>Array of <see cref="godot_variant"/> arguments.</description> + /// </item> + /// <item> + /// <term>ret</term> + /// <description>Return value of type <see cref="godot_variant"/>.</description> + /// </item> + ///</list> + /// <para> + /// The delegate should be downcast to a more specific delegate type before invoking. + /// </para> + /// </summary> + /// <example> + /// Usage example: + /// + /// <code> + /// static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + /// { + /// if (args.Count != 1) + /// throw new ArgumentException($"Callable expected {1} arguments but received {args.Count}."); + /// + /// TResult res = ((Func<int, string>)delegateObj)( + /// VariantConversionCallbacks.GetToManagedCallback<int>()(args[0]) + /// ); + /// + /// ret = VariantConversionCallbacks.GetToVariantCallback<string>()(res); + /// } + /// + /// var callable = Callable.CreateWithUnsafeTrampoline((int num) => "foo" + num.ToString(), &Trampoline); + /// var res = (string)callable.Call(10); + /// Console.WriteLine(res); + /// </code> + /// </example> + /// <param name="delegate">Delegate method that will be called.</param> + /// <param name="trampoline">Trampoline function pointer for invoking the delegate.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Callable CreateWithUnsafeTrampoline(Delegate @delegate, + delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void> trampoline) + => new(@delegate, trampoline); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs new file mode 100644 index 0000000000..6c6a104019 --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Callable.generics.cs @@ -0,0 +1,480 @@ +using System; +using System.Runtime.CompilerServices; +using Godot.NativeInterop; + +namespace Godot; + +#nullable enable + +public readonly partial struct Callable +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowIfArgCountMismatch(NativeVariantPtrArgs args, int countExpected, + [CallerArgumentExpression("args")] string? paramName = null) + { + if (countExpected != args.Count) + ThrowArgCountMismatch(countExpected, args.Count, paramName); + + static void ThrowArgCountMismatch(int countExpected, int countReceived, string? paramName) + { + throw new ArgumentException( + "Invalid argument count for invoking callable." + + $" Expected {countExpected} arguments, received {countReceived}.", + paramName); + } + } + + /// <summary> + /// Constructs a new <see cref="Callable"/> for the given <paramref name="action"/>. + /// </summary> + /// <param name="action">Action method that will be called.</param> + public static unsafe Callable From( + Action action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 0); + + ((Action)delegateObj)(); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0>( + Action<T0> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 1); + + ((Action<T0>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1>( + Action<T0, T1> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 2); + + ((Action<T0, T1>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2>( + Action<T0, T1, T2> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 3); + + ((Action<T0, T1, T2>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3>( + Action<T0, T1, T2, T3> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 4); + + ((Action<T0, T1, T2, T3>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3, T4>( + Action<T0, T1, T2, T3, T4> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 5); + + ((Action<T0, T1, T2, T3, T4>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5>( + Action<T0, T1, T2, T3, T4, T5> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 6); + + ((Action<T0, T1, T2, T3, T4, T5>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6>( + Action<T0, T1, T2, T3, T4, T5, T6> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 7); + + ((Action<T0, T1, T2, T3, T4, T5, T6>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6, T7>( + Action<T0, T1, T2, T3, T4, T5, T6, T7> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 8); + + ((Action<T0, T1, T2, T3, T4, T5, T6, T7>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]), + VariantConversionCallbacks.GetToManagedCallback<T7>()(args[7]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <inheritdoc cref="From(Action)"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6, T7, T8>( + Action<T0, T1, T2, T3, T4, T5, T6, T7, T8> action + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 9); + + ((Action<T0, T1, T2, T3, T4, T5, T6, T7, T8>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]), + VariantConversionCallbacks.GetToManagedCallback<T7>()(args[7]), + VariantConversionCallbacks.GetToManagedCallback<T8>()(args[8]) + ); + + ret = default; + } + + return CreateWithUnsafeTrampoline(action, &Trampoline); + } + + /// <summary> + /// Constructs a new <see cref="Callable"/> for the given <paramref name="func"/>. + /// </summary> + /// <param name="func">Action method that will be called.</param> + public static unsafe Callable From<TResult>( + Func<TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 0); + + TResult res = ((Func<TResult>)delegateObj)(); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, TResult>( + Func<T0, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 1); + + TResult res = ((Func<T0, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, TResult>( + Func<T0, T1, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 2); + + TResult res = ((Func<T0, T1, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, TResult>( + Func<T0, T1, T2, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 3); + + TResult res = ((Func<T0, T1, T2, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, TResult>( + Func<T0, T1, T2, T3, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 4); + + TResult res = ((Func<T0, T1, T2, T3, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, TResult>( + Func<T0, T1, T2, T3, T4, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 5); + + TResult res = ((Func<T0, T1, T2, T3, T4, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, TResult>( + Func<T0, T1, T2, T3, T4, T5, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 6); + + TResult res = ((Func<T0, T1, T2, T3, T4, T5, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6, TResult>( + Func<T0, T1, T2, T3, T4, T5, T6, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 7); + + TResult res = ((Func<T0, T1, T2, T3, T4, T5, T6, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6, T7, TResult>( + Func<T0, T1, T2, T3, T4, T5, T6, T7, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 8); + + TResult res = ((Func<T0, T1, T2, T3, T4, T5, T6, T7, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]), + VariantConversionCallbacks.GetToManagedCallback<T7>()(args[7]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } + + /// <inheritdoc cref="From{TResult}(Func{TResult})"/> + public static unsafe Callable From<T0, T1, T2, T3, T4, T5, T6, T7, T8, TResult>( + Func<T0, T1, T2, T3, T4, T5, T6, T7, T8, TResult> func + ) + { + static void Trampoline(object delegateObj, NativeVariantPtrArgs args, out godot_variant ret) + { + ThrowIfArgCountMismatch(args, 9); + + TResult res = ((Func<T0, T1, T2, T3, T4, T5, T6, T7, T8, TResult>)delegateObj)( + VariantConversionCallbacks.GetToManagedCallback<T0>()(args[0]), + VariantConversionCallbacks.GetToManagedCallback<T1>()(args[1]), + VariantConversionCallbacks.GetToManagedCallback<T2>()(args[2]), + VariantConversionCallbacks.GetToManagedCallback<T3>()(args[3]), + VariantConversionCallbacks.GetToManagedCallback<T4>()(args[4]), + VariantConversionCallbacks.GetToManagedCallback<T5>()(args[5]), + VariantConversionCallbacks.GetToManagedCallback<T6>()(args[6]), + VariantConversionCallbacks.GetToManagedCallback<T7>()(args[7]), + VariantConversionCallbacks.GetToManagedCallback<T8>()(args[8]) + ); + + ret = VariantConversionCallbacks.GetToVariantCallback<TResult>()(res); + } + + return CreateWithUnsafeTrampoline(func, &Trampoline); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs index 3c75d18943..d19e0c08f2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs @@ -30,33 +30,23 @@ namespace Godot } [UnmanagedCallersOnly] - internal static unsafe void InvokeWithVariantArgs(IntPtr delegateGCHandle, godot_variant** args, uint argc, - godot_variant* outRet) + internal static unsafe void InvokeWithVariantArgs(IntPtr delegateGCHandle, void* trampoline, + godot_variant** args, int argc, godot_variant* outRet) { try { - // TODO: Optimize - var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; - var managedArgs = new object?[argc]; - - var parameterInfos = @delegate.Method.GetParameters(); - var paramsLength = parameterInfos.Length; - - if (argc != paramsLength) + if (trampoline == null) { - throw new InvalidOperationException( - $"The delegate expects {paramsLength} arguments, but received {argc}."); + throw new ArgumentNullException(nameof(trampoline), + "Cannot dynamically invoke delegate because the trampoline is null."); } - for (uint i = 0; i < argc; i++) - { - managedArgs[i] = Marshaling.ConvertVariantToManagedObjectOfType( - *args[i], parameterInfos[i].ParameterType); - } + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + var trampolineFn = (delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void>)trampoline; - object? invokeRet = @delegate.DynamicInvoke(managedArgs); + trampolineFn(@delegate, new NativeVariantPtrArgs(args, argc), out godot_variant ret); - *outRet = Marshaling.ConvertManagedObjectToVariant(invokeRet); + *outRet = ret; } catch (Exception e) { @@ -76,6 +66,11 @@ namespace Godot internal static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData) { + if (@delegate is null) + { + return false; + } + if (@delegate is MulticastDelegate multicastDelegate) { bool someDelegatesSerialized = false; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index 93103d0f6b..f8793332a0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -356,35 +356,47 @@ namespace Godot.Collections IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> { + private static godot_variant ToVariantFunc(in Dictionary<TKey, TValue> godotDictionary) => + VariantUtils.CreateFromDictionary(godotDictionary); + + private static Dictionary<TKey, TValue> FromVariantFunc(in godot_variant variant) => + VariantUtils.ConvertToDictionaryObject<TKey, TValue>(variant); + // ReSharper disable StaticMemberInGenericType // Warning is about unique static fields being created for each generic type combination: // https://www.jetbrains.com/help/resharper/StaticMemberInGenericType.html // In our case this is exactly what we want. - private static unsafe delegate* managed<in TKey, godot_variant> _convertKeyToVariantCallback; - private static unsafe delegate* managed<in godot_variant, TKey> _convertKeyToManagedCallback; - private static unsafe delegate* managed<in TValue, godot_variant> _convertValueToVariantCallback; - private static unsafe delegate* managed<in godot_variant, TValue> _convertValueToManagedCallback; + private static readonly unsafe delegate* managed<in TKey, godot_variant> ConvertKeyToVariantCallback; + private static readonly unsafe delegate* managed<in godot_variant, TKey> ConvertKeyToManagedCallback; + private static readonly unsafe delegate* managed<in TValue, godot_variant> ConvertValueToVariantCallback; + private static readonly unsafe delegate* managed<in godot_variant, TValue> ConvertValueToManagedCallback; // ReSharper restore StaticMemberInGenericType static unsafe Dictionary() { - _convertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TKey>(); - _convertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TKey>(); - _convertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TValue>(); - _convertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TValue>(); + VariantConversionCallbacks.GenericConversionCallbacks[typeof(Dictionary<TKey, TValue>)] = + ( + (IntPtr)(delegate* managed<in Dictionary<TKey, TValue>, godot_variant>)&ToVariantFunc, + (IntPtr)(delegate* managed<in godot_variant, Dictionary<TKey, TValue>>)&FromVariantFunc + ); + + ConvertKeyToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TKey>(); + ConvertKeyToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TKey>(); + ConvertValueToVariantCallback = VariantConversionCallbacks.GetToVariantCallback<TValue>(); + ConvertValueToManagedCallback = VariantConversionCallbacks.GetToManagedCallback<TValue>(); } private static unsafe void ValidateVariantConversionCallbacks() { - if (_convertKeyToVariantCallback == null || _convertKeyToManagedCallback == null) + if (ConvertKeyToVariantCallback == null || ConvertKeyToManagedCallback == null) { throw new InvalidOperationException( $"The dictionary key type is not supported for conversion to Variant: '{typeof(TKey).FullName}'."); } - if (_convertValueToVariantCallback == null || _convertValueToManagedCallback == null) + if (ConvertValueToVariantCallback == null || ConvertValueToManagedCallback == null) { throw new InvalidOperationException( $"The dictionary value type is not supported for conversion to Variant: '{typeof(TValue).FullName}'."); @@ -473,14 +485,14 @@ namespace Godot.Collections { get { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; if (NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant value).ToBool()) { using (value) - return _convertValueToManagedCallback(value); + return ConvertValueToManagedCallback(value); } else { @@ -489,8 +501,8 @@ namespace Godot.Collections } set { - using var variantKey = _convertKeyToVariantCallback(key); - using var variantValue = _convertValueToVariantCallback(value); + using var variantKey = ConvertKeyToVariantCallback(key); + using var variantValue = ConvertValueToVariantCallback(value); var self = (godot_dictionary)_underlyingDict.NativeValue; NativeFuncs.godotsharp_dictionary_set_value(ref self, variantKey, variantValue); @@ -539,8 +551,8 @@ namespace Godot.Collections using (value) { return new KeyValuePair<TKey, TValue>( - _convertKeyToManagedCallback(key), - _convertValueToManagedCallback(value)); + ConvertKeyToManagedCallback(key), + ConvertValueToManagedCallback(value)); } } @@ -552,13 +564,13 @@ namespace Godot.Collections /// <param name="value">The object to add.</param> public unsafe void Add(TKey key, TValue value) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; if (NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool()) throw new ArgumentException("An element with the same key already exists.", nameof(key)); - using var variantValue = _convertValueToVariantCallback(value); + using var variantValue = ConvertValueToVariantCallback(value); NativeFuncs.godotsharp_dictionary_add(ref self, variantKey, variantValue); } @@ -569,7 +581,7 @@ namespace Godot.Collections /// <returns>Whether or not this dictionary contains the given key.</returns> public unsafe bool ContainsKey(TKey key) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; return NativeFuncs.godotsharp_dictionary_contains_key(ref self, variantKey).ToBool(); } @@ -580,7 +592,7 @@ namespace Godot.Collections /// <param name="key">The key of the element to remove.</param> public unsafe bool Remove(TKey key) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool(); } @@ -593,13 +605,13 @@ namespace Godot.Collections /// <returns>If an object was found for the given <paramref name="key"/>.</returns> public unsafe bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) { - using var variantKey = _convertKeyToVariantCallback(key); + using var variantKey = ConvertKeyToVariantCallback(key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); using (retValue) - value = found ? _convertValueToManagedCallback(retValue) : default; + value = found ? ConvertValueToManagedCallback(retValue) : default; return found; } @@ -625,7 +637,7 @@ namespace Godot.Collections unsafe bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item) { - using var variantKey = _convertKeyToVariantCallback(item.Key); + using var variantKey = ConvertKeyToVariantCallback(item.Key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); @@ -635,7 +647,7 @@ namespace Godot.Collections if (!found) return false; - using var variantValue = _convertValueToVariantCallback(item.Value); + using var variantValue = ConvertValueToVariantCallback(item.Value); return NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool(); } } @@ -670,7 +682,7 @@ namespace Godot.Collections unsafe bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item) { - using var variantKey = _convertKeyToVariantCallback(item.Key); + using var variantKey = ConvertKeyToVariantCallback(item.Key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, variantKey, out godot_variant retValue).ToBool(); @@ -680,7 +692,7 @@ namespace Godot.Collections if (!found) return false; - using var variantValue = _convertValueToVariantCallback(item.Value); + using var variantValue = ConvertValueToVariantCallback(item.Value); if (NativeFuncs.godotsharp_variant_equals(variantValue, retValue).ToBool()) { return NativeFuncs.godotsharp_dictionary_remove_key( @@ -717,6 +729,7 @@ namespace Godot.Collections public static implicit operator Variant(Dictionary<TKey, TValue> from) => Variant.CreateFrom(from); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static explicit operator Dictionary<TKey, TValue>(Variant from) => from.AsGodotDictionary<TKey, TValue>(); + public static explicit operator Dictionary<TKey, TValue>(Variant from) => + from.AsGodotDictionary<TKey, TValue>(); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 76b186cd15..649661ee06 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -721,10 +721,19 @@ namespace Godot.NativeInterop if (p_managed_callable.Delegate != null) { var gcHandle = CustomGCHandle.AllocStrong(p_managed_callable.Delegate); - IntPtr objectPtr = p_managed_callable.Target != null ? Object.GetPtr(p_managed_callable.Target) : IntPtr.Zero; - NativeFuncs.godotsharp_callable_new_with_delegate( - GCHandle.ToIntPtr(gcHandle), objectPtr, out godot_callable callable); - return callable; + + IntPtr objectPtr = p_managed_callable.Target != null ? + Object.GetPtr(p_managed_callable.Target) : + IntPtr.Zero; + + unsafe + { + NativeFuncs.godotsharp_callable_new_with_delegate( + GCHandle.ToIntPtr(gcHandle), (IntPtr)p_managed_callable.Trampoline, + objectPtr, out godot_callable callable); + + return callable; + } } else { @@ -748,19 +757,22 @@ namespace Godot.NativeInterop public static Callable ConvertCallableToManaged(in godot_callable p_callable) { if (NativeFuncs.godotsharp_callable_get_data_for_marshalling(p_callable, - out IntPtr delegateGCHandle, out IntPtr godotObject, - out godot_string_name name).ToBool()) + out IntPtr delegateGCHandle, out IntPtr trampoline, + out IntPtr godotObject, out godot_string_name name).ToBool()) { if (delegateGCHandle != IntPtr.Zero) { - return new Callable((Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target); - } - else - { - return new Callable( - InteropUtils.UnmanagedGetManaged(godotObject), - StringName.CreateTakingOwnershipOfDisposableValue(name)); + unsafe + { + return Callable.CreateWithUnsafeTrampoline( + (Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target, + (delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void>)trampoline); + } } + + return new Callable( + InteropUtils.UnmanagedGetManaged(godotObject), + StringName.CreateTakingOwnershipOfDisposableValue(name)); } // Some other unsupported callable diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index 20ede9f0dd..088f4e7ecf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -141,11 +141,11 @@ namespace Godot.NativeInterop public static partial void godotsharp_packed_string_array_add(ref godot_packed_string_array r_dest, in godot_string p_element); - public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, IntPtr p_object, - out godot_callable r_callable); + public static partial void godotsharp_callable_new_with_delegate(IntPtr p_delegate_handle, IntPtr p_trampoline, + IntPtr p_object, out godot_callable r_callable); internal static partial godot_bool godotsharp_callable_get_data_for_marshalling(in godot_callable p_callable, - out IntPtr r_delegate_handle, out IntPtr r_object, out godot_string_name r_name); + out IntPtr r_delegate_handle, out IntPtr r_trampoline, out IntPtr r_object, out godot_string_name r_name); internal static partial godot_variant godotsharp_callable_call(in godot_callable p_callable, godot_variant** p_args, int p_arg_count, out godot_variant_call_error p_call_error); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs index 422df74c23..d8c5d99cb8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeVariantPtrArgs.cs @@ -8,8 +8,22 @@ namespace Godot.NativeInterop public unsafe ref struct NativeVariantPtrArgs { private godot_variant** _args; + private int _argc; - internal NativeVariantPtrArgs(godot_variant** args) => _args = args; + internal NativeVariantPtrArgs(godot_variant** args, int argc) + { + _args = args; + _argc = argc; + } + + /// <summary> + /// Returns the number of arguments. + /// </summary> + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _argc; + } public ref godot_variant this[int index] { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs index 9cde62c7c5..4b3db0c01a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantConversionCallbacks.cs @@ -1,10 +1,15 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace Godot.NativeInterop; +// TODO: Change VariantConversionCallbacks<T>. Store the callback in a static field for quick repeated access, instead of checking every time. internal static unsafe class VariantConversionCallbacks { + internal static System.Collections.Generic.Dictionary<Type, (IntPtr ToVariant, IntPtr FromVariant)> + GenericConversionCallbacks = new(); + [SuppressMessage("ReSharper", "RedundantNameQualifier")] internal static delegate*<in T, godot_variant> GetToVariantCallback<T>() { @@ -502,6 +507,26 @@ internal static unsafe class VariantConversionCallbacks &FromVariant; } + // TODO: + // IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode. + // We could make the Godot collections implement an interface and use IsAssignableFrom instead. + // Or we could just skip the check and always look for a conversion callback for the type. + if (typeOfT.IsGenericType) + { + var genericTypeDef = typeOfT.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) || + genericTypeDef == typeof(Godot.Collections.Array<>)) + { + RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle); + + if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion)) + { + return (delegate*<in T, godot_variant>)genericConversion.ToVariant; + } + } + } + return null; } @@ -1005,6 +1030,26 @@ internal static unsafe class VariantConversionCallbacks &ToVariant; } + // TODO: + // IsGenericType and GetGenericTypeDefinition don't work in NativeAOT's reflection-free mode. + // We could make the Godot collections implement an interface and use IsAssignableFrom instead. + // Or we could just skip the check and always look for a conversion callback for the type. + if (typeOfT.IsGenericType) + { + var genericTypeDef = typeOfT.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>) || + genericTypeDef == typeof(Godot.Collections.Array<>)) + { + RuntimeHelpers.RunClassConstructor(typeOfT.TypeHandle); + + if (GenericConversionCallbacks.TryGetValue(typeOfT, out var genericConversion)) + { + return (delegate*<in godot_variant, T>)genericConversion.FromVariant; + } + } + } + // ReSharper restore RedundantCast return null; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index 5cb678c280..60ee6eb6f4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -202,7 +202,7 @@ namespace Godot // ReSharper disable once VirtualMemberNeverOverridden.Global protected internal virtual void RaiseGodotClassSignalCallbacks(in godot_string_name signal, - NativeVariantPtrArgs args, int argCount) + NativeVariantPtrArgs args) { } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs index 13070c8033..664b2e0f34 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs @@ -292,6 +292,18 @@ namespace Godot } /// <summary> + /// Constructs a <see cref="Plane"/> from a <paramref name="normal"/> vector and + /// a <paramref name="point"/> on the plane. + /// </summary> + /// <param name="normal">The normal of the plane, must be normalized.</param> + /// <param name="point">The point on the plane.</param> + public Plane(Vector3 normal, Vector3 point) + { + _normal = normal; + D = _normal.Dot(point); + } + + /// <summary> /// Constructs a <see cref="Plane"/> from the three points, given in clockwise order. /// </summary> /// <param name="v1">The first point.</param> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index a63b668387..e3fb254f49 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -52,6 +52,7 @@ <Compile Include="Core\AABB.cs" /> <Compile Include="Core\Bridge\GodotSerializationInfo.cs" /> <Compile Include="Core\Bridge\MethodInfo.cs" /> + <Compile Include="Core\Callable.generics.cs" /> <Compile Include="Core\CustomGCHandle.cs" /> <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\AssemblyHasScriptsAttribute.cs" /> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 2717b945f6..e20a88076a 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -447,15 +447,16 @@ void godotsharp_packed_string_array_add(PackedStringArray *r_dest, const String r_dest->append(*p_element); } -void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, const Object *p_object, Callable *r_callable) { +void godotsharp_callable_new_with_delegate(GCHandleIntPtr p_delegate_handle, void *p_trampoline, + const Object *p_object, Callable *r_callable) { // TODO: Use pooling for ManagedCallable instances. ObjectID objid = p_object ? p_object->get_instance_id() : ObjectID(); - CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle, objid)); + CallableCustom *managed_callable = memnew(ManagedCallable(p_delegate_handle, p_trampoline, objid)); memnew_placement(r_callable, Callable(managed_callable)); } bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, - GCHandleIntPtr *r_delegate_handle, Object **r_object, StringName *r_name) { + GCHandleIntPtr *r_delegate_handle, void **r_trampoline, Object **r_object, StringName *r_name) { if (p_callable->is_custom()) { CallableCustom *custom = p_callable->get_custom(); CallableCustom::CompareEqualFunc compare_equal_func = custom->get_compare_equal_func(); @@ -463,18 +464,21 @@ bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, if (compare_equal_func == ManagedCallable::compare_equal_func_ptr) { ManagedCallable *managed_callable = static_cast<ManagedCallable *>(custom); *r_delegate_handle = managed_callable->get_delegate(); + *r_trampoline = managed_callable->get_trampoline(); *r_object = nullptr; memnew_placement(r_name, StringName()); return true; } else if (compare_equal_func == SignalAwaiterCallable::compare_equal_func_ptr) { SignalAwaiterCallable *signal_awaiter_callable = static_cast<SignalAwaiterCallable *>(custom); *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(signal_awaiter_callable->get_object()); memnew_placement(r_name, StringName(signal_awaiter_callable->get_signal())); return true; } else if (compare_equal_func == EventSignalCallable::compare_equal_func_ptr) { EventSignalCallable *event_signal_callable = static_cast<EventSignalCallable *>(custom); *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(event_signal_callable->get_object()); memnew_placement(r_name, StringName(event_signal_callable->get_signal())); return true; @@ -482,11 +486,13 @@ bool godotsharp_callable_get_data_for_marshalling(const Callable *p_callable, // Some other CallableCustom. We only support ManagedCallable. *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = nullptr; memnew_placement(r_name, StringName()); return false; } else { *r_delegate_handle = { nullptr }; + *r_trampoline = nullptr; *r_object = ObjectDB::get_instance(p_callable->get_object_id()); memnew_placement(r_name, StringName(p_callable->get_method())); return true; diff --git a/modules/mono/godotsharp_dirs.cpp b/modules/mono/godotsharp_dirs.cpp index c7e47d2718..185a7e60cf 100644 --- a/modules/mono/godotsharp_dirs.cpp +++ b/modules/mono/godotsharp_dirs.cpp @@ -94,138 +94,63 @@ String _get_mono_user_dir() { class _GodotSharpDirs { public: - String res_data_dir; String res_metadata_dir; - String res_config_dir; - String res_temp_dir; - String res_temp_assemblies_base_dir; String res_temp_assemblies_dir; String mono_user_dir; - String mono_logs_dir; - - String api_assemblies_base_dir; String api_assemblies_dir; #ifdef TOOLS_ENABLED - String mono_solutions_dir; String build_logs_dir; - String data_editor_tools_dir; -#else - // Equivalent of res_assemblies_dir, but in the data directory rather than in 'res://'. - // Only defined on export templates. Used when exporting assemblies outside of PCKs. - String data_game_assemblies_dir; -#endif - - String data_mono_etc_dir; - String data_mono_lib_dir; - -#ifdef WINDOWS_ENABLED - String data_mono_bin_dir; #endif private: _GodotSharpDirs() { - res_data_dir = ProjectSettings::get_singleton()->get_project_data_path().path_join("mono"); + String res_data_dir = ProjectSettings::get_singleton()->get_project_data_path().path_join("mono"); res_metadata_dir = res_data_dir.path_join("metadata"); - res_config_dir = res_data_dir.path_join("etc").path_join("mono"); // TODO use paths from csproj - res_temp_dir = res_data_dir.path_join("temp"); - res_temp_assemblies_base_dir = res_temp_dir.path_join("bin"); - res_temp_assemblies_dir = res_temp_assemblies_base_dir.path_join(_get_expected_build_config()); - - api_assemblies_base_dir = res_data_dir.path_join("assemblies"); + res_temp_assemblies_dir = res_data_dir.path_join("temp").path_join("bin").path_join(_get_expected_build_config()); #ifdef WEB_ENABLED mono_user_dir = "user://"; #else mono_user_dir = _get_mono_user_dir(); #endif - mono_logs_dir = mono_user_dir.path_join("mono_logs"); - -#ifdef TOOLS_ENABLED - mono_solutions_dir = mono_user_dir.path_join("solutions"); - build_logs_dir = mono_user_dir.path_join("build_logs"); - - String base_path = ProjectSettings::get_singleton()->globalize_path("res://"); -#endif String exe_dir = OS::get_singleton()->get_executable_path().get_base_dir(); + String res_dir = OS::get_singleton()->get_bundle_resource_dir(); #ifdef TOOLS_ENABLED - String data_dir_root = exe_dir.path_join("GodotSharp"); data_editor_tools_dir = data_dir_root.path_join("Tools"); - api_assemblies_base_dir = data_dir_root.path_join("Api"); - - String data_mono_root_dir = data_dir_root.path_join("Mono"); - data_mono_etc_dir = data_mono_root_dir.path_join("etc"); - -#ifdef ANDROID_ENABLED - data_mono_lib_dir = gdmono::android::support::get_app_native_lib_dir(); -#else - data_mono_lib_dir = data_mono_root_dir.path_join("lib"); -#endif - -#ifdef WINDOWS_ENABLED - data_mono_bin_dir = data_mono_root_dir.path_join("bin"); -#endif - + String api_assemblies_base_dir = data_dir_root.path_join("Api"); + build_logs_dir = mono_user_dir.path_join("build_logs"); #ifdef MACOS_ENABLED if (!DirAccess::exists(data_editor_tools_dir)) { - data_editor_tools_dir = exe_dir.path_join("../Resources/GodotSharp/Tools"); + data_editor_tools_dir = res_dir.path_join("GodotSharp").path_join("Tools"); } - if (!DirAccess::exists(api_assemblies_base_dir)) { - api_assemblies_base_dir = exe_dir.path_join("../Resources/GodotSharp/Api"); - } - - if (!DirAccess::exists(data_mono_root_dir)) { - data_mono_etc_dir = exe_dir.path_join("../Resources/GodotSharp/Mono/etc"); - data_mono_lib_dir = exe_dir.path_join("../Resources/GodotSharp/Mono/lib"); + api_assemblies_base_dir = res_dir.path_join("GodotSharp").path_join("Api"); } #endif - -#else - + api_assemblies_dir = api_assemblies_base_dir.path_join(GDMono::get_expected_api_build_config()); +#else // TOOLS_ENABLED + String arch = Engine::get_singleton()->get_architecture_name(); String appname = ProjectSettings::get_singleton()->get("application/config/name"); String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); - String data_dir_root = exe_dir.path_join("data_" + appname_safe); + String data_dir_root = exe_dir.path_join("data_" + appname_safe + "_" + arch); if (!DirAccess::exists(data_dir_root)) { - data_dir_root = exe_dir.path_join("data_Godot"); + data_dir_root = exe_dir.path_join("data_Godot_" + arch); } - - String data_mono_root_dir = data_dir_root.path_join("Mono"); - data_mono_etc_dir = data_mono_root_dir.path_join("etc"); - -#ifdef ANDROID_ENABLED - data_mono_lib_dir = gdmono::android::support::get_app_native_lib_dir(); -#else - data_mono_lib_dir = data_mono_root_dir.path_join("lib"); - data_game_assemblies_dir = data_dir_root.path_join("Assemblies"); -#endif - -#ifdef WINDOWS_ENABLED - data_mono_bin_dir = data_mono_root_dir.path_join("bin"); -#endif - #ifdef MACOS_ENABLED - if (!DirAccess::exists(data_mono_root_dir)) { - data_mono_etc_dir = exe_dir.path_join("../Resources/GodotSharp/Mono/etc"); - data_mono_lib_dir = exe_dir.path_join("../Resources/GodotSharp/Mono/lib"); + if (!DirAccess::exists(data_dir_root)) { + data_dir_root = res_dir.path_join("data_" + appname_safe + "_" + arch); } - - if (!DirAccess::exists(data_game_assemblies_dir)) { - data_game_assemblies_dir = exe_dir.path_join("../Resources/GodotSharp/Assemblies"); + if (!DirAccess::exists(data_dir_root)) { + data_dir_root = res_dir.path_join("data_Godot_" + arch); } #endif - -#endif - -#ifdef TOOLS_ENABLED - api_assemblies_dir = api_assemblies_base_dir.path_join(GDMono::get_expected_api_build_config()); -#else api_assemblies_dir = data_dir_root; #endif } @@ -237,26 +162,10 @@ public: } }; -String get_res_data_dir() { - return _GodotSharpDirs::get_singleton().res_data_dir; -} - String get_res_metadata_dir() { return _GodotSharpDirs::get_singleton().res_metadata_dir; } -String get_res_config_dir() { - return _GodotSharpDirs::get_singleton().res_config_dir; -} - -String get_res_temp_dir() { - return _GodotSharpDirs::get_singleton().res_temp_dir; -} - -String get_res_temp_assemblies_base_dir() { - return _GodotSharpDirs::get_singleton().res_temp_assemblies_base_dir; -} - String get_res_temp_assemblies_dir() { return _GodotSharpDirs::get_singleton().res_temp_assemblies_dir; } @@ -265,23 +174,11 @@ String get_api_assemblies_dir() { return _GodotSharpDirs::get_singleton().api_assemblies_dir; } -String get_api_assemblies_base_dir() { - return _GodotSharpDirs::get_singleton().api_assemblies_base_dir; -} - String get_mono_user_dir() { return _GodotSharpDirs::get_singleton().mono_user_dir; } -String get_mono_logs_dir() { - return _GodotSharpDirs::get_singleton().mono_logs_dir; -} - #ifdef TOOLS_ENABLED -String get_mono_solutions_dir() { - return _GodotSharpDirs::get_singleton().mono_solutions_dir; -} - String get_build_logs_dir() { return _GodotSharpDirs::get_singleton().build_logs_dir; } @@ -289,23 +186,6 @@ String get_build_logs_dir() { String get_data_editor_tools_dir() { return _GodotSharpDirs::get_singleton().data_editor_tools_dir; } -#else -String get_data_game_assemblies_dir() { - return _GodotSharpDirs::get_singleton().data_game_assemblies_dir; -} #endif -String get_data_mono_etc_dir() { - return _GodotSharpDirs::get_singleton().data_mono_etc_dir; -} - -String get_data_mono_lib_dir() { - return _GodotSharpDirs::get_singleton().data_mono_lib_dir; -} - -#ifdef WINDOWS_ENABLED -String get_data_mono_bin_dir() { - return _GodotSharpDirs::get_singleton().data_mono_bin_dir; -} -#endif } // namespace GodotSharpDirs diff --git a/modules/mono/godotsharp_dirs.h b/modules/mono/godotsharp_dirs.h index 03e62ffd82..cdfb8e4787 100644 --- a/modules/mono/godotsharp_dirs.h +++ b/modules/mono/godotsharp_dirs.h @@ -35,34 +35,18 @@ namespace GodotSharpDirs { -String get_res_data_dir(); String get_res_metadata_dir(); -String get_res_config_dir(); -String get_res_temp_dir(); -String get_res_temp_assemblies_base_dir(); String get_res_temp_assemblies_dir(); String get_api_assemblies_dir(); -String get_api_assemblies_base_dir(); String get_mono_user_dir(); -String get_mono_logs_dir(); #ifdef TOOLS_ENABLED -String get_mono_solutions_dir(); String get_build_logs_dir(); - String get_data_editor_tools_dir(); -#else -String get_data_game_assemblies_dir(); #endif -String get_data_mono_etc_dir(); -String get_data_mono_lib_dir(); - -#ifdef WINDOWS_ENABLED -String get_data_mono_bin_dir(); -#endif } // namespace GodotSharpDirs #endif // GODOTSHARP_DIRS_H diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 0c2c533090..28edc41d98 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -92,7 +92,7 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant ERR_FAIL_COND(delegate_handle.value == nullptr); GDMonoCache::managed_callbacks.DelegateUtils_InvokeWithVariantArgs( - delegate_handle, p_arguments, p_argcount, &r_return_value); + delegate_handle, trampoline, p_arguments, p_argcount, &r_return_value); r_call_error.error = Callable::CallError::CALL_OK; } @@ -106,7 +106,8 @@ void ManagedCallable::release_delegate_handle() { // Why you do this clang-format... /* clang-format off */ -ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id) : delegate_handle(p_delegate_handle), object_id(p_object_id) { +ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle, void *p_trampoline, ObjectID p_object_id) : + delegate_handle(p_delegate_handle), trampoline(p_trampoline), object_id(p_object_id) { #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(instances_mutex); diff --git a/modules/mono/managed_callable.h b/modules/mono/managed_callable.h index 26cd164fb6..b3a137dedf 100644 --- a/modules/mono/managed_callable.h +++ b/modules/mono/managed_callable.h @@ -40,6 +40,7 @@ class ManagedCallable : public CallableCustom { friend class CSharpLanguage; GCHandleIntPtr delegate_handle; + void *trampoline = nullptr; ObjectID object_id; #ifdef GD_MONO_HOT_RELOAD @@ -58,6 +59,7 @@ public: void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; _FORCE_INLINE_ GCHandleIntPtr get_delegate() const { return delegate_handle; } + _FORCE_INLINE_ void *get_trampoline() const { return trampoline; } static bool compare_equal(const CallableCustom *p_a, const CallableCustom *p_b); static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b); @@ -67,7 +69,7 @@ public: void release_delegate_handle(); - ManagedCallable(GCHandleIntPtr p_delegate_handle, ObjectID p_object_id); + ManagedCallable(GCHandleIntPtr p_delegate_handle, void *p_trampoline, ObjectID p_object_id); ~ManagedCallable(); }; diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 13b599fe55..9c26fa2b0a 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -74,7 +74,7 @@ struct ManagedCallbacks { using Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add = void(GD_CLR_STDCALL *)(CSharpScript *p_script, void *p_def_vals, int32_t p_count); using FuncSignalAwaiter_SignalCallback = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, int32_t, bool *); - using FuncDelegateUtils_InvokeWithVariantArgs = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, uint32_t, const Variant *); + using FuncDelegateUtils_InvokeWithVariantArgs = void(GD_CLR_STDCALL *)(GCHandleIntPtr, void *, const Variant **, int32_t, const Variant *); using FuncDelegateUtils_DelegateEquals = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr); using FuncDelegateUtils_TrySerializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const Array *); using FuncDelegateUtils_TryDeserializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(const Array *, GCHandleIntPtr *); diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml index 62bb396d15..a7b89f58ac 100644 --- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml +++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml @@ -42,6 +42,10 @@ The root path to use for RPCs and replication. Instead of an absolute path, a relative path will be used to find the node upon which the RPC should be executed. This effectively allows to have different branches of the scene tree to be managed by different MultiplayerAPI, allowing for example to run both client and server in the same scene. </member> + <member name="server_relay" type="bool" setter="set_server_relay_enabled" getter="is_server_relay_enabled" default="true"> + Enable or disable the server feature that notifies clients of other peers' connection/disconnection, and relays messages between them. When this option is [code]false[/code], clients won't be automatically notified of other peers and won't be able to send them packets through the server. + [b]Note:[/b] Support for this feature may depend on the current [MultiplayerPeer] configuration. See [method MultiplayerPeer.is_server_relay_supported]. + </member> </members> <signals> <signal name="peer_packet"> diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp index 9755f426d5..95857392c7 100644 --- a/modules/multiplayer/multiplayer_synchronizer.cpp +++ b/modules/multiplayer/multiplayer_synchronizer.cpp @@ -118,6 +118,10 @@ void MultiplayerSynchronizer::set_net_id(uint32_t p_net_id) { } bool MultiplayerSynchronizer::update_outbound_sync_time(uint64_t p_msec) { + if (last_sync_msec == p_msec) { + // last_sync_msec has been updated on this frame. + return true; + } if (p_msec >= last_sync_msec + interval_msec) { last_sync_msec = p_msec; return true; diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp index b3b642f815..7df9b95b30 100644 --- a/modules/multiplayer/scene_cache_interface.cpp +++ b/modules/multiplayer/scene_cache_interface.cpp @@ -105,8 +105,7 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac multiplayer_peer->set_transfer_channel(0); multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); - multiplayer_peer->set_target_peer(p_from); - multiplayer_peer->put_packet(packet.ptr(), packet.size()); + multiplayer->send_command(p_from, packet.ptr(), packet.size()); } void SceneCacheInterface::process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len) { @@ -162,10 +161,9 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodePath p_path, Pat Error err = OK; for (int peer_id : p_peers) { - multiplayer_peer->set_target_peer(peer_id); multiplayer_peer->set_transfer_channel(0); multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); - err = multiplayer_peer->put_packet(packet.ptr(), packet.size()); + err = multiplayer->send_command(peer_id, packet.ptr(), packet.size()); ERR_FAIL_COND_V(err != OK, err); // Insert into confirmed, but as false since it was not confirmed. psc->confirmed_peers.insert(peer_id, false); diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp index 3fc1eef366..f94f8ef658 100644 --- a/modules/multiplayer/scene_multiplayer.cpp +++ b/modules/multiplayer/scene_multiplayer.cpp @@ -67,12 +67,20 @@ Error SceneMultiplayer::poll() { const uint8_t *packet; int len; + int channel = multiplayer_peer->get_packet_channel(); + MultiplayerPeer::TransferMode mode = multiplayer_peer->get_packet_mode(); + Error err = multiplayer_peer->get_packet(&packet, len); ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error getting packet! %d", err)); - remote_sender_id = sender; - _process_packet(sender, packet, len); - remote_sender_id = 0; + if (len && (packet[0] & CMD_MASK) == NETWORK_COMMAND_SYS) { + // Sys messages are processed separately since they might call _process_packet themselves. + _process_sys(sender, packet, len, mode, channel); + } else { + remote_sender_id = sender; + _process_packet(sender, packet, len); + remote_sender_id = 0; + } if (!multiplayer_peer.is_valid()) { return OK; // It's also possible that a packet or RPC caused a disconnection, so also check here. @@ -86,6 +94,7 @@ void SceneMultiplayer::clear() { connected_peers.clear(); packet_cache.clear(); cache->clear(); + relay_buffer->clear(); } void SceneMultiplayer::set_root_path(const NodePath &p_path) { @@ -166,10 +175,123 @@ void SceneMultiplayer::_process_packet(int p_from, const uint8_t *p_packet, int case NETWORK_COMMAND_SYNC: { replicator->on_sync_receive(p_from, p_packet, p_packet_len); } break; + default: { + ERR_FAIL_MSG("Invalid network command from " + itos(p_from)); + } break; + } +} + +Error SceneMultiplayer::send_command(int p_to, const uint8_t *p_packet, int p_packet_len) { + if (server_relay && get_unique_id() != 1 && p_to != 1 && multiplayer_peer->is_server_relay_supported()) { + // Send relay packet. + relay_buffer->seek(0); + relay_buffer->put_u8(NETWORK_COMMAND_SYS); + relay_buffer->put_u8(SYS_COMMAND_RELAY); + relay_buffer->put_32(p_to); // Set the destination. + relay_buffer->put_data(p_packet, p_packet_len); + multiplayer_peer->set_target_peer(1); + const Vector<uint8_t> data = relay_buffer->get_data_array(); + return multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + } + if (p_to < 0) { + for (const int &pid : connected_peers) { + if (pid == -p_to) { + continue; + } + multiplayer_peer->set_target_peer(pid); + multiplayer_peer->put_packet(p_packet, p_packet_len); + } + return OK; + } else { + multiplayer_peer->set_target_peer(p_to); + return multiplayer_peer->put_packet(p_packet, p_packet_len); + } +} + +void SceneMultiplayer::_process_sys(int p_from, const uint8_t *p_packet, int p_packet_len, MultiplayerPeer::TransferMode p_mode, int p_channel) { + ERR_FAIL_COND_MSG(p_packet_len < SYS_CMD_SIZE, "Invalid packet received. Size too small."); + uint8_t sys_cmd_type = p_packet[1]; + int32_t peer = int32_t(decode_uint32(&p_packet[2])); + switch (sys_cmd_type) { + case SYS_COMMAND_ADD_PEER: { + ERR_FAIL_COND(!server_relay || !multiplayer_peer->is_server_relay_supported() || get_unique_id() == 1 || p_from != 1); + _add_peer(peer); + } break; + case SYS_COMMAND_DEL_PEER: { + ERR_FAIL_COND(!server_relay || !multiplayer_peer->is_server_relay_supported() || get_unique_id() == 1 || p_from != 1); + _del_peer(peer); + } break; + case SYS_COMMAND_RELAY: { + ERR_FAIL_COND(!server_relay || !multiplayer_peer->is_server_relay_supported()); + ERR_FAIL_COND(p_packet_len < SYS_CMD_SIZE + 1); + const uint8_t *packet = p_packet + SYS_CMD_SIZE; + int len = p_packet_len - SYS_CMD_SIZE; + bool should_process = false; + if (get_unique_id() == 1) { // I am the server. + // Direct messages to server should not go through relay. + ERR_FAIL_COND(peer > 0 && !connected_peers.has(peer)); + // Send relay packet. + relay_buffer->seek(0); + relay_buffer->put_u8(NETWORK_COMMAND_SYS); + relay_buffer->put_u8(SYS_COMMAND_RELAY); + relay_buffer->put_32(p_from); // Set the source. + relay_buffer->put_data(packet, len); + const Vector<uint8_t> data = relay_buffer->get_data_array(); + multiplayer_peer->set_transfer_mode(p_mode); + multiplayer_peer->set_transfer_channel(p_channel); + if (peer > 0) { + multiplayer_peer->set_target_peer(peer); + multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + } else { + for (const int &P : connected_peers) { + // Not to sender, nor excluded. + if (P == p_from || (peer < 0 && P != -peer)) { + continue; + } + multiplayer_peer->set_target_peer(P); + multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + } + } + if (peer == 0 || peer == -1) { + should_process = true; + peer = p_from; // Process as the source. + } + } else { + ERR_FAIL_COND(p_from != 1); // Bug. + should_process = true; + } + if (should_process) { + remote_sender_id = peer; + _process_packet(peer, packet, len); + remote_sender_id = 0; + } + } break; + default: { + ERR_FAIL(); + } } } void SceneMultiplayer::_add_peer(int p_id) { + if (server_relay && get_unique_id() == 1 && multiplayer_peer->is_server_relay_supported()) { + // Notify others of connection, and send connected peers to newly connected one. + uint8_t buf[SYS_CMD_SIZE]; + buf[0] = NETWORK_COMMAND_SYS; + buf[1] = SYS_COMMAND_ADD_PEER; + multiplayer_peer->set_transfer_channel(0); + multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); + for (const int &P : connected_peers) { + // Send new peer to already connected. + encode_uint32(p_id, &buf[2]); + multiplayer_peer->set_target_peer(P); + multiplayer_peer->put_packet(buf, sizeof(buf)); + // Send already connected to new peer. + encode_uint32(P, &buf[2]); + multiplayer_peer->set_target_peer(p_id); + multiplayer_peer->put_packet(buf, sizeof(buf)); + } + } + connected_peers.insert(p_id); cache->on_peer_change(p_id, true); replicator->on_peer_change(p_id, true); @@ -177,6 +299,23 @@ void SceneMultiplayer::_add_peer(int p_id) { } void SceneMultiplayer::_del_peer(int p_id) { + if (server_relay && get_unique_id() == 1 && multiplayer_peer->is_server_relay_supported()) { + // Notify others of disconnection. + uint8_t buf[SYS_CMD_SIZE]; + buf[0] = NETWORK_COMMAND_SYS; + buf[1] = SYS_COMMAND_DEL_PEER; + multiplayer_peer->set_transfer_channel(0); + multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); + encode_uint32(p_id, &buf[2]); + for (const int &P : connected_peers) { + if (P == p_id) { + continue; + } + multiplayer_peer->set_target_peer(P); + multiplayer_peer->put_packet(buf, sizeof(buf)); + } + } + replicator->on_peer_change(p_id, false); cache->on_peer_change(p_id, false); connected_peers.erase(p_id); @@ -209,11 +348,9 @@ Error SceneMultiplayer::send_bytes(Vector<uint8_t> p_data, int p_to, Multiplayer packet_cache.write[0] = NETWORK_COMMAND_RAW; memcpy(&packet_cache.write[1], &r[0], p_data.size()); - multiplayer_peer->set_target_peer(p_to); multiplayer_peer->set_transfer_channel(p_channel); multiplayer_peer->set_transfer_mode(p_mode); - - return multiplayer_peer->put_packet(packet_cache.ptr(), p_data.size() + 1); + return send_command(p_to, packet_cache.ptr(), p_data.size() + 1); } void SceneMultiplayer::_process_raw(int p_from, const uint8_t *p_packet, int p_packet_len) { @@ -303,6 +440,15 @@ Error SceneMultiplayer::object_configuration_remove(Object *p_obj, Variant p_con return ERR_INVALID_PARAMETER; } +void SceneMultiplayer::set_server_relay_enabled(bool p_enabled) { + ERR_FAIL_COND_MSG(multiplayer_peer.is_valid() && multiplayer_peer->get_connection_status() != MultiplayerPeer::CONNECTION_DISCONNECTED, "Cannot change the server relay option while the multiplayer peer is active."); + server_relay = p_enabled; +} + +bool SceneMultiplayer::is_server_relay_enabled() const { + return server_relay; +} + void SceneMultiplayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_root_path", "path"), &SceneMultiplayer::set_root_path); ClassDB::bind_method(D_METHOD("get_root_path"), &SceneMultiplayer::get_root_path); @@ -311,17 +457,22 @@ void SceneMultiplayer::_bind_methods() { ClassDB::bind_method(D_METHOD("is_refusing_new_connections"), &SceneMultiplayer::is_refusing_new_connections); ClassDB::bind_method(D_METHOD("set_allow_object_decoding", "enable"), &SceneMultiplayer::set_allow_object_decoding); ClassDB::bind_method(D_METHOD("is_object_decoding_allowed"), &SceneMultiplayer::is_object_decoding_allowed); + ClassDB::bind_method(D_METHOD("set_server_relay_enabled", "enabled"), &SceneMultiplayer::set_server_relay_enabled); + ClassDB::bind_method(D_METHOD("is_server_relay_enabled"), &SceneMultiplayer::is_server_relay_enabled); ClassDB::bind_method(D_METHOD("send_bytes", "bytes", "id", "mode", "channel"), &SceneMultiplayer::send_bytes, DEFVAL(MultiplayerPeer::TARGET_PEER_BROADCAST), DEFVAL(MultiplayerPeer::TRANSFER_MODE_RELIABLE), DEFVAL(0)); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_connections"), "set_refuse_new_connections", "is_refusing_new_connections"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "server_relay"), "set_server_relay_enabled", "is_server_relay_enabled"); + ADD_PROPERTY_DEFAULT("refuse_new_connections", false); ADD_SIGNAL(MethodInfo("peer_packet", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packet"))); } SceneMultiplayer::SceneMultiplayer() { + relay_buffer.instantiate(); replicator = Ref<SceneReplicationInterface>(memnew(SceneReplicationInterface(this))); rpc = Ref<SceneRPCInterface>(memnew(SceneRPCInterface(this))); cache = Ref<SceneCacheInterface>(memnew(SceneCacheInterface(this))); diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h index a99cca7b21..06d7b2c7a7 100644 --- a/modules/multiplayer/scene_multiplayer.h +++ b/modules/multiplayer/scene_multiplayer.h @@ -49,6 +49,17 @@ public: NETWORK_COMMAND_SPAWN, NETWORK_COMMAND_DESPAWN, NETWORK_COMMAND_SYNC, + NETWORK_COMMAND_SYS, + }; + + enum SysCommands { + SYS_COMMAND_ADD_PEER, + SYS_COMMAND_DEL_PEER, + SYS_COMMAND_RELAY, + }; + + enum { + SYS_CMD_SIZE = 6, // Command + sys command + peer_id (+ optional payload). }; // For each command, the 4 MSB can contain custom flags, as defined by subsystems. @@ -74,6 +85,8 @@ private: NodePath root_path; bool allow_object_decoding = false; + bool server_relay = true; + Ref<StreamPeerBuffer> relay_buffer; Ref<SceneCacheInterface> cache; Ref<SceneReplicationInterface> replicator; @@ -84,6 +97,7 @@ protected: void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len); void _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len); + void _process_sys(int p_from, const uint8_t *p_packet, int p_packet_len, MultiplayerPeer::TransferMode p_mode, int p_channel); void _add_peer(int p_id); void _del_peer(int p_id); @@ -111,6 +125,7 @@ public: void set_root_path(const NodePath &p_path); NodePath get_root_path() const; + Error send_command(int p_to, const uint8_t *p_packet, int p_packet_len); // Used internally to relay packets when needed. Error send_bytes(Vector<uint8_t> p_data, int p_to = MultiplayerPeer::TARGET_PEER_BROADCAST, MultiplayerPeer::TransferMode p_mode = MultiplayerPeer::TRANSFER_MODE_RELIABLE, int p_channel = 0); String get_rpc_md5(const Object *p_obj); @@ -123,6 +138,9 @@ public: void set_allow_object_decoding(bool p_enable); bool is_object_decoding_allowed() const; + void set_server_relay_enabled(bool p_enabled); + bool is_server_relay_enabled() const; + Ref<SceneCacheInterface> get_path_cache() { return cache; } #ifdef DEBUG_ENABLED diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp index df9985916b..659ce7316a 100644 --- a/modules/multiplayer/scene_replication_interface.cpp +++ b/modules/multiplayer/scene_replication_interface.cpp @@ -72,7 +72,7 @@ void SceneReplicationInterface::_free_remotes(const PeerInfo &p_info) { for (const KeyValue<uint32_t, ObjectID> &E : p_info.recv_nodes) { Node *node = tracked_nodes.has(E.value) ? get_id_as<Node>(E.value) : nullptr; ERR_CONTINUE(!node); - node->queue_delete(); + node->queue_free(); } } @@ -261,11 +261,11 @@ Error SceneReplicationInterface::_update_sync_visibility(int p_peer, Multiplayer if (p_peer == 0) { for (KeyValue<int, PeerInfo> &E : peers_info) { // Might be visible to this specific peer. - is_visible = is_visible || p_sync->is_visible_to(E.key); - if (is_visible == E.value.sync_nodes.has(sid)) { + bool is_visible_to_peer = is_visible || p_sync->is_visible_to(E.key); + if (is_visible_to_peer == E.value.sync_nodes.has(sid)) { continue; } - if (is_visible) { + if (is_visible_to_peer) { E.value.sync_nodes.insert(sid); } else { E.value.sync_nodes.erase(sid); @@ -370,10 +370,9 @@ Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size, #endif Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); - peer->set_target_peer(p_peer); peer->set_transfer_channel(0); peer->set_transfer_mode(p_reliable ? MultiplayerPeer::TRANSFER_MODE_RELIABLE : MultiplayerPeer::TRANSFER_MODE_UNRELIABLE); - return peer->put_packet(p_buffer, p_size); + return multiplayer->send_command(p_peer, p_buffer, p_size); } Error SceneReplicationInterface::_make_spawn_packet(Node *p_node, MultiplayerSpawner *p_spawner, int &r_len) { @@ -582,7 +581,7 @@ Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p if (node->get_parent() != nullptr) { node->get_parent()->remove_child(node); } - node->queue_delete(); + node->queue_free(); spawner->emit_signal(SNAME("despawned"), node); return OK; diff --git a/modules/multiplayer/scene_rpc_interface.cpp b/modules/multiplayer/scene_rpc_interface.cpp index 65090b9316..acc113c901 100644 --- a/modules/multiplayer/scene_rpc_interface.cpp +++ b/modules/multiplayer/scene_rpc_interface.cpp @@ -412,8 +412,7 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con if (has_all_peers) { // They all have verified paths, so send fast. - peer->set_target_peer(p_to); // To all of you. - peer->put_packet(packet_cache.ptr(), ofs); // A message with love. + multiplayer->send_command(p_to, packet_cache.ptr(), ofs); } else { // Unreachable because the node ID is never compressed if the peers doesn't know it. CRASH_COND(node_id_compression != NETWORK_NODE_ID_COMPRESSION_32); @@ -438,16 +437,14 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con bool confirmed = multiplayer->get_path_cache()->is_cache_confirmed(from_path, P); - peer->set_target_peer(P); // To this one specifically. - if (confirmed) { // This one confirmed path, so use id. encode_uint32(psc_id, &(packet_cache.write[1])); - peer->put_packet(packet_cache.ptr(), ofs); + multiplayer->send_command(P, packet_cache.ptr(), ofs); } else { // This one did not confirm path yet, so use entire path (sorry!). encode_uint32(0x80000000 | ofs, &(packet_cache.write[1])); // Offset to path and flag. - peer->put_packet(packet_cache.ptr(), ofs + path_len); + multiplayer->send_command(P, packet_cache.ptr(), ofs + path_len); } } } diff --git a/modules/navigation/navigation_mesh_generator.cpp b/modules/navigation/navigation_mesh_generator.cpp index f989fc45a5..f0d3e329ce 100644 --- a/modules/navigation/navigation_mesh_generator.cpp +++ b/modules/navigation/navigation_mesh_generator.cpp @@ -266,10 +266,10 @@ void NavigationMeshGenerator::_parse_geometry(const Transform3D &p_navmesh_trans if (err == OK) { PackedVector3Array faces; - for (int j = 0; j < md.faces.size(); ++j) { - Geometry3D::MeshData::Face face = md.faces[j]; + for (uint32_t j = 0; j < md.faces.size(); ++j) { + const Geometry3D::MeshData::Face &face = md.faces[j]; - for (int k = 2; k < face.indices.size(); ++k) { + for (uint32_t k = 2; k < face.indices.size(); ++k) { faces.push_back(md.vertices[face.indices[0]]); faces.push_back(md.vertices[face.indices[k - 1]]); faces.push_back(md.vertices[face.indices[k]]); @@ -392,10 +392,10 @@ void NavigationMeshGenerator::_parse_geometry(const Transform3D &p_navmesh_trans if (err == OK) { PackedVector3Array faces; - for (int j = 0; j < md.faces.size(); ++j) { - Geometry3D::MeshData::Face face = md.faces[j]; + for (uint32_t j = 0; j < md.faces.size(); ++j) { + const Geometry3D::MeshData::Face &face = md.faces[j]; - for (int k = 2; k < face.indices.size(); ++k) { + for (uint32_t k = 2; k < face.indices.size(); ++k) { faces.push_back(md.vertices[face.indices[0]]); faces.push_back(md.vertices[face.indices[k - 1]]); faces.push_back(md.vertices[face.indices[k]]); diff --git a/modules/noise/noise_texture_2d.cpp b/modules/noise/noise_texture_2d.cpp index 23d60c4866..8c785d7591 100644 --- a/modules/noise/noise_texture_2d.cpp +++ b/modules/noise/noise_texture_2d.cpp @@ -176,9 +176,7 @@ Ref<Image> NoiseTexture2D::_modulate_with_gradient(Ref<Image> p_image, Ref<Gradi int width = p_image->get_width(); int height = p_image->get_height(); - Ref<Image> new_image; - new_image.instantiate(); - new_image->create(width, height, false, Image::FORMAT_RGBA8); + Ref<Image> new_image = Image::create_empty(width, height, false, Image::FORMAT_RGBA8); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { diff --git a/modules/openxr/SCsub b/modules/openxr/SCsub index 5ac167ad98..b5978ab134 100644 --- a/modules/openxr/SCsub +++ b/modules/openxr/SCsub @@ -96,6 +96,7 @@ env_openxr.add_source_files(module_obj, "extensions/openxr_composition_layer_dep env_openxr.add_source_files(module_obj, "extensions/openxr_htc_vive_tracker_extension.cpp") env_openxr.add_source_files(module_obj, "extensions/openxr_hand_tracking_extension.cpp") env_openxr.add_source_files(module_obj, "extensions/openxr_fb_passthrough_extension_wrapper.cpp") +env_openxr.add_source_files(module_obj, "extensions/openxr_fb_display_refresh_rate_extension.cpp") env.modules_sources += module_obj diff --git a/modules/openxr/action_map/openxr_action_map.cpp b/modules/openxr/action_map/openxr_action_map.cpp index 185e44c29d..123f860ce9 100644 --- a/modules/openxr/action_map/openxr_action_map.cpp +++ b/modules/openxr/action_map/openxr_action_map.cpp @@ -224,7 +224,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our interaction profiles Ref<OpenXRInteractionProfile> profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/khr/simple_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -236,7 +236,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Vive controller profile profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -257,7 +257,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our WMR controller profile profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/microsoft/motion_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -280,7 +280,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Meta touch controller profile profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/oculus/touch_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -305,7 +305,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Valve index controller profile profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/valve/index_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -333,7 +333,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our HP MR controller profile profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/hp/mixed_reality_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -356,7 +356,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Samsung Odyssey controller profile, // Note that this controller is only identified specifically on WMR, on SteamVR this is identified as a normal WMR controller. profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/samsung/odyssey_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -379,7 +379,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Vive Cosmos controller profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_cosmos_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -403,7 +403,7 @@ void OpenXRActionMap::create_default_action_sets() { // Note, Vive Focus 3 currently is not yet supported as a stand alone device // however HTC currently has a beta OpenXR runtime in testing we may support in the near future profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/htc/vive_focus3_controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); @@ -427,7 +427,7 @@ void OpenXRActionMap::create_default_action_sets() { // Create our Huawei controller profile = OpenXRInteractionProfile::new_profile("/interaction_profiles/huawei/controller"); - profile->add_new_binding(default_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); + profile->add_new_binding(default_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(aim_pose, "/user/hand/left/input/aim/pose,/user/hand/right/input/aim/pose"); profile->add_new_binding(grip_pose, "/user/hand/left/input/grip/pose,/user/hand/right/input/grip/pose"); profile->add_new_binding(palm_pose, "/user/hand/left/input/palm_ext/pose,/user/hand/right/input/palm_ext/pose"); diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 25bf496de9..f089fd066e 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -10,6 +10,19 @@ <tutorials> <link title="Setting up XR">$DOCS_URL/tutorials/xr/setting_up_xr.html</link> </tutorials> + <methods> + <method name="get_available_display_refresh_rates" qualifiers="const"> + <return type="Array" /> + <description> + Returns display refresh rates supported by the current HMD. Only returned if this feature is supported by the OpenXR runtime and after the interface has been initialized. + </description> + </method> + </methods> + <members> + <member name="display_refresh_rate" type="float" setter="set_display_refresh_rate" getter="get_display_refresh_rate" default="0.0"> + The display refresh rate for the current HMD. Only functional if this feature is supported by the OpenXR runtime and after the interface has been initialized. + </member> + </members> <signals> <signal name="pose_recentered"> <description> diff --git a/modules/openxr/editor/openxr_action_map_editor.cpp b/modules/openxr/editor/openxr_action_map_editor.cpp index 51c402d746..b03402e80d 100644 --- a/modules/openxr/editor/openxr_action_map_editor.cpp +++ b/modules/openxr/editor/openxr_action_map_editor.cpp @@ -128,7 +128,7 @@ void OpenXRActionMapEditor::_update_interaction_profiles() { interaction_profiles.remove_at(0); tabs->remove_child(interaction_profile); - interaction_profile->queue_delete(); + interaction_profile->queue_free(); } // in with the new... @@ -205,7 +205,7 @@ void OpenXRActionMapEditor::_on_remove_action_set(Object *p_action_set_editor) { action_map->remove_action_set(action_set); actionsets_vb->remove_child(action_set_editor); - action_set_editor->queue_delete(); + action_set_editor->queue_free(); } void OpenXRActionMapEditor::_on_action_removed() { @@ -290,7 +290,7 @@ void OpenXRActionMapEditor::_on_tab_button_pressed(int p_tab) { action_map->remove_interaction_profile(interaction_profile); tabs->remove_child(profile_editor); - profile_editor->queue_delete(); + profile_editor->queue_free(); } void OpenXRActionMapEditor::open_action_map(String p_path) { diff --git a/modules/openxr/editor/openxr_action_set_editor.cpp b/modules/openxr/editor/openxr_action_set_editor.cpp index 804808a6b9..3869146e8e 100644 --- a/modules/openxr/editor/openxr_action_set_editor.cpp +++ b/modules/openxr/editor/openxr_action_set_editor.cpp @@ -140,7 +140,7 @@ void OpenXRActionSetEditor::_on_remove_action(Object *p_action_editor) { // And remove it.... action_map->remove_action(action->get_name_with_set()); // remove it from the set and any interaction profile it relates to actions_vb->remove_child(action_editor); - action_editor->queue_delete(); + action_editor->queue_free(); // Let action map editor know so we can update our interaction profiles emit_signal("action_removed"); diff --git a/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp new file mode 100644 index 0000000000..c0bbaea5b4 --- /dev/null +++ b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp @@ -0,0 +1,123 @@ +/*************************************************************************/ +/* openxr_fb_display_refresh_rate_extension.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "openxr_fb_display_refresh_rate_extension.h" + +OpenXRDisplayRefreshRateExtension *OpenXRDisplayRefreshRateExtension::singleton = nullptr; + +OpenXRDisplayRefreshRateExtension *OpenXRDisplayRefreshRateExtension::get_singleton() { + return singleton; +} + +OpenXRDisplayRefreshRateExtension::OpenXRDisplayRefreshRateExtension(OpenXRAPI *p_openxr_api) : + OpenXRExtensionWrapper(p_openxr_api) { + singleton = this; + + // Extensions we use for our hand tracking. + request_extensions[XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME] = &display_refresh_rate_ext; +} + +OpenXRDisplayRefreshRateExtension::~OpenXRDisplayRefreshRateExtension() { + display_refresh_rate_ext = false; +} + +void OpenXRDisplayRefreshRateExtension::on_instance_created(const XrInstance p_instance) { + if (display_refresh_rate_ext) { + EXT_INIT_XR_FUNC(xrEnumerateDisplayRefreshRatesFB); + EXT_INIT_XR_FUNC(xrGetDisplayRefreshRateFB); + EXT_INIT_XR_FUNC(xrRequestDisplayRefreshRateFB); + } +} + +void OpenXRDisplayRefreshRateExtension::on_instance_destroyed() { + display_refresh_rate_ext = false; +} + +float OpenXRDisplayRefreshRateExtension::get_refresh_rate() const { + float refresh_rate = 0.0; + + if (display_refresh_rate_ext) { + float rate; + XrResult result = xrGetDisplayRefreshRateFB(openxr_api->get_session(), &rate); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to obtain refresh rate [", openxr_api->get_error_string(result), "]"); + } else { + refresh_rate = rate; + } + } + + return refresh_rate; +} + +void OpenXRDisplayRefreshRateExtension::set_refresh_rate(float p_refresh_rate) { + if (display_refresh_rate_ext) { + XrResult result = xrRequestDisplayRefreshRateFB(openxr_api->get_session(), p_refresh_rate); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to set refresh rate [", openxr_api->get_error_string(result), "]"); + } + } +} + +Array OpenXRDisplayRefreshRateExtension::get_available_refresh_rates() const { + Array arr; + XrResult result; + + if (display_refresh_rate_ext) { + uint32_t display_refresh_rate_count = 0; + result = xrEnumerateDisplayRefreshRatesFB(openxr_api->get_session(), 0, &display_refresh_rate_count, nullptr); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to obtain refresh rates count [", openxr_api->get_error_string(result), "]"); + } + + if (display_refresh_rate_count > 0) { + float *display_refresh_rates = (float *)memalloc(sizeof(float) * display_refresh_rate_count); + if (display_refresh_rates == nullptr) { + print_line("OpenXR: Failed to obtain refresh rates memory buffer [", openxr_api->get_error_string(result), "]"); + return arr; + } + + result = xrEnumerateDisplayRefreshRatesFB(openxr_api->get_session(), display_refresh_rate_count, &display_refresh_rate_count, display_refresh_rates); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to obtain refresh rates count [", openxr_api->get_error_string(result), "]"); + memfree(display_refresh_rates); + return arr; + } + + for (uint32_t i = 0; i < display_refresh_rate_count; i++) { + float refresh_rate = display_refresh_rates[i]; + arr.push_back(Variant(refresh_rate)); + } + + memfree(display_refresh_rates); + } + } + + return arr; +} diff --git a/modules/websocket/websocket_client.h b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h index e747aee4e4..dcd52fe4d1 100644 --- a/modules/websocket/websocket_client.h +++ b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* websocket_client.h */ +/* openxr_fb_display_refresh_rate_extension.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,48 +28,43 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef WEBSOCKET_CLIENT_H -#define WEBSOCKET_CLIENT_H +#ifndef OPENXR_FB_DISPLAY_REFRESH_RATE_EXTENSION_H +#define OPENXR_FB_DISPLAY_REFRESH_RATE_EXTENSION_H -#include "core/crypto/crypto.h" -#include "core/error/error_list.h" -#include "websocket_multiplayer_peer.h" -#include "websocket_peer.h" +// This extension gives us access to the possible display refresh rates +// supported by the HMD. +// While this is an FB extension it has been adopted by most runtimes and +// will likely become core in the near future. -class WebSocketClient : public WebSocketMultiplayerPeer { - GDCLASS(WebSocketClient, WebSocketMultiplayerPeer); - GDCICLASS(WebSocketClient); +#include "../openxr_api.h" +#include "../util.h" -protected: - Ref<WebSocketPeer> _peer; - bool verify_tls = true; - Ref<X509Certificate> tls_cert; - - static void _bind_methods(); +#include "openxr_extension_wrapper.h" +class OpenXRDisplayRefreshRateExtension : public OpenXRExtensionWrapper { public: - Error connect_to_url(String p_url, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false, const Vector<String> p_custom_headers = Vector<String>()); + static OpenXRDisplayRefreshRateExtension *get_singleton(); + + OpenXRDisplayRefreshRateExtension(OpenXRAPI *p_openxr_api); + virtual ~OpenXRDisplayRefreshRateExtension() override; + + virtual void on_instance_created(const XrInstance p_instance) override; + virtual void on_instance_destroyed() override; - void set_verify_tls_enabled(bool p_verify_tls); - bool is_verify_tls_enabled() const; - Ref<X509Certificate> get_trusted_tls_certificate() const; - void set_trusted_tls_certificate(Ref<X509Certificate> p_cert); + float get_refresh_rate() const; + void set_refresh_rate(float p_refresh_rate); - virtual Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) = 0; - virtual void disconnect_from_host(int p_code = 1000, String p_reason = "") = 0; - virtual IPAddress get_connected_host() const = 0; - virtual uint16_t get_connected_port() const = 0; + Array get_available_refresh_rates() const; - virtual bool is_server() const override; +private: + static OpenXRDisplayRefreshRateExtension *singleton; - void _on_peer_packet(); - void _on_connect(String p_protocol); - void _on_close_request(int p_code, String p_reason); - void _on_disconnect(bool p_was_clean); - void _on_error(); + bool display_refresh_rate_ext = false; - WebSocketClient(); - ~WebSocketClient(); + // OpenXR API call wrappers + EXT_PROTO_XRRESULT_FUNC4(xrEnumerateDisplayRefreshRatesFB, (XrSession), session, (uint32_t), displayRefreshRateCapacityInput, (uint32_t *), displayRefreshRateCountOutput, (float *), displayRefreshRates); + EXT_PROTO_XRRESULT_FUNC2(xrGetDisplayRefreshRateFB, (XrSession), session, (float *), display_refresh_rate); + EXT_PROTO_XRRESULT_FUNC2(xrRequestDisplayRefreshRateFB, (XrSession), session, (float), display_refresh_rate); }; -#endif // WEBSOCKET_CLIENT_H +#endif // OPENXR_FB_DISPLAY_REFRESH_RATE_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp index 88cc7c061c..4d996e6283 100644 --- a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp +++ b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp @@ -69,6 +69,34 @@ bool OpenXRHTCViveTrackerExtension::on_event_polled(const XrEventDataBuffer &eve bool OpenXRHTCViveTrackerExtension::is_path_supported(const String &p_path) { if (p_path == "/interaction_profiles/htc/vive_tracker_htcx") { return available; + } else if (p_path == "/user/vive_tracker_htcx/role/handheld_object") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/left_foot") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/right_foot") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/left_shoulder") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/right_shoulder") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/left_elbow") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/right_elbow") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/left_knee") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/right_knee") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/waist") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/chest") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/chest") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/camera") { + return available; + } else if (p_path == "/user/vive_tracker_htcx/role/keyboard") { + return available; } // Not a path under this extensions control, so we return true; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 8a67462613..1ff1dac512 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -50,6 +50,7 @@ #endif #include "extensions/openxr_composition_layer_depth_extension.h" +#include "extensions/openxr_fb_display_refresh_rate_extension.h" #include "extensions/openxr_fb_passthrough_extension_wrapper.h" #include "extensions/openxr_hand_tracking_extension.h" #include "extensions/openxr_htc_vive_tracker_extension.h" @@ -443,12 +444,12 @@ bool OpenXRAPI::load_supported_view_configuration_views(XrViewConfigurationType for (uint32_t i = 0; i < view_count; i++) { print_verbose("OpenXR: Found supported view configuration view"); - print_verbose(String(" - width: ") + view_configuration_views[i].maxImageRectWidth); - print_verbose(String(" - height: ") + view_configuration_views[i].maxImageRectHeight); - print_verbose(String(" - sample count: ") + view_configuration_views[i].maxSwapchainSampleCount); - print_verbose(String(" - recommended render width: ") + view_configuration_views[i].recommendedImageRectWidth); - print_verbose(String(" - recommended render height: ") + view_configuration_views[i].recommendedImageRectHeight); - print_verbose(String(" - recommended render sample count: ") + view_configuration_views[i].recommendedSwapchainSampleCount); + print_verbose(String(" - width: ") + itos(view_configuration_views[i].maxImageRectWidth)); + print_verbose(String(" - height: ") + itos(view_configuration_views[i].maxImageRectHeight)); + print_verbose(String(" - sample count: ") + itos(view_configuration_views[i].maxSwapchainSampleCount)); + print_verbose(String(" - recommended render width: ") + itos(view_configuration_views[i].recommendedImageRectWidth)); + print_verbose(String(" - recommended render height: ") + itos(view_configuration_views[i].recommendedImageRectHeight)); + print_verbose(String(" - recommended render sample count: ") + itos(view_configuration_views[i].recommendedSwapchainSampleCount)); } return true; @@ -1748,6 +1749,31 @@ void OpenXRAPI::end_frame() { } } +float OpenXRAPI::get_display_refresh_rate() const { + OpenXRDisplayRefreshRateExtension *drrext = OpenXRDisplayRefreshRateExtension::get_singleton(); + if (drrext) { + return drrext->get_refresh_rate(); + } + + return 0.0; +} + +void OpenXRAPI::set_display_refresh_rate(float p_refresh_rate) { + OpenXRDisplayRefreshRateExtension *drrext = OpenXRDisplayRefreshRateExtension::get_singleton(); + if (drrext != nullptr) { + drrext->set_refresh_rate(p_refresh_rate); + } +} + +Array OpenXRAPI::get_available_display_refresh_rates() const { + OpenXRDisplayRefreshRateExtension *drrext = OpenXRDisplayRefreshRateExtension::get_singleton(); + if (drrext != nullptr) { + return drrext->get_available_refresh_rates(); + } + + return Array(); +} + OpenXRAPI::OpenXRAPI() { // OpenXRAPI is only constructed if OpenXR is enabled. singleton = this; @@ -1817,6 +1843,7 @@ OpenXRAPI::OpenXRAPI() { register_extension_wrapper(memnew(OpenXRHTCViveTrackerExtension(this))); register_extension_wrapper(memnew(OpenXRHandTrackingExtension(this))); register_extension_wrapper(memnew(OpenXRFbPassthroughExtensionWrapper(this))); + register_extension_wrapper(memnew(OpenXRDisplayRefreshRateExtension(this))); } OpenXRAPI::~OpenXRAPI() { diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index bd69432dcb..5dce749351 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -84,8 +84,6 @@ private: bool ext_vive_focus3_available = false; bool ext_huawei_controller_available = false; - bool is_path_supported(const String &p_path); - // composition layer providers Vector<OpenXRCompositionLayerProvider *> composition_layer_providers; @@ -302,6 +300,7 @@ public: void parse_velocities(const XrSpaceVelocity &p_velocity, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity); bool xr_result(XrResult result, const char *format, Array args = Array()) const; + bool is_path_supported(const String &p_path); static bool openxr_is_enabled(bool p_check_run_in_editor = true); static OpenXRAPI *get_singleton(); @@ -336,6 +335,11 @@ public: void post_draw_viewport(RID p_render_target); void end_frame(); + // Display refresh rate + float get_display_refresh_rate() const; + void set_display_refresh_rate(float p_refresh_rate); + Array get_available_display_refresh_rates() const; + // action map String get_default_action_map_resource_name(); diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 68414ae84e..bdf437b0b7 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -41,6 +41,13 @@ void OpenXRInterface::_bind_methods() { ADD_SIGNAL(MethodInfo("session_focussed")); ADD_SIGNAL(MethodInfo("session_visible")); ADD_SIGNAL(MethodInfo("pose_recentered")); + + // Display refresh rate + ClassDB::bind_method(D_METHOD("get_display_refresh_rate"), &OpenXRInterface::get_display_refresh_rate); + ClassDB::bind_method(D_METHOD("set_display_refresh_rate", "refresh_rate"), &OpenXRInterface::set_display_refresh_rate); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "display_refresh_rate"), "set_display_refresh_rate", "get_display_refresh_rate"); + + ClassDB::bind_method(D_METHOD("get_available_display_refresh_rates"), &OpenXRInterface::get_available_display_refresh_rates); } StringName OpenXRInterface::get_name() const { @@ -147,24 +154,25 @@ void OpenXRInterface::_load_action_map() { Ref<OpenXRAction> xr_action = actions[j]; PackedStringArray toplevel_paths = xr_action->get_toplevel_paths(); - Vector<Tracker *> trackers_new; + Vector<Tracker *> trackers_for_action; for (int k = 0; k < toplevel_paths.size(); k++) { - Tracker *tracker = find_tracker(toplevel_paths[k], true); - if (tracker) { - trackers_new.push_back(tracker); + // Only check for our tracker if our path is supported. + if (openxr_api->is_path_supported(toplevel_paths[k])) { + Tracker *tracker = find_tracker(toplevel_paths[k], true); + if (tracker) { + trackers_for_action.push_back(tracker); + } } } - Action *action = create_action(action_set, xr_action->get_name(), xr_action->get_localized_name(), xr_action->get_action_type(), trackers); - if (action) { - // we link our actions back to our trackers so we know which actions to check when we're processing our trackers - for (int t = 0; t < trackers_new.size(); t++) { - link_action_to_tracker(trackers_new[t], action); + // Only add our action if we have atleast one valid toplevel path + if (trackers_for_action.size() > 0) { + Action *action = create_action(action_set, xr_action->get_name(), xr_action->get_localized_name(), xr_action->get_action_type(), trackers_for_action); + if (action) { + // add this to our map for creating our interaction profiles + xr_actions[xr_action] = action; } - - // add this to our map for creating our interaction profiles - xr_actions[xr_action] = action; } } } @@ -282,6 +290,13 @@ OpenXRInterface::Action *OpenXRInterface::create_action(ActionSet *p_action_set, action->action_rid = openxr_api->action_create(p_action_set->action_set_rid, p_action_name, p_localized_name, p_action_type, tracker_rids); p_action_set->actions.push_back(action); + // we link our actions back to our trackers so we know which actions to check when we're processing our trackers + for (int i = 0; i < p_trackers.size(); i++) { + if (p_trackers[i]->actions.find(action) == -1) { + p_trackers[i]->actions.push_back(action); + } + } + return action; } @@ -330,6 +345,8 @@ OpenXRInterface::Tracker *OpenXRInterface::find_tracker(const String &p_tracker_ return nullptr; } + ERR_FAIL_COND_V(!openxr_api->is_path_supported(p_tracker_name), nullptr); + // Create our RID RID tracker_rid = openxr_api->tracker_create(p_tracker_name); ERR_FAIL_COND_V(tracker_rid.is_null(), nullptr); @@ -389,12 +406,6 @@ void OpenXRInterface::tracker_profile_changed(RID p_tracker, RID p_interaction_p } } -void OpenXRInterface::link_action_to_tracker(Tracker *p_tracker, Action *p_action) { - if (p_tracker->actions.find(p_action) == -1) { - p_tracker->actions.push_back(p_action); - } -} - void OpenXRInterface::handle_tracker(Tracker *p_tracker) { ERR_FAIL_NULL(openxr_api); ERR_FAIL_COND(p_tracker->positional_tracker.is_null()); @@ -447,9 +458,18 @@ void OpenXRInterface::handle_tracker(Tracker *p_tracker) { void OpenXRInterface::trigger_haptic_pulse(const String &p_action_name, const StringName &p_tracker_name, double p_frequency, double p_amplitude, double p_duration_sec, double p_delay_sec) { ERR_FAIL_NULL(openxr_api); + Action *action = find_action(p_action_name); ERR_FAIL_NULL(action); - Tracker *tracker = find_tracker(p_tracker_name); + + // We need to map our tracker name to our OpenXR name for our inbuild names. + String tracker_name = p_tracker_name; + if (tracker_name == "left_hand") { + tracker_name = "/user/hand/left"; + } else if (tracker_name == "right_hand") { + tracker_name = "/user/hand/right"; + } + Tracker *tracker = find_tracker(tracker_name); ERR_FAIL_NULL(tracker); // TODO OpenXR does not support delay, so we may need to add support for that somehow... @@ -571,6 +591,36 @@ bool OpenXRInterface::set_play_area_mode(XRInterface::PlayAreaMode p_mode) { return false; } +float OpenXRInterface::get_display_refresh_rate() const { + if (openxr_api == nullptr) { + return 0.0; + } else if (!openxr_api->is_initialized()) { + return 0.0; + } else { + return openxr_api->get_display_refresh_rate(); + } +} + +void OpenXRInterface::set_display_refresh_rate(float p_refresh_rate) { + if (openxr_api == nullptr) { + return; + } else if (!openxr_api->is_initialized()) { + return; + } else { + openxr_api->set_display_refresh_rate(p_refresh_rate); + } +} + +Array OpenXRInterface::get_available_display_refresh_rates() const { + if (openxr_api == nullptr) { + return Array(); + } else if (!openxr_api->is_initialized()) { + return Array(); + } else { + return openxr_api->get_available_display_refresh_rates(); + } +} + Size2 OpenXRInterface::get_render_target_size() { if (openxr_api == nullptr) { return Size2(); diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index 72935b039c..454612346f 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -91,7 +91,6 @@ private: void free_actions(ActionSet *p_action_set); Tracker *find_tracker(const String &p_tracker_name, bool p_create = false); - void link_action_to_tracker(Tracker *p_tracker, Action *p_action); void handle_tracker(Tracker *p_tracker); void free_trackers(); @@ -120,6 +119,10 @@ public: virtual XRInterface::PlayAreaMode get_play_area_mode() const override; virtual bool set_play_area_mode(XRInterface::PlayAreaMode p_mode) override; + float get_display_refresh_rate() const; + void set_display_refresh_rate(float p_refresh_rate); + Array get_available_display_refresh_rates() const; + virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; diff --git a/modules/regex/doc_classes/RegEx.xml b/modules/regex/doc_classes/RegEx.xml index 56404f796c..2abfc93722 100644 --- a/modules/regex/doc_classes/RegEx.xml +++ b/modules/regex/doc_classes/RegEx.xml @@ -4,7 +4,7 @@ Class for searching text for patterns using regular expressions. </brief_description> <description> - A regular expression (or regex) is a compact language that can be used to recognise strings that follow a specific pattern, such as URLs, email addresses, complete sentences, etc. For instance, a regex of [code]ab[0-9][/code] would find any string that is [code]ab[/code] followed by any number from [code]0[/code] to [code]9[/code]. For a more in-depth look, you can easily find various tutorials and detailed explanations on the Internet. + A regular expression (or regex) is a compact language that can be used to recognise strings that follow a specific pattern, such as URLs, email addresses, complete sentences, etc. For example, a regex of [code]ab[0-9][/code] would find any string that is [code]ab[/code] followed by any number from [code]0[/code] to [code]9[/code]. For a more in-depth look, you can easily find various tutorials and detailed explanations on the Internet. To begin, the RegEx object needs to be compiled with the search pattern using [method compile] before it can be used. [codeblock] var regex = RegEx.new() diff --git a/modules/squish/image_decompress_squish.cpp b/modules/squish/image_decompress_squish.cpp index 3a810e5259..2abd9a0c69 100644 --- a/modules/squish/image_decompress_squish.cpp +++ b/modules/squish/image_decompress_squish.cpp @@ -70,7 +70,7 @@ void image_decompress_squish(Image *p_image) { h >>= 1; } - p_image->create(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); + p_image->set_data(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); if (p_image->get_format() == Image::FORMAT_DXT5_RA_AS_RG) { p_image->convert_ra_rgba8_to_rg(); diff --git a/modules/svg/SCsub b/modules/svg/SCsub index 93262f4f87..ae3e1bdedb 100644 --- a/modules/svg/SCsub +++ b/modules/svg/SCsub @@ -11,6 +11,16 @@ thirdparty_obj = [] thirdparty_dir = "#thirdparty/thorvg/" thirdparty_sources = [ + "src/lib/sw_engine/tvgSwFill.cpp", + "src/lib/sw_engine/tvgSwImage.cpp", + "src/lib/sw_engine/tvgSwMath.cpp", + "src/lib/sw_engine/tvgSwMemPool.cpp", + "src/lib/sw_engine/tvgSwRaster.cpp", + "src/lib/sw_engine/tvgSwRenderer.cpp", + "src/lib/sw_engine/tvgSwRle.cpp", + "src/lib/sw_engine/tvgSwShape.cpp", + "src/lib/sw_engine/tvgSwStroke.cpp", + "src/lib/tvgAccessor.cpp", "src/lib/tvgBezier.cpp", "src/lib/tvgCanvas.cpp", "src/lib/tvgFill.cpp", @@ -28,27 +38,18 @@ thirdparty_sources = [ "src/lib/tvgShape.cpp", "src/lib/tvgSwCanvas.cpp", "src/lib/tvgTaskScheduler.cpp", + "src/loaders/external_png/tvgPngLoader.cpp", + "src/loaders/jpg/tvgJpgd.cpp", + "src/loaders/jpg/tvgJpgLoader.cpp", "src/loaders/raw/tvgRawLoader.cpp", - "src/loaders/svg/tvgXmlParser.cpp", - "src/loaders/svg/tvgSvgUtil.cpp", - "src/loaders/svg/tvgSvgSceneBuilder.cpp", - "src/loaders/svg/tvgSvgPath.cpp", - "src/loaders/svg/tvgSvgLoader.cpp", "src/loaders/svg/tvgSvgCssStyle.cpp", + "src/loaders/svg/tvgSvgLoader.cpp", + "src/loaders/svg/tvgSvgPath.cpp", + "src/loaders/svg/tvgSvgSceneBuilder.cpp", + "src/loaders/svg/tvgSvgUtil.cpp", + "src/loaders/svg/tvgXmlParser.cpp", "src/loaders/tvg/tvgTvgBinInterpreter.cpp", "src/loaders/tvg/tvgTvgLoader.cpp", - "src/loaders/jpg/tvgJpgLoader.cpp", - "src/loaders/jpg/tvgJpgd.cpp", - "src/loaders/external_png/tvgPngLoader.cpp", - "src/lib/sw_engine/tvgSwFill.cpp", - "src/lib/sw_engine/tvgSwImage.cpp", - "src/lib/sw_engine/tvgSwMath.cpp", - "src/lib/sw_engine/tvgSwMemPool.cpp", - "src/lib/sw_engine/tvgSwRaster.cpp", - "src/lib/sw_engine/tvgSwRenderer.cpp", - "src/lib/sw_engine/tvgSwRle.cpp", - "src/lib/sw_engine/tvgSwShape.cpp", - "src/lib/sw_engine/tvgSwStroke.cpp", "src/savers/tvg/tvgTvgSaver.cpp", ] @@ -62,14 +63,18 @@ env_thirdparty.Prepend( CPPPATH=[ thirdparty_dir + "src/lib", thirdparty_dir + "src/lib/sw_engine", + thirdparty_dir + "src/loaders/external_png", + thirdparty_dir + "src/loaders/jpg", thirdparty_dir + "src/loaders/raw", thirdparty_dir + "src/loaders/svg", - thirdparty_dir + "src/loaders/jpg", - thirdparty_dir + "src/loaders/png", thirdparty_dir + "src/loaders/tvg", thirdparty_dir + "src/savers/tvg", ] ) +# Also requires libpng headers +if env["builtin_libpng"]: + env_thirdparty.Prepend(CPPPATH=["#thirdparty/libpng"]) + env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources) env.modules_sources += thirdparty_obj diff --git a/modules/svg/image_loader_svg.cpp b/modules/svg/image_loader_svg.cpp index f43f2784c7..b8c412a201 100644 --- a/modules/svg/image_loader_svg.cpp +++ b/modules/svg/image_loader_svg.cpp @@ -135,7 +135,7 @@ void ImageLoaderSVG::create_image_from_string(Ref<Image> p_image, String p_strin res = sw_canvas->clear(true); memfree(buffer); - p_image->create(width, height, false, Image::FORMAT_RGBA8, image); + p_image->set_data(width, height, false, Image::FORMAT_RGBA8, image); } void ImageLoaderSVG::get_recognized_extensions(List<String> *p_extensions) const { diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index c9b0fa7dd5..0929d3a2b0 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -810,12 +810,12 @@ _FORCE_INLINE_ TextServerAdvanced::FontTexturePosition TextServerAdvanced::find_ ret.y = 0x7fffffff; ret.x = 0; + const int *ct_offsets_ptr = ct.offsets.ptr(); for (int j = 0; j < ct.texture_w - mw; j++) { int max_y = 0; - for (int k = j; k < j + mw; k++) { - int y = ct.offsets[k]; + int y = ct_offsets_ptr[k]; if (y > max_y) { max_y = y; } @@ -1393,7 +1393,10 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f int error = 0; if (!ft_library) { error = FT_Init_FreeType(&ft_library); - ERR_FAIL_COND_V_MSG(error != 0, false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + if (error != 0) { + memdelete(fd); + ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + } } memset(&fd->stream, 0, sizeof(FT_StreamRec)); @@ -1422,6 +1425,7 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f if (error) { FT_Done_Face(fd->face); fd->face = nullptr; + memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); } @@ -1835,6 +1839,7 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f FT_Done_MM_Var(ft_library, amaster); } #else + memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); #endif } else { @@ -2494,9 +2499,7 @@ void TextServerAdvanced::_font_set_texture_image(const RID &p_font_rid, const Ve tex.texture_h = p_image->get_height(); tex.format = p_image->get_format(); - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -2515,11 +2518,7 @@ Ref<Image> TextServerAdvanced::_font_get_texture_image(const RID &p_font_rid, co ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); const FontTexture &tex = fd->cache[size]->textures[p_texture_index]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - - return img; + return Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); } void TextServerAdvanced::_font_set_texture_offsets(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const PackedInt32Array &p_offset) { @@ -2853,9 +2852,7 @@ RID TextServerAdvanced::_font_get_glyph_texture_rid(const RID &p_font_rid, const if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -2901,9 +2898,7 @@ Size2 TextServerAdvanced::_font_get_glyph_texture_size(const RID &p_font_rid, co if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -3248,9 +3243,7 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -3340,9 +3333,7 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 518c877baa..4a46e17868 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -233,12 +233,13 @@ _FORCE_INLINE_ TextServerFallback::FontTexturePosition TextServerFallback::find_ ret.y = 0x7fffffff; ret.x = 0; + const int *ct_offsets_ptr = ct.offsets.ptr(); for (int j = 0; j < ct.texture_w - mw; j++) { int max_y = 0; for (int k = j; k < j + mw; k++) { - int y = ct.offsets[k]; + int y = ct_offsets_ptr[k]; if (y > max_y) { max_y = y; } @@ -818,7 +819,10 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f int error = 0; if (!ft_library) { error = FT_Init_FreeType(&ft_library); - ERR_FAIL_COND_V_MSG(error != 0, false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + if (error != 0) { + memdelete(fd); + ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + } } memset(&fd->stream, 0, sizeof(FT_StreamRec)); @@ -847,6 +851,7 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f if (error) { FT_Done_Face(fd->face); fd->face = nullptr; + memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); } @@ -945,6 +950,7 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f FT_Done_MM_Var(ft_library, amaster); } #else + memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); #endif } @@ -1588,9 +1594,7 @@ void TextServerFallback::_font_set_texture_image(const RID &p_font_rid, const Ve tex.texture_h = p_image->get_height(); tex.format = p_image->get_format(); - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -1609,11 +1613,7 @@ Ref<Image> TextServerFallback::_font_get_texture_image(const RID &p_font_rid, co ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); const FontTexture &tex = fd->cache[size]->textures[p_texture_index]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - - return img; + return Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); } void TextServerFallback::_font_set_texture_offsets(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const PackedInt32Array &p_offset) { @@ -1933,9 +1933,7 @@ RID TextServerFallback::_font_get_glyph_texture_rid(const RID &p_font_rid, const if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -1981,9 +1979,7 @@ Size2 TextServerFallback::_font_get_glyph_texture_size(const RID &p_font_rid, co if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -2310,9 +2306,7 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } @@ -2402,9 +2396,7 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { FontTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img; - img.instantiate(); - img->create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); if (fd->mipmaps) { img->generate_mipmaps(); } diff --git a/modules/tga/image_loader_tga.cpp b/modules/tga/image_loader_tga.cpp index aed95294e7..a6fc650414 100644 --- a/modules/tga/image_loader_tga.cpp +++ b/modules/tga/image_loader_tga.cpp @@ -246,7 +246,7 @@ Error ImageLoaderTGA::convert_to_image(Ref<Image> p_image, const uint8_t *p_buff } } - p_image->create(width, height, false, Image::FORMAT_RGBA8, image_data); + p_image->initialize_data(width, height, false, Image::FORMAT_RGBA8, image_data); return OK; } diff --git a/modules/theora/video_stream_theora.cpp b/modules/theora/video_stream_theora.cpp index 1284412cd8..69fb079970 100644 --- a/modules/theora/video_stream_theora.cpp +++ b/modules/theora/video_stream_theora.cpp @@ -336,9 +336,7 @@ void VideoStreamPlaybackTheora::set_file(const String &p_file) { size.x = w; size.y = h; - Ref<Image> img; - img.instantiate(); - img->create(w, h, false, Image::FORMAT_RGBA8); + Ref<Image> img = Image::create_empty(w, h, false, Image::FORMAT_RGBA8); texture->set_image(img); } else { diff --git a/modules/tinyexr/image_loader_tinyexr.cpp b/modules/tinyexr/image_loader_tinyexr.cpp index 5c43bfc8b7..c5aa110fe6 100644 --- a/modules/tinyexr/image_loader_tinyexr.cpp +++ b/modules/tinyexr/image_loader_tinyexr.cpp @@ -280,7 +280,7 @@ Error ImageLoaderTinyEXR::load_image(Ref<Image> p_image, Ref<FileAccess> f, BitF } } - p_image->create(exr_image.width, exr_image.height, false, format, imgdata); + p_image->set_data(exr_image.width, exr_image.height, false, format, imgdata); FreeEXRHeader(&exr_header); FreeEXRImage(&exr_image); diff --git a/modules/upnp/doc_classes/UPNP.xml b/modules/upnp/doc_classes/UPNP.xml index 4888dca822..d4054948f6 100644 --- a/modules/upnp/doc_classes/UPNP.xml +++ b/modules/upnp/doc_classes/UPNP.xml @@ -41,7 +41,7 @@ func _ready(): thread = Thread.new() - thread.start(self, "_upnp_setup", SERVER_PORT) + thread.start(_upnp_setup.bind(SERVER_PORT)) func _exit_tree(): # Wait for thread finish here to handle game exit while the thread is running. diff --git a/modules/webp/resource_saver_webp.cpp b/modules/webp/resource_saver_webp.cpp index bd71c2869a..56ff8097b4 100644 --- a/modules/webp/resource_saver_webp.cpp +++ b/modules/webp/resource_saver_webp.cpp @@ -38,8 +38,8 @@ Error ResourceSaverWebP::save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags) { Ref<ImageTexture> texture = p_resource; - ERR_FAIL_COND_V_MSG(!texture.is_valid(), ERR_INVALID_PARAMETER, "Can't save invalid texture as WEBP."); - ERR_FAIL_COND_V_MSG(!texture->get_width(), ERR_INVALID_PARAMETER, "Can't save empty texture as WEBP."); + ERR_FAIL_COND_V_MSG(!texture.is_valid(), ERR_INVALID_PARAMETER, "Can't save invalid texture as WebP."); + ERR_FAIL_COND_V_MSG(!texture->get_width(), ERR_INVALID_PARAMETER, "Can't save empty texture as WebP."); Ref<Image> img = texture->get_image(); @@ -52,7 +52,7 @@ Error ResourceSaverWebP::save_image(const String &p_path, const Ref<Image> &p_im Vector<uint8_t> buffer = save_image_to_buffer(p_img, p_lossy, p_quality); Error err; Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err); - ERR_FAIL_COND_V_MSG(err, err, vformat("Can't save WEBP at path: '%s'.", p_path)); + ERR_FAIL_COND_V_MSG(err, err, vformat("Can't save WebP at path: '%s'.", p_path)); const uint8_t *reader = buffer.ptr(); diff --git a/modules/webp/webp_common.cpp b/modules/webp/webp_common.cpp index 8657a98853..c6440f1796 100644 --- a/modules/webp/webp_common.cpp +++ b/modules/webp/webp_common.cpp @@ -139,7 +139,7 @@ Ref<Image> _webp_unpack(const Vector<uint8_t> &p_buffer) { ERR_FAIL_COND_V(r[0] != 'R' || r[1] != 'I' || r[2] != 'F' || r[3] != 'F' || r[8] != 'W' || r[9] != 'E' || r[10] != 'B' || r[11] != 'P', Ref<Image>()); WebPBitstreamFeatures features; if (WebPGetFeatures(r, size, &features) != VP8_STATUS_OK) { - ERR_FAIL_V_MSG(Ref<Image>(), "Error unpacking WEBP image."); + ERR_FAIL_V_MSG(Ref<Image>(), "Error unpacking WebP image."); } Vector<uint8_t> dst_image; @@ -183,7 +183,7 @@ Error webp_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p ERR_FAIL_COND_V_MSG(errdec, ERR_FILE_CORRUPT, "Failed decoding WebP image."); - p_image->create(features.width, features.height, false, features.has_alpha ? Image::FORMAT_RGBA8 : Image::FORMAT_RGB8, dst_image); + p_image->set_data(features.width, features.height, false, features.has_alpha ? Image::FORMAT_RGBA8 : Image::FORMAT_RGB8, dst_image); return OK; } diff --git a/modules/webrtc/doc_classes/WebRTCMultiplayerPeer.xml b/modules/webrtc/doc_classes/WebRTCMultiplayerPeer.xml index 927888fe21..0b42c6ed35 100644 --- a/modules/webrtc/doc_classes/WebRTCMultiplayerPeer.xml +++ b/modules/webrtc/doc_classes/WebRTCMultiplayerPeer.xml @@ -6,7 +6,7 @@ <description> This class constructs a full mesh of [WebRTCPeerConnection] (one connection for each peer) that can be used as a [member MultiplayerAPI.multiplayer_peer]. You can add each [WebRTCPeerConnection] via [method add_peer] or remove them via [method remove_peer]. Peers must be added in [constant WebRTCPeerConnection.STATE_NEW] state to allow it to create the appropriate channels. This class will not create offers nor set descriptions, it will only poll them, and notify connections and disconnections. - [signal MultiplayerPeer.connection_succeeded] and [signal MultiplayerPeer.server_disconnected] will not be emitted unless [code]server_compatibility[/code] is [code]true[/code] in [method initialize]. Beside that data transfer works like in a [MultiplayerPeer]. + [signal MultiplayerPeer.connection_succeeded] and [signal MultiplayerPeer.server_disconnected] will not be emitted unless the peer is created using [method create_client]. Beside that data transfer works like in a [MultiplayerPeer]. [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android. </description> <tutorials> @@ -28,6 +28,31 @@ Close all the add peer connections and channels, freeing all resources. </description> </method> + <method name="create_client"> + <return type="int" enum="Error" /> + <param index="0" name="peer_id" type="int" /> + <param index="1" name="channels_config" type="Array" default="[]" /> + <description> + Initialize the multiplayer peer as a client with the given [code]peer_id[/code] (must be between 2 and 2147483647). In this mode, you should only call [method add_peer] once and with [code]peer_id[/code] of [code]1[/code]. This mode enables [method MultiplayerPeer.is_server_relay_supported], allowing the upper [MultiplayerAPI] layer to perform peer exchange and packet relaying. + You can optionally specify a [code]channels_config[/code] array of [enum MultiplayerPeer.TransferMode] which will be used to create extra channels (WebRTC only supports one transfer mode per channel). + </description> + </method> + <method name="create_mesh"> + <return type="int" enum="Error" /> + <param index="0" name="peer_id" type="int" /> + <param index="1" name="channels_config" type="Array" default="[]" /> + <description> + Initialize the multiplayer peer as a mesh (i.e. all peers connect to each other) with the given [code]peer_id[/code] (must be between 1 and 2147483647). + </description> + </method> + <method name="create_server"> + <return type="int" enum="Error" /> + <param index="0" name="channels_config" type="Array" default="[]" /> + <description> + Initialize the multiplayer peer as a server (with unique ID of [code]1[/code]). This mode enables [method MultiplayerPeer.is_server_relay_supported], allowing the upper [MultiplayerAPI] layer to perform peer exchange and packet relaying. + You can optionally specify a [code]channels_config[/code] array of [enum MultiplayerPeer.TransferMode] which will be used to create extra channels (WebRTC only supports one transfer mode per channel). + </description> + </method> <method name="get_peer"> <return type="Dictionary" /> <param index="0" name="peer_id" type="int" /> @@ -48,18 +73,6 @@ Returns [code]true[/code] if the given [code]peer_id[/code] is in the peers map (it might not be connected though). </description> </method> - <method name="initialize"> - <return type="int" enum="Error" /> - <param index="0" name="peer_id" type="int" /> - <param index="1" name="server_compatibility" type="bool" default="false" /> - <param index="2" name="channels_config" type="Array" default="[]" /> - <description> - Initialize the multiplayer peer with the given [code]peer_id[/code] (must be between 1 and 2147483647). - If [code]server_compatibilty[/code] is [code]false[/code] (default), the multiplayer peer will be immediately in state [constant MultiplayerPeer.CONNECTION_CONNECTED] and [signal MultiplayerPeer.connection_succeeded] will not be emitted. - If [code]server_compatibilty[/code] is [code]true[/code] the peer will suppress all [signal MultiplayerPeer.peer_connected] signals until a peer with id [constant MultiplayerPeer.TARGET_PEER_SERVER] connects and then emit [signal MultiplayerPeer.connection_succeeded]. After that the signal [signal MultiplayerPeer.peer_connected] will be emitted for every already connected peer, and any new peer that might connect. If the server peer disconnects after that, signal [signal MultiplayerPeer.server_disconnected] will be emitted and state will become [constant MultiplayerPeer.CONNECTION_CONNECTED]. - You can optionally specify a [code]channels_config[/code] array of [enum MultiplayerPeer.TransferMode] which will be used to create extra channels (WebRTC only supports one transfer mode per channel). - </description> - </method> <method name="remove_peer"> <return type="void" /> <param index="0" name="peer_id" type="int" /> diff --git a/modules/webrtc/webrtc_multiplayer_peer.cpp b/modules/webrtc/webrtc_multiplayer_peer.cpp index e03b6b2473..5ea81d5a1b 100644 --- a/modules/webrtc/webrtc_multiplayer_peer.cpp +++ b/modules/webrtc/webrtc_multiplayer_peer.cpp @@ -34,7 +34,9 @@ #include "core/os/os.h" void WebRTCMultiplayerPeer::_bind_methods() { - ClassDB::bind_method(D_METHOD("initialize", "peer_id", "server_compatibility", "channels_config"), &WebRTCMultiplayerPeer::initialize, DEFVAL(false), DEFVAL(Array())); + ClassDB::bind_method(D_METHOD("create_server", "channels_config"), &WebRTCMultiplayerPeer::create_server, DEFVAL(Array())); + ClassDB::bind_method(D_METHOD("create_client", "peer_id", "channels_config"), &WebRTCMultiplayerPeer::create_client, DEFVAL(Array())); + ClassDB::bind_method(D_METHOD("create_mesh", "peer_id", "channels_config"), &WebRTCMultiplayerPeer::create_mesh, DEFVAL(Array())); ClassDB::bind_method(D_METHOD("add_peer", "peer", "peer_id", "unreliable_lifetime"), &WebRTCMultiplayerPeer::add_peer, DEFVAL(1)); ClassDB::bind_method(D_METHOD("remove_peer", "peer_id"), &WebRTCMultiplayerPeer::remove_peer); ClassDB::bind_method(D_METHOD("has_peer", "peer_id"), &WebRTCMultiplayerPeer::has_peer); @@ -52,6 +54,15 @@ int WebRTCMultiplayerPeer::get_packet_peer() const { return next_packet_peer; } +int WebRTCMultiplayerPeer::get_packet_channel() const { + return next_packet_channel < CH_RESERVED_MAX ? 0 : next_packet_channel - CH_RESERVED_MAX + 1; +} + +MultiplayerPeer::TransferMode WebRTCMultiplayerPeer::get_packet_mode() const { + ERR_FAIL_INDEX_V(next_packet_channel, channels_modes.size(), TRANSFER_MODE_RELIABLE); + return channels_modes[next_packet_channel]; +} + bool WebRTCMultiplayerPeer::is_server() const { return unique_id == TARGET_PEER_SERVER; } @@ -113,24 +124,14 @@ void WebRTCMultiplayerPeer::poll() { // Signal newly connected peers for (int &E : add) { // Already connected to server: simply notify new peer. - // NOTE: Mesh is always connected. - if (connection_status == CONNECTION_CONNECTED) { - emit_signal(SNAME("peer_connected"), E); - } - - // Server emulation mode suppresses peer_conencted until server connects. - if (server_compat && E == TARGET_PEER_SERVER) { + if (network_mode == MODE_CLIENT) { + ERR_CONTINUE(E != TARGET_PEER_SERVER); // Bug. // Server connected. connection_status = CONNECTION_CONNECTED; emit_signal(SNAME("peer_connected"), TARGET_PEER_SERVER); emit_signal(SNAME("connection_succeeded")); - // Notify of all previously connected peers - for (const KeyValue<int, Ref<ConnectedPeer>> &F : peer_map) { - if (F.key != 1 && F.value->connected) { - emit_signal(SNAME("peer_connected"), F.key); - } - } - break; // Because we already notified of all newly added peers. + } else { + emit_signal(SNAME("peer_connected"), E); } } // Fetch next packet @@ -150,11 +151,14 @@ void WebRTCMultiplayerPeer::_find_next_peer() { ++E; continue; } + int idx = 0; for (const Ref<WebRTCDataChannel> &F : E->value->channels) { if (F->get_available_packet_count()) { + next_packet_channel = idx; next_packet_peer = E->key; return; } + idx++; } ++E; } @@ -165,11 +169,14 @@ void WebRTCMultiplayerPeer::_find_next_peer() { ++E; continue; } + int idx = 0; for (const Ref<WebRTCDataChannel> &F : E->value->channels) { if (F->get_available_packet_count()) { + next_packet_channel = idx; next_packet_peer = E->key; return; } + idx++; } if (E->key == (int)next_packet_peer) { break; @@ -177,6 +184,7 @@ void WebRTCMultiplayerPeer::_find_next_peer() { ++E; } // No packet found + next_packet_channel = 0; next_packet_peer = 0; } @@ -184,11 +192,28 @@ MultiplayerPeer::ConnectionStatus WebRTCMultiplayerPeer::get_connection_status() return connection_status; } -Error WebRTCMultiplayerPeer::initialize(int p_self_id, bool p_server_compat, Array p_channels_config) { +Error WebRTCMultiplayerPeer::create_server(Array p_channels_config) { + return _initialize(1, MODE_SERVER, p_channels_config); +} + +Error WebRTCMultiplayerPeer::create_client(int p_self_id, Array p_channels_config) { + ERR_FAIL_COND_V_MSG(p_self_id == 1, ERR_INVALID_PARAMETER, "Clients cannot have ID 1."); + return _initialize(p_self_id, MODE_CLIENT, p_channels_config); +} + +Error WebRTCMultiplayerPeer::create_mesh(int p_self_id, Array p_channels_config) { + return _initialize(p_self_id, MODE_MESH, p_channels_config); +} + +Error WebRTCMultiplayerPeer::_initialize(int p_self_id, NetworkMode p_mode, Array p_channels_config) { ERR_FAIL_COND_V(p_self_id < 1 || p_self_id > ~(1 << 31), ERR_INVALID_PARAMETER); channels_config.clear(); + channels_modes.clear(); + channels_modes.push_back(TRANSFER_MODE_RELIABLE); + channels_modes.push_back(TRANSFER_MODE_UNRELIABLE_ORDERED); + channels_modes.push_back(TRANSFER_MODE_UNRELIABLE); for (int i = 0; i < p_channels_config.size(); i++) { - ERR_FAIL_COND_V_MSG(p_channels_config[i].get_type() != Variant::INT, ERR_INVALID_PARAMETER, "The 'channels_config' array must contain only enum values from 'MultiplayerPeer.Multiplayer::TransferMode'"); + ERR_FAIL_COND_V_MSG(p_channels_config[i].get_type() != Variant::INT, ERR_INVALID_PARAMETER, "The 'channels_config' array must contain only enum values from 'MultiplayerPeer.TransferMode'"); int mode = p_channels_config[i].operator int(); // Initialize data channel configurations. Dictionary cfg; @@ -207,16 +232,17 @@ Error WebRTCMultiplayerPeer::initialize(int p_self_id, bool p_server_compat, Arr case TRANSFER_MODE_RELIABLE: break; default: - ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat("The 'channels_config' array must contain only enum values from 'MultiplayerPeer.Multiplayer::TransferMode'. Got: %d", mode)); + ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat("The 'channels_config' array must contain only enum values from 'MultiplayerPeer.TransferMode'. Got: %d", mode)); } channels_config.push_back(cfg); + channels_modes.push_back((TransferMode)mode); } unique_id = p_self_id; - server_compat = p_server_compat; + network_mode = p_mode; // Mesh and server are always connected - if (!server_compat || p_self_id == 1) { + if (p_mode != MODE_CLIENT) { connection_status = CONNECTION_CONNECTED; } else { connection_status = CONNECTION_CONNECTING; @@ -224,6 +250,10 @@ Error WebRTCMultiplayerPeer::initialize(int p_self_id, bool p_server_compat, Arr return OK; } +bool WebRTCMultiplayerPeer::is_server_relay_supported() const { + return network_mode == MODE_SERVER || network_mode == MODE_CLIENT; +} + int WebRTCMultiplayerPeer::get_unique_id() const { ERR_FAIL_COND_V(connection_status == CONNECTION_DISCONNECTED, 1); return unique_id; @@ -261,7 +291,10 @@ Dictionary WebRTCMultiplayerPeer::get_peers() { } Error WebRTCMultiplayerPeer::add_peer(Ref<WebRTCPeerConnection> p_peer, int p_peer_id, int p_unreliable_lifetime) { - ERR_FAIL_COND_V(p_peer_id < 0 || p_peer_id > ~(1 << 31), ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(network_mode == MODE_NONE, ERR_UNCONFIGURED); + ERR_FAIL_COND_V(network_mode == MODE_CLIENT && p_peer_id != 1, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(network_mode == MODE_SERVER && p_peer_id == 1, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_peer_id < 1 || p_peer_id > ~(1 << 31), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(p_unreliable_lifetime < 0, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(is_refusing_new_connections(), ERR_UNAUTHORIZED); // Peer must be valid, and in new state (to create data channels) @@ -308,8 +341,12 @@ void WebRTCMultiplayerPeer::remove_peer(int p_peer_id) { if (peer->connected) { peer->connected = false; emit_signal(SNAME("peer_disconnected"), p_peer_id); - if (server_compat && p_peer_id == TARGET_PEER_SERVER) { - emit_signal(SNAME("server_disconnected")); + if (network_mode == MODE_CLIENT && p_peer_id == TARGET_PEER_SERVER) { + if (connection_status == CONNECTION_CONNECTING) { + emit_signal(SNAME("connection_failed")); + } else { + emit_signal(SNAME("server_disconnected")); + } connection_status = CONNECTION_DISCONNECTED; } } @@ -403,7 +440,9 @@ void WebRTCMultiplayerPeer::close() { channels_config.clear(); unique_id = 0; next_packet_peer = 0; + next_packet_channel = 0; target_peer = 0; + network_mode = MODE_NONE; connection_status = CONNECTION_DISCONNECTED; } diff --git a/modules/webrtc/webrtc_multiplayer_peer.h b/modules/webrtc/webrtc_multiplayer_peer.h index ea7c60036b..3f608200fd 100644 --- a/modules/webrtc/webrtc_multiplayer_peer.h +++ b/modules/webrtc/webrtc_multiplayer_peer.h @@ -48,6 +48,13 @@ private: CH_RESERVED_MAX = 3 }; + enum NetworkMode { + MODE_NONE, + MODE_SERVER, + MODE_CLIENT, + MODE_MESH, + }; + class ConnectedPeer : public RefCounted { public: Ref<WebRTCPeerConnection> connection; @@ -67,19 +74,25 @@ private: int client_count = 0; ConnectionStatus connection_status = CONNECTION_DISCONNECTED; int next_packet_peer = 0; - bool server_compat = false; + int next_packet_channel = 0; + NetworkMode network_mode = MODE_NONE; HashMap<int, Ref<ConnectedPeer>> peer_map; + List<TransferMode> channels_modes; List<Dictionary> channels_config; void _peer_to_dict(Ref<ConnectedPeer> p_connected_peer, Dictionary &r_dict); void _find_next_peer(); + Ref<ConnectedPeer> _get_next_peer(); + Error _initialize(int p_self_id, NetworkMode p_mode, Array p_channels_config = Array()); public: WebRTCMultiplayerPeer() {} ~WebRTCMultiplayerPeer(); - Error initialize(int p_self_id, bool p_server_compat = false, Array p_channels_config = Array()); + Error create_server(Array p_channels_config = Array()); + Error create_client(int p_self_id, Array p_channels_config = Array()); + Error create_mesh(int p_self_id, Array p_channels_config = Array()); Error add_peer(Ref<WebRTCPeerConnection> p_peer, int p_peer_id, int p_unreliable_lifetime = 1); void remove_peer(int p_peer_id); bool has_peer(int p_peer_id); @@ -98,8 +111,11 @@ public: int get_unique_id() const override; int get_packet_peer() const override; + int get_packet_channel() const override; + TransferMode get_packet_mode() const override; bool is_server() const override; + bool is_server_relay_supported() const override; void poll() override; diff --git a/modules/websocket/doc_classes/WebSocketClient.xml b/modules/websocket/doc_classes/WebSocketClient.xml deleted file mode 100644 index 1978d2e7c6..0000000000 --- a/modules/websocket/doc_classes/WebSocketClient.xml +++ /dev/null @@ -1,94 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="WebSocketClient" inherits="WebSocketMultiplayerPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> - <brief_description> - A WebSocket client implementation. - </brief_description> - <description> - This class implements a WebSocket client compatible with any RFC 6455-compliant WebSocket server. - This client can be optionally used as a multiplayer peer for the [MultiplayerAPI]. - After starting the client ([method connect_to_url]), you will need to [method MultiplayerPeer.poll] it at regular intervals (e.g. inside [method Node._process]). - You will receive appropriate signals when connecting, disconnecting, or when new data is available. - [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android. - </description> - <tutorials> - </tutorials> - <methods> - <method name="connect_to_url"> - <return type="int" enum="Error" /> - <param index="0" name="url" type="String" /> - <param index="1" name="protocols" type="PackedStringArray" default="PackedStringArray()" /> - <param index="2" name="gd_mp_api" type="bool" default="false" /> - <param index="3" name="custom_headers" type="PackedStringArray" default="PackedStringArray()" /> - <description> - Connects to the given URL requesting one of the given [code]protocols[/code] as sub-protocol. If the list empty (default), no sub-protocol will be requested. - If [code]true[/code] is passed as [code]gd_mp_api[/code], the client will behave like a multiplayer peer for the [MultiplayerAPI], connections to non-Godot servers will not work, and [signal data_received] will not be emitted. - If [code]false[/code] is passed instead (default), you must call [PacketPeer] functions ([code]put_packet[/code], [code]get_packet[/code], etc.) on the [WebSocketPeer] returned via [code]get_peer(1)[/code] and not on this object directly (e.g. [code]get_peer(1).put_packet(data)[/code]). - You can optionally pass a list of [code]custom_headers[/code] to be added to the handshake HTTP request. - [b]Note:[/b] To avoid mixed content warnings or errors in Web, you may have to use a [code]url[/code] that starts with [code]wss://[/code] (secure) instead of [code]ws://[/code]. When doing so, make sure to use the fully qualified domain name that matches the one defined in the server's TLS certificate. Do not connect directly via the IP address for [code]wss://[/code] connections, as it won't match with the TLS certificate. - [b]Note:[/b] Specifying [code]custom_headers[/code] is not supported in Web exports due to browsers' restrictions. - </description> - </method> - <method name="disconnect_from_host"> - <return type="void" /> - <param index="0" name="code" type="int" default="1000" /> - <param index="1" name="reason" type="String" default="""" /> - <description> - Disconnects this client from the connected host. See [method WebSocketPeer.close] for more information. - </description> - </method> - <method name="get_connected_host" qualifiers="const"> - <return type="String" /> - <description> - Returns the IP address of the currently connected host. - </description> - </method> - <method name="get_connected_port" qualifiers="const"> - <return type="int" /> - <description> - Returns the IP port of the currently connected host. - </description> - </method> - </methods> - <members> - <member name="trusted_tls_certificate" type="X509Certificate" setter="set_trusted_tls_certificate" getter="get_trusted_tls_certificate"> - If specified, this [X509Certificate] will be the only one accepted when connecting to an TLS host. Any other certificate provided by the server will be regarded as invalid. - [b]Note:[/b] Specifying a custom [code]trusted_tls_certificate[/code] is not supported in Web exports due to browsers' restrictions. - </member> - <member name="verify_tls" type="bool" setter="set_verify_tls_enabled" getter="is_verify_tls_enabled"> - If [code]true[/code], TLS certificate verification is enabled. - [b]Note:[/b] You must specify the certificates to be used in the Project Settings for it to work when exported. - </member> - </members> - <signals> - <signal name="connection_closed"> - <param index="0" name="was_clean_close" type="bool" /> - <description> - Emitted when the connection to the server is closed. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. - </description> - </signal> - <signal name="connection_error"> - <description> - Emitted when the connection to the server fails. - </description> - </signal> - <signal name="connection_established"> - <param index="0" name="protocol" type="String" /> - <description> - Emitted when a connection with the server is established, [code]protocol[/code] will contain the sub-protocol agreed with the server. - </description> - </signal> - <signal name="data_received"> - <description> - Emitted when a WebSocket message is received. - [b]Note:[/b] This signal is [i]not[/i] emitted when used as high-level multiplayer peer. - </description> - </signal> - <signal name="server_close_request"> - <param index="0" name="code" type="int" /> - <param index="1" name="reason" type="String" /> - <description> - Emitted when the server requests a clean close. You should keep polling until you get a [signal connection_closed] signal to achieve the clean close. See [method WebSocketPeer.close] for more details. - </description> - </signal> - </signals> -</class> diff --git a/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml b/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml index 4cc4d515e7..c4481b046b 100644 --- a/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml +++ b/modules/websocket/doc_classes/WebSocketMultiplayerPeer.xml @@ -10,6 +10,42 @@ <tutorials> </tutorials> <methods> + <method name="close"> + <return type="void" /> + <description> + Closes this [MultiplayerPeer], resetting the state to [constant MultiplayerPeer.CONNECTION_CONNECTED]. + [b]Note:[/b] To make sure remote peers receive a clean close prefer disconnecting clients via [method disconnect_peer]. + </description> + </method> + <method name="create_client"> + <return type="int" enum="Error" /> + <param index="0" name="url" type="String" /> + <param index="1" name="verify_tls" type="bool" default="true" /> + <param index="2" name="tls_certificate" type="X509Certificate" default="null" /> + <description> + Starts a new multiplayer client connecting to the given [param url]. If [param verify_tls] is [code]false[/code] certificate validation will be disabled. If specified, the [param tls_certificate] will be used to verify the TLS host. + [b]Note[/b]: It is recommended to specify the scheme part of the URL, i.e. the [param url] should start with either [code]ws://[/code] or [code]wss://[/code]. + </description> + </method> + <method name="create_server"> + <return type="int" enum="Error" /> + <param index="0" name="port" type="int" /> + <param index="1" name="bind_address" type="String" default=""*"" /> + <param index="2" name="tls_key" type="CryptoKey" default="null" /> + <param index="3" name="tls_certificate" type="X509Certificate" default="null" /> + <description> + Starts a new multiplayer server listening on the given [param port]. You can optionally specify a [param bind_address], and provide a [param tls_key] and [param tls_certificate] to use TLS. + </description> + </method> + <method name="disconnect_peer"> + <return type="void" /> + <param index="0" name="id" type="int" /> + <param index="1" name="code" type="int" default="1000" /> + <param index="2" name="reason" type="String" default="""" /> + <description> + Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more information. + </description> + </method> <method name="get_peer" qualifiers="const"> <return type="WebSocketPeer" /> <param index="0" name="peer_id" type="int" /> @@ -17,27 +53,39 @@ Returns the [WebSocketPeer] associated to the given [code]peer_id[/code]. </description> </method> - <method name="set_buffers"> - <return type="int" enum="Error" /> - <param index="0" name="input_buffer_size_kb" type="int" /> - <param index="1" name="input_max_packets" type="int" /> - <param index="2" name="output_buffer_size_kb" type="int" /> - <param index="3" name="output_max_packets" type="int" /> + <method name="get_peer_address" qualifiers="const"> + <return type="String" /> + <param index="0" name="id" type="int" /> <description> - Configures the buffer sizes for this WebSocket peer. Default values can be specified in the Project Settings under [code]network/limits[/code]. For server, values are meant per connected peer. - The first two parameters define the size and queued packets limits of the input buffer, the last two of the output buffer. - Buffer sizes are expressed in KiB, so [code]4 = 2^12 = 4096 bytes[/code]. All parameters will be rounded up to the nearest power of two. - [b]Note:[/b] Web exports only use the input buffer since the output one is managed by browsers. + Returns the IP address of the given peer. </description> </method> - </methods> - <signals> - <signal name="peer_packet"> - <param index="0" name="peer_source" type="int" /> + <method name="get_peer_port" qualifiers="const"> + <return type="int" /> + <param index="0" name="id" type="int" /> <description> - Emitted when a packet is received from a peer. - [b]Note:[/b] This signal is only emitted when the client or server is configured to use Godot multiplayer API. + Returns the remote port of the given peer. </description> - </signal> - </signals> + </method> + </methods> + <members> + <member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()"> + The extra headers to use during handshake. See [member WebSocketPeer.handshake_headers] for more details. + </member> + <member name="handshake_timeout" type="float" setter="set_handshake_timeout" getter="get_handshake_timeout" default="3.0"> + The maximum time each peer can stay in a connecting state before being dropped. + </member> + <member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535"> + The inbound buffer size for connected peers. See [member WebSocketPeer.inbound_buffer_size] for more details. + </member> + <member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="2048"> + The maximum number of queued packets for connected peers. See [member WebSocketPeer.max_queued_packets] for more details. + </member> + <member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535"> + The outbound buffer size for connected peers. See [member WebSocketPeer.outbound_buffer_size] for more details. + </member> + <member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()"> + The supported WebSocket sub-protocols. See [member WebSocketPeer.supported_protocols] for more details. + </member> + </members> </class> diff --git a/modules/websocket/doc_classes/WebSocketPeer.xml b/modules/websocket/doc_classes/WebSocketPeer.xml index 627b9c607c..fe0aae412e 100644 --- a/modules/websocket/doc_classes/WebSocketPeer.xml +++ b/modules/websocket/doc_classes/WebSocketPeer.xml @@ -1,25 +1,82 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="WebSocketPeer" inherits="PacketPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> - A class representing a specific WebSocket connection. + A WebSocket connection. </brief_description> <description> - This class represents a specific WebSocket connection, allowing you to do lower level operations with it. - You can choose to write to the socket in binary or text mode, and you can recognize the mode used for writing by the other peer. + This class represents WebSocket connection, and can be used as a WebSocket client (RFC 6455-compliant) or as a remote peer of a WebSocket server. + You can send WebSocket binary frames using [method PacketPeer.put_packet], and WebSocket text frames using [method send] (prefer text frames when interacting with text-based API). You can check the frame type of the last packet via [method was_string_packet]. + To start a WebSocket client, first call [method connect_to_url], then regularly call [method poll] (e.g. during [Node] process). You can query the socket state via [method get_ready_state], get the number of pending packets using [method PacketPeer.get_available_packet_count], and retrieve them via [method PacketPeer.get_packet]. + [codeblocks] + [gdscript] + extends Node + + var socket = WebSocketPeer.new() + + func _ready(): + socket.connect_to_url("wss://example.com") + + func _process(delta): + socket.poll() + var state = socket.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + while socket.get_available_packet_count(): + print("Packet: ", socket.get_packet()) + elif state == WebSocketPeer.STATE_CLOSING: + # Keep polling to achieve proper close. + pass + elif state == WebSocketPeer.STATE_CLOSED: + var code = socket.get_close_code() + var reason = socket.get_close_reason() + print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1]) + set_process(false) # Stop processing. + [/gdscript] + [/codeblocks] + To use the peer as part of a WebSocket server refer to [method accept_stream] and the online tutorial. </description> <tutorials> </tutorials> <methods> + <method name="accept_stream"> + <return type="int" enum="Error" /> + <param index="0" name="stream" type="StreamPeer" /> + <description> + Accepts a peer connection performing the HTTP handshake as a WebSocket server. The [param stream] must be a valid TCP stream retrieved via [method TCPServer.take_connection], or a TLS stream accepted via [method StreamPeerTLS.accept_stream]. + [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. + </description> + </method> <method name="close"> <return type="void" /> <param index="0" name="code" type="int" default="1000" /> <param index="1" name="reason" type="String" default="""" /> <description> - Closes this WebSocket connection. [code]code[/code] is the status code for the closure (see RFC 6455 section 7.4 for a list of valid status codes). [code]reason[/code] is the human readable reason for closing the connection (can be any UTF-8 string that's smaller than 123 bytes). - [b]Note:[/b] To achieve a clean close, you will need to keep polling until either [signal WebSocketClient.connection_closed] or [signal WebSocketServer.client_disconnected] is received. + Closes this WebSocket connection. [param code] is the status code for the closure (see RFC 6455 section 7.4 for a list of valid status codes). [param reason] is the human readable reason for closing the connection (can be any UTF-8 string that's smaller than 123 bytes). If [param code] is negative, the connection will be closed immediately without notifying the remote peer. + [b]Note:[/b] To achieve a clean close, you will need to keep polling until [constant STATE_CLOSED] is reached. [b]Note:[/b] The Web export might not support all status codes. Please refer to browser-specific documentation for more details. </description> </method> + <method name="connect_to_url"> + <return type="int" enum="Error" /> + <param index="0" name="url" type="String" /> + <param index="1" name="verify_tls" type="bool" default="true" /> + <param index="2" name="trusted_tls_certificate" type="X509Certificate" default="null" /> + <description> + Connects to the given URL. If [param verify_tls] is [code]false[/code] certificate validation will be disabled. If specified, the [param trusted_tls_certificate] will be the only one accepted when connecting to a TLS host. + [b]Note:[/b] To avoid mixed content warnings or errors in Web, you may have to use a [code]url[/code] that starts with [code]wss://[/code] (secure) instead of [code]ws://[/code]. When doing so, make sure to use the fully qualified domain name that matches the one defined in the server's TLS certificate. Do not connect directly via the IP address for [code]wss://[/code] connections, as it won't match with the TLS certificate. + </description> + </method> + <method name="get_close_code" qualifiers="const"> + <return type="int" /> + <description> + Returns the received WebSocket close frame status code, or [code]-1[/code] when the connection was not cleanly closed. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED]. + </description> + </method> + <method name="get_close_reason" qualifiers="const"> + <return type="String" /> + <description> + Returns the received WebSocket close frame status reason string. Only call this method when [method get_ready_state] returns [constant STATE_CLOSED]. + </description> + </method> <method name="get_connected_host" qualifiers="const"> <return type="String" /> <description> @@ -40,31 +97,51 @@ Returns the current amount of data in the outbound websocket buffer. [b]Note:[/b] Web exports use WebSocket.bufferedAmount, while other platforms use an internal buffer. </description> </method> - <method name="get_write_mode" qualifiers="const"> - <return type="int" enum="WebSocketPeer.WriteMode" /> + <method name="get_ready_state" qualifiers="const"> + <return type="int" enum="WebSocketPeer.State" /> <description> - Gets the current selected write mode. See [enum WriteMode]. + Returns the ready state of the connection. See [enum State]. </description> </method> - <method name="is_connected_to_host" qualifiers="const"> - <return type="bool" /> + <method name="get_requested_url" qualifiers="const"> + <return type="String" /> <description> - Returns [code]true[/code] if this peer is currently connected. + Returns the URL requested by this peer. The URL is derived from the [code]url[/code] passed to [method connect_to_url] or from the HTTP headers when acting as server (i.e. when using [method accept_stream]). </description> </method> - <method name="set_no_delay"> + <method name="get_selected_protocol" qualifiers="const"> + <return type="String" /> + <description> + Returns the selected WebSocket sub-protocol for this connection or an empty string if the sub-protocol has not been selected yet. + </description> + </method> + <method name="poll"> <return type="void" /> - <param index="0" name="enabled" type="bool" /> <description> - Disable Nagle's algorithm on the underling TCP socket (default). See [method StreamPeerTCP.set_no_delay] for more information. - [b]Note:[/b] Not available in the Web export. + Updates the connection state and receive incoming packets. Call this function regularly to keep it in a clean state. </description> </method> - <method name="set_write_mode"> + <method name="send"> + <return type="int" enum="Error" /> + <param index="0" name="message" type="PackedByteArray" /> + <param index="1" name="write_mode" type="int" enum="WebSocketPeer.WriteMode" default="1" /> + <description> + Sends the given [param message] using the desired [param write_mode]. When sending a [String], prefer using [method send_text]. + </description> + </method> + <method name="send_text"> + <return type="int" enum="Error" /> + <param index="0" name="message" type="String" /> + <description> + Sends the given [param message] using WebSocket text mode. Perfer this method over [method PacketPeer.put_packet] when interacting with third-party text-based API (e.g. when using [JSON] formatted messages). + </description> + </method> + <method name="set_no_delay"> <return type="void" /> - <param index="0" name="mode" type="int" enum="WebSocketPeer.WriteMode" /> + <param index="0" name="enabled" type="bool" /> <description> - Sets the socket to use the given [enum WriteMode]. + Disable Nagle's algorithm on the underling TCP socket (default). See [method StreamPeerTCP.set_no_delay] for more information. + [b]Note:[/b] Not available in the Web export. </description> </method> <method name="was_string_packet" qualifiers="const"> @@ -74,6 +151,24 @@ </description> </method> </methods> + <members> + <member name="handshake_headers" type="PackedStringArray" setter="set_handshake_headers" getter="get_handshake_headers" default="PackedStringArray()"> + The extra HTTP headers to be sent during the WebSocket handshake. + [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. + </member> + <member name="inbound_buffer_size" type="int" setter="set_inbound_buffer_size" getter="get_inbound_buffer_size" default="65535"> + The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the inbound packets). + </member> + <member name="max_queued_packets" type="int" setter="set_max_queued_packets" getter="get_max_queued_packets" default="2048"> + The maximum amount of packets that will be allowed in the queues (both inbound and outbound). + </member> + <member name="outbound_buffer_size" type="int" setter="set_outbound_buffer_size" getter="get_outbound_buffer_size" default="65535"> + The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the outbound packets). + </member> + <member name="supported_protocols" type="PackedStringArray" setter="set_supported_protocols" getter="get_supported_protocols" default="PackedStringArray()"> + The WebSocket sub-protocols allowed during the WebSocket handshake. + </member> + </members> <constants> <constant name="WRITE_MODE_TEXT" value="0" enum="WriteMode"> Specifies that WebSockets messages should be transferred as text payload (only valid UTF-8 is allowed). @@ -81,5 +176,17 @@ <constant name="WRITE_MODE_BINARY" value="1" enum="WriteMode"> Specifies that WebSockets messages should be transferred as binary payload (any byte combination is allowed). </constant> + <constant name="STATE_CONNECTING" value="0" enum="State"> + Socket has been created. The connection is not yet open. + </constant> + <constant name="STATE_OPEN" value="1" enum="State"> + The connection is open and ready to communicate. + </constant> + <constant name="STATE_CLOSING" value="2" enum="State"> + The connection is in the process of closing. This means a close request has been sent to the remote peer but confirmation has not been received. + </constant> + <constant name="STATE_CLOSED" value="3" enum="State"> + The connection is closed or couldn't be opened. + </constant> </constants> </class> diff --git a/modules/websocket/doc_classes/WebSocketServer.xml b/modules/websocket/doc_classes/WebSocketServer.xml deleted file mode 100644 index 07a55b73f1..0000000000 --- a/modules/websocket/doc_classes/WebSocketServer.xml +++ /dev/null @@ -1,127 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="WebSocketServer" inherits="WebSocketMultiplayerPeer" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> - <brief_description> - A WebSocket server implementation. - </brief_description> - <description> - This class implements a WebSocket server that can also support the high-level multiplayer API. - After starting the server ([method listen]), you will need to [method MultiplayerPeer.poll] it at regular intervals (e.g. inside [method Node._process]). When clients connect, disconnect, or send data, you will receive the appropriate signal. - [b]Note:[/b] Not available in Web exports. - [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android. - </description> - <tutorials> - </tutorials> - <methods> - <method name="disconnect_peer"> - <return type="void" /> - <param index="0" name="id" type="int" /> - <param index="1" name="code" type="int" default="1000" /> - <param index="2" name="reason" type="String" default="""" /> - <description> - Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more information. - </description> - </method> - <method name="get_peer_address" qualifiers="const"> - <return type="String" /> - <param index="0" name="id" type="int" /> - <description> - Returns the IP address of the given peer. - </description> - </method> - <method name="get_peer_port" qualifiers="const"> - <return type="int" /> - <param index="0" name="id" type="int" /> - <description> - Returns the remote port of the given peer. - </description> - </method> - <method name="has_peer" qualifiers="const"> - <return type="bool" /> - <param index="0" name="id" type="int" /> - <description> - Returns [code]true[/code] if a peer with the given ID is connected. - </description> - </method> - <method name="is_listening" qualifiers="const"> - <return type="bool" /> - <description> - Returns [code]true[/code] if the server is actively listening on a port. - </description> - </method> - <method name="listen"> - <return type="int" enum="Error" /> - <param index="0" name="port" type="int" /> - <param index="1" name="protocols" type="PackedStringArray" default="PackedStringArray()" /> - <param index="2" name="gd_mp_api" type="bool" default="false" /> - <description> - Starts listening on the given port. - You can specify the desired subprotocols via the "protocols" array. If the list empty (default), no sub-protocol will be requested. - If [code]true[/code] is passed as [code]gd_mp_api[/code], the server will behave like a multiplayer peer for the [MultiplayerAPI], connections from non-Godot clients will not work, and [signal data_received] will not be emitted. - If [code]false[/code] is passed instead (default), you must call [PacketPeer] functions ([code]put_packet[/code], [code]get_packet[/code], etc.), on the [WebSocketPeer] returned via [code]get_peer(id)[/code] to communicate with the peer with given [code]id[/code] (e.g. [code]get_peer(id).get_available_packet_count[/code]). - </description> - </method> - <method name="set_extra_headers"> - <return type="void" /> - <param index="0" name="headers" type="PackedStringArray" default="PackedStringArray()" /> - <description> - Sets additional headers to be sent to clients during the HTTP handshake. - </description> - </method> - <method name="stop"> - <return type="void" /> - <description> - Stops the server and clear its state. - </description> - </method> - </methods> - <members> - <member name="bind_ip" type="String" setter="set_bind_ip" getter="get_bind_ip" default=""*""> - When not set to [code]*[/code] will restrict incoming connections to the specified IP address. Setting [code]bind_ip[/code] to [code]127.0.0.1[/code] will cause the server to listen only to the local host. - </member> - <member name="ca_chain" type="X509Certificate" setter="set_ca_chain" getter="get_ca_chain"> - When using TLS (see [member private_key] and [member tls_certificate]), you can set this to a valid [X509Certificate] to be provided as additional CA chain information during the TLS handshake. - </member> - <member name="handshake_timeout" type="float" setter="set_handshake_timeout" getter="get_handshake_timeout" default="3.0"> - The time in seconds before a pending client (i.e. a client that has not yet finished the HTTP handshake) is considered stale and forcefully disconnected. - </member> - <member name="private_key" type="CryptoKey" setter="set_private_key" getter="get_private_key"> - When set to a valid [CryptoKey] (along with [member tls_certificate]) will cause the server to require TLS instead of regular TCP (i.e. the [code]wss://[/code] protocol). - </member> - <member name="tls_certificate" type="X509Certificate" setter="set_tls_certificate" getter="get_tls_certificate"> - When set to a valid [X509Certificate] (along with [member private_key]) will cause the server to require TLS instead of regular TCP (i.e. the [code]wss://[/code] protocol). - </member> - </members> - <signals> - <signal name="client_close_request"> - <param index="0" name="id" type="int" /> - <param index="1" name="code" type="int" /> - <param index="2" name="reason" type="String" /> - <description> - Emitted when a client requests a clean close. You should keep polling until you get a [signal client_disconnected] signal with the same [code]id[/code] to achieve the clean close. See [method WebSocketPeer.close] for more details. - </description> - </signal> - <signal name="client_connected"> - <param index="0" name="id" type="int" /> - <param index="1" name="protocol" type="String" /> - <param index="2" name="resource_name" type="String" /> - <description> - Emitted when a new client connects. "protocol" will be the sub-protocol agreed with the client, and "resource_name" will be the resource name of the URI the peer used. - "resource_name" is a path (at the very least a single forward slash) and potentially a query string. - </description> - </signal> - <signal name="client_disconnected"> - <param index="0" name="id" type="int" /> - <param index="1" name="was_clean_close" type="bool" /> - <description> - Emitted when a client disconnects. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. - </description> - </signal> - <signal name="data_received"> - <param index="0" name="id" type="int" /> - <description> - Emitted when a new message is received. - [b]Note:[/b] This signal is [i]not[/i] emitted when used as high-level multiplayer peer. - </description> - </signal> - </signals> -</class> diff --git a/modules/websocket/editor/editor_debugger_server_websocket.cpp b/modules/websocket/editor/editor_debugger_server_websocket.cpp index 0443147d98..48bfbaa14e 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.cpp +++ b/modules/websocket/editor/editor_debugger_server_websocket.cpp @@ -38,18 +38,31 @@ #include "editor/editor_node.h" #include "editor/editor_settings.h" -void EditorDebuggerServerWebSocket::_peer_connected(int p_id, String _protocol) { - pending_peers.push_back(p_id); -} +void EditorDebuggerServerWebSocket::poll() { + if (pending_peer.is_null() && tcp_server->is_connection_available()) { + Ref<WebSocketPeer> peer = Ref<WebSocketPeer>(WebSocketPeer::create()); + ERR_FAIL_COND(peer.is_null()); // Bug. -void EditorDebuggerServerWebSocket::_peer_disconnected(int p_id, bool p_was_clean) { - if (pending_peers.find(p_id)) { - pending_peers.erase(p_id); - } -} + Vector<String> ws_protocols; + ws_protocols.push_back("binary"); // Compatibility for emscripten TCP-to-WebSocket. + peer->set_supported_protocols(ws_protocols); -void EditorDebuggerServerWebSocket::poll() { - server->poll(); + Error err = peer->accept_stream(tcp_server->take_connection()); + if (err == OK) { + pending_timer = OS::get_singleton()->get_ticks_msec(); + pending_peer = peer; + } + } + if (pending_peer.is_valid() && pending_peer->get_ready_state() != WebSocketPeer::STATE_OPEN) { + pending_peer->poll(); + WebSocketPeer::State ready_state = pending_peer->get_ready_state(); + if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) { + pending_peer.unref(); // Failed. + } + if (ready_state == WebSocketPeer::STATE_CONNECTING && OS::get_singleton()->get_ticks_msec() - pending_timer > 3000) { + pending_peer.unref(); // Timeout. + } + } } String EditorDebuggerServerWebSocket::get_uri() const { @@ -69,15 +82,10 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } - // Set up the server - server->set_bind_ip(bind_host); - Vector<String> compatible_protocols; - compatible_protocols.push_back("binary"); // compatibility with EMSCRIPTEN TCP-to-WebSocket layer. - // Try listening on ports const int max_attempts = 5; for (int attempt = 1;; ++attempt) { - const Error err = server->listen(bind_port, compatible_protocols); + const Error err = tcp_server->listen(bind_port, bind_host); if (err == OK) { break; } @@ -96,31 +104,27 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { } void EditorDebuggerServerWebSocket::stop() { - server->stop(); - pending_peers.clear(); + pending_peer.unref(); + tcp_server->stop(); } bool EditorDebuggerServerWebSocket::is_active() const { - return server->is_listening(); + return tcp_server->is_listening(); } bool EditorDebuggerServerWebSocket::is_connection_available() const { - return pending_peers.size() > 0; + return pending_peer.is_valid() && pending_peer->get_ready_state() == WebSocketPeer::STATE_OPEN; } Ref<RemoteDebuggerPeer> EditorDebuggerServerWebSocket::take_connection() { ERR_FAIL_COND_V(!is_connection_available(), Ref<RemoteDebuggerPeer>()); - RemoteDebuggerPeer *peer = memnew(RemoteDebuggerPeerWebSocket(server->get_peer(pending_peers[0]))); - pending_peers.pop_front(); + RemoteDebuggerPeer *peer = memnew(RemoteDebuggerPeerWebSocket(pending_peer)); + pending_peer.unref(); return peer; } EditorDebuggerServerWebSocket::EditorDebuggerServerWebSocket() { - server = Ref<WebSocketServer>(WebSocketServer::create()); - int max_pkts = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages"); - server->set_buffers(8192, max_pkts, 8192, max_pkts); - server->connect("client_connected", callable_mp(this, &EditorDebuggerServerWebSocket::_peer_connected)); - server->connect("client_disconnected", callable_mp(this, &EditorDebuggerServerWebSocket::_peer_disconnected)); + tcp_server.instantiate(); } EditorDebuggerServerWebSocket::~EditorDebuggerServerWebSocket() { diff --git a/modules/websocket/editor/editor_debugger_server_websocket.h b/modules/websocket/editor/editor_debugger_server_websocket.h index 7c0705302d..31e54cb5df 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.h +++ b/modules/websocket/editor/editor_debugger_server_websocket.h @@ -33,15 +33,18 @@ #ifdef TOOLS_ENABLED -#include "../websocket_server.h" +#include "../websocket_peer.h" + +#include "core/io/tcp_server.h" #include "editor/debugger/editor_debugger_server.h" class EditorDebuggerServerWebSocket : public EditorDebuggerServer { GDCLASS(EditorDebuggerServerWebSocket, EditorDebuggerServer); private: - Ref<WebSocketServer> server; - List<int> pending_peers; + Ref<TCPServer> tcp_server; + Ref<WebSocketPeer> pending_peer; + uint64_t pending_timer = 0; String endpoint; public: diff --git a/modules/websocket/emws_client.cpp b/modules/websocket/emws_client.cpp deleted file mode 100644 index 933a1f43e9..0000000000 --- a/modules/websocket/emws_client.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/*************************************************************************/ -/* emws_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifdef WEB_ENABLED - -#include "emws_client.h" - -#include "core/config/project_settings.h" -#include "core/io/ip.h" -#include "emscripten.h" - -void EMWSClient::_esws_on_connect(void *obj, char *proto) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_is_connecting = false; - client->_on_connect(String(proto)); -} - -void EMWSClient::_esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - - Error err = static_cast<EMWSPeer *>(*client->get_peer(1))->read_msg(p_data, p_data_size, p_is_string == 1); - if (err == OK) { - client->_on_peer_packet(); - } -} - -void EMWSClient::_esws_on_error(void *obj) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_is_connecting = false; - client->_on_error(); -} - -void EMWSClient::_esws_on_close(void *obj, int code, const char *reason, int was_clean) { - EMWSClient *client = static_cast<EMWSClient *>(obj); - client->_on_close_request(code, String(reason)); - client->_is_connecting = false; - client->disconnect_from_host(); - client->_on_disconnect(was_clean != 0); -} - -Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocols, const Vector<String> p_custom_headers) { - if (_js_id) { - godot_js_websocket_destroy(_js_id); - _js_id = 0; - } - - String proto_string; - for (int i = 0; i < p_protocols.size(); i++) { - if (i != 0) { - proto_string += ","; - } - proto_string += p_protocols[i]; - } - - String str = "ws://"; - - if (p_custom_headers.size()) { - WARN_PRINT_ONCE("Custom headers are not supported in Web platform."); - } - if (p_tls) { - str = "wss://"; - if (tls_cert.is_valid()) { - WARN_PRINT_ONCE("Custom SSL certificate is not supported in Web platform."); - } - } - str += p_host + ":" + itos(p_port) + p_path; - _is_connecting = true; - - _js_id = godot_js_websocket_create(this, str.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close); - if (!_js_id) { - return FAILED; - } - - static_cast<Ref<EMWSPeer>>(_peer)->set_sock(_js_id, _in_buf_size, _in_pkt_size, _out_buf_size); - - return OK; -} - -void EMWSClient::poll() { -} - -Ref<WebSocketPeer> EMWSClient::get_peer(int p_peer_id) const { - return _peer; -} - -MultiplayerPeer::ConnectionStatus EMWSClient::get_connection_status() const { - if (_peer->is_connected_to_host()) { - if (_is_connecting) { - return CONNECTION_CONNECTING; - } - return CONNECTION_CONNECTED; - } - - return CONNECTION_DISCONNECTED; -} - -void EMWSClient::disconnect_from_host(int p_code, String p_reason) { - _peer->close(p_code, p_reason); -} - -IPAddress EMWSClient::get_connected_host() const { - ERR_FAIL_V_MSG(IPAddress(), "Not supported in Web export."); -} - -uint16_t EMWSClient::get_connected_port() const { - ERR_FAIL_V_MSG(0, "Not supported in Web export."); -} - -int EMWSClient::get_max_packet_size() const { - return (1 << _in_buf_size) - PROTO_SIZE; -} - -Error EMWSClient::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - return OK; -} - -EMWSClient::EMWSClient() { - _peer = Ref<EMWSPeer>(memnew(EMWSPeer)); -} - -EMWSClient::~EMWSClient() { - disconnect_from_host(); - _peer = Ref<EMWSPeer>(); - if (_js_id) { - godot_js_websocket_destroy(_js_id); - } -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index 859c92b457..3bd132bc73 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -34,55 +34,116 @@ #include "core/io/ip.h" -void EMWSPeer::set_sock(int p_sock, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size) { - peer_sock = p_sock; - _in_buffer.resize(p_in_pkt_size, p_in_buf_size); - _packet_buffer.resize((1 << p_in_buf_size)); - _out_buf_size = p_out_buf_size; +void EMWSPeer::_esws_on_connect(void *p_obj, char *p_proto) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->ready_state = STATE_OPEN; + peer->selected_protocol.parse_utf8(p_proto); } -void EMWSPeer::set_write_mode(WriteMode p_mode) { - write_mode = p_mode; +void EMWSPeer::_esws_on_message(void *p_obj, const uint8_t *p_data, int p_data_size, int p_is_string) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + uint8_t is_string = p_is_string ? 1 : 0; + peer->in_buffer.write_packet(p_data, p_data_size, &is_string); } -EMWSPeer::WriteMode EMWSPeer::get_write_mode() const { - return write_mode; +void EMWSPeer::_esws_on_error(void *p_obj) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->ready_state = STATE_CLOSED; } -Error EMWSPeer::read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string) { - uint8_t is_string = p_is_string ? 1 : 0; - return _in_buffer.write_packet(p_data, p_size, &is_string); +void EMWSPeer::_esws_on_close(void *p_obj, int p_code, const char *p_reason, int p_was_clean) { + EMWSPeer *peer = static_cast<EMWSPeer *>(p_obj); + peer->close_code = p_code; + peer->close_reason.parse_utf8(p_reason); + peer->ready_state = STATE_CLOSED; } -Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V(_out_buf_size && ((uint64_t)godot_js_websocket_buffered_amount(peer_sock) + p_buffer_size >= (1ULL << _out_buf_size)), ERR_OUT_OF_MEMORY); +Error EMWSPeer::connect_to_url(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(ready_state != STATE_CLOSED, ERR_ALREADY_IN_USE); + _clear(); + + String host; + String path; + String scheme; + int port = 0; + Error err = p_url.parse_url(scheme, host, port, path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); + + if (scheme.is_empty()) { + scheme = "ws://"; + } + ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme)); + + String proto_string; + for (int i = 0; i < supported_protocols.size(); i++) { + if (i != 0) { + proto_string += ","; + } + proto_string += supported_protocols[i]; + } + + if (handshake_headers.size()) { + WARN_PRINT_ONCE("Custom headers are not supported in Web platform."); + } + if (p_tls_certificate.is_valid()) { + WARN_PRINT_ONCE("Custom SSL certificates are not supported in Web platform."); + } - int is_bin = write_mode == WebSocketPeer::WRITE_MODE_BINARY ? 1 : 0; + requested_url = scheme + host; - if (godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, is_bin) != 0) { + if (port && ((scheme == "ws://" && port != 80) || (scheme == "wss://" && port != 443))) { + requested_url += ":" + String::num(port); + } + + peer_sock = godot_js_websocket_create(this, requested_url.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close); + if (peer_sock == -1) { return FAILED; } + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_CONNECTING; + return OK; +} + +Error EMWSPeer::accept_stream(Ref<StreamPeer> p_stream) { + WARN_PRINT_ONCE("Acting as WebSocket server is not supported in Web platforms."); + return ERR_UNAVAILABLE; +} + +Error EMWSPeer::_send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary) { + ERR_FAIL_COND_V(outbound_buffer_size > 0 && (get_current_outbound_buffered_amount() + p_buffer_size >= outbound_buffer_size), ERR_OUT_OF_MEMORY); + if (godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, p_binary ? 1 : 0) != 0) { + return FAILED; + } return OK; } +Error EMWSPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) { + return _send(p_buffer, p_buffer_size, p_mode == WRITE_MODE_BINARY); +} + +Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { + return _send(p_buffer, p_buffer_size, true); +} + Error EMWSPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { - if (_in_buffer.packets_left() == 0) { + if (in_buffer.packets_left() == 0) { return ERR_UNAVAILABLE; } int read = 0; - Error err = _in_buffer.read_packet(_packet_buffer.ptrw(), _packet_buffer.size(), &_is_string, read); + Error err = in_buffer.read_packet(packet_buffer.ptrw(), packet_buffer.size(), &was_string, read); ERR_FAIL_COND_V(err != OK, err); - *r_buffer = _packet_buffer.ptr(); + *r_buffer = packet_buffer.ptr(); r_buffer_size = read; return OK; } int EMWSPeer::get_available_packet_count() const { - return _in_buffer.packets_left(); + return in_buffer.packets_left(); } int EMWSPeer::get_current_outbound_buffered_amount() const { @@ -93,20 +154,66 @@ int EMWSPeer::get_current_outbound_buffered_amount() const { } bool EMWSPeer::was_string_packet() const { - return _is_string; + return was_string; } -bool EMWSPeer::is_connected_to_host() const { - return peer_sock != -1; +void EMWSPeer::_clear() { + if (peer_sock != -1) { + godot_js_websocket_destroy(peer_sock); + peer_sock = -1; + } + ready_state = STATE_CLOSED; + was_string = 0; + close_code = -1; + close_reason.clear(); + selected_protocol.clear(); + requested_url.clear(); + in_buffer.clear(); + packet_buffer.clear(); } void EMWSPeer::close(int p_code, String p_reason) { - if (peer_sock != -1) { - godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data()); + if (p_code < 0) { + if (peer_sock != -1) { + godot_js_websocket_destroy(peer_sock); + peer_sock = -1; + } + ready_state = STATE_CLOSED; + } + if (ready_state == STATE_CONNECTING || ready_state == STATE_OPEN) { + ready_state = STATE_CLOSING; + if (peer_sock != -1) { + godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data()); + } else { + ready_state = STATE_CLOSED; + } } - _is_string = 0; - _in_buffer.clear(); - peer_sock = -1; + in_buffer.clear(); + packet_buffer.clear(); +} + +void EMWSPeer::poll() { + // Automatically polled by the navigator. +} + +WebSocketPeer::State EMWSPeer::get_ready_state() const { + return ready_state; +} + +int EMWSPeer::get_close_code() const { + return close_code; +} + +String EMWSPeer::get_close_reason() const { + return close_reason; +} + +String EMWSPeer::get_selected_protocol() const { + return selected_protocol; +} + +String EMWSPeer::get_requested_url() const { + return requested_url; } IPAddress EMWSPeer::get_connected_host() const { @@ -122,11 +229,10 @@ void EMWSPeer::set_no_delay(bool p_enabled) { } EMWSPeer::EMWSPeer() { - close(); } EMWSPeer::~EMWSPeer() { - close(); + _clear(); } #endif // WEB_ENABLED diff --git a/modules/websocket/emws_peer.h b/modules/websocket/emws_peer.h index cdbc9212a5..322cc3b700 100644 --- a/modules/websocket/emws_peer.h +++ b/modules/websocket/emws_peer.h @@ -54,33 +54,53 @@ extern void godot_js_websocket_destroy(int p_id); } class EMWSPeer : public WebSocketPeer { - GDCIIMPL(EMWSPeer, WebSocketPeer); - private: int peer_sock = -1; - WriteMode write_mode = WRITE_MODE_BINARY; - Vector<uint8_t> _packet_buffer; - PacketBuffer<uint8_t> _in_buffer; - uint8_t _is_string = 0; - int _out_buf_size = 0; + State ready_state = STATE_CLOSED; + Vector<uint8_t> packet_buffer; + PacketBuffer<uint8_t> in_buffer; + uint8_t was_string = 0; + int close_code = -1; + String close_reason; + String selected_protocol; + String requested_url; + + static WebSocketPeer *_create() { return memnew(EMWSPeer); } + static void _esws_on_connect(void *obj, char *proto); + static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); + static void _esws_on_error(void *obj); + static void _esws_on_close(void *obj, int code, const char *reason, int was_clean); + + void _clear(); + Error _send(const uint8_t *p_buffer, int p_buffer_size, bool p_binary); public: - Error read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string); - void set_sock(int p_sock, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size); + static void initialize() { WebSocketPeer::_create = EMWSPeer::_create; } + + // PacketPeer virtual int get_available_packet_count() const override; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override; virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; - virtual int get_max_packet_size() const override { return _packet_buffer.size(); }; - virtual int get_current_outbound_buffered_amount() const override; + virtual int get_max_packet_size() const override { return packet_buffer.size(); }; + // WebSocketPeer + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override; + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) override; + virtual Error accept_stream(Ref<StreamPeer> p_stream) override; virtual void close(int p_code = 1000, String p_reason = "") override; - virtual bool is_connected_to_host() const override; + virtual void poll() override; + + virtual State get_ready_state() const override; + virtual int get_close_code() const override; + virtual String get_close_reason() const override; + virtual int get_current_outbound_buffered_amount() const override; + virtual IPAddress get_connected_host() const override; virtual uint16_t get_connected_port() const override; + virtual String get_selected_protocol() const override; + virtual String get_requested_url() const override; - virtual WriteMode get_write_mode() const override; - virtual void set_write_mode(WriteMode p_mode) override; virtual bool was_string_packet() const override; virtual void set_no_delay(bool p_enabled) override; diff --git a/modules/websocket/packet_buffer.h b/modules/websocket/packet_buffer.h index 7b4a164576..cd81dc43cd 100644 --- a/modules/websocket/packet_buffer.h +++ b/modules/websocket/packet_buffer.h @@ -41,32 +41,29 @@ private: T info; } _Packet; - RingBuffer<_Packet> _packets; + Vector<_Packet> _packets; + int _queued = 0; + int _write_pos = 0; + int _read_pos = 0; RingBuffer<uint8_t> _payload; public: Error write_packet(const uint8_t *p_payload, uint32_t p_size, const T *p_info) { -#ifdef TOOLS_ENABLED - // Verbose buffer warnings - if (p_payload && _payload.space_left() < (int32_t)p_size) { - ERR_PRINT("Buffer payload full! Dropping data."); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); - } - if (p_info && _packets.space_left() < 1) { - ERR_PRINT("Too many packets in queue! Dropping data."); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); - } -#else - ERR_FAIL_COND_V(p_payload && (uint32_t)_payload.space_left() < p_size, ERR_OUT_OF_MEMORY); - ERR_FAIL_COND_V(p_info && _packets.space_left() < 1, ERR_OUT_OF_MEMORY); -#endif + ERR_FAIL_COND_V_MSG(p_payload && (uint32_t)_payload.space_left() < p_size, ERR_OUT_OF_MEMORY, "Buffer payload full! Dropping data."); + ERR_FAIL_COND_V_MSG(p_info && _queued >= _packets.size(), ERR_OUT_OF_MEMORY, "Too many packets in queue! Dropping data."); // If p_info is nullptr, only the payload is written if (p_info) { + ERR_FAIL_COND_V(_write_pos > _packets.size(), ERR_OUT_OF_MEMORY); _Packet p; p.size = p_size; - memcpy(&p.info, p_info, sizeof(T)); - _packets.write(p); + p.info = *p_info; + _packets.write[_write_pos] = p; + _queued += 1; + _write_pos++; + if (_write_pos >= _packets.size()) { + _write_pos = 0; + } } // If p_payload is nullptr, only the packet information is written. @@ -78,9 +75,14 @@ public: } Error read_packet(uint8_t *r_payload, int p_bytes, T *r_info, int &r_read) { - ERR_FAIL_COND_V(_packets.data_left() < 1, ERR_UNAVAILABLE); - _Packet p; - _packets.read(&p, 1); + ERR_FAIL_COND_V(_queued < 1, ERR_UNAVAILABLE); + _Packet p = _packets[_read_pos]; + _read_pos += 1; + if (_read_pos >= _packets.size()) { + _read_pos = 0; + } + _queued -= 1; + ERR_FAIL_COND_V(_payload.data_left() < (int)p.size, ERR_BUG); ERR_FAIL_COND_V(p_bytes < (int)p.size, ERR_OUT_OF_MEMORY); @@ -90,22 +92,24 @@ public: return OK; } - void discard_payload(int p_size) { - _packets.decrease_write(p_size); - } - - void resize(int p_pkt_shift, int p_buf_shift) { - _packets.resize(p_pkt_shift); + void resize(int p_buf_shift, int p_max_packets) { _payload.resize(p_buf_shift); + _packets.resize(p_max_packets); + _read_pos = 0; + _write_pos = 0; + _queued = 0; } int packets_left() const { - return _packets.data_left(); + return _queued; } void clear() { _payload.resize(0); _packets.resize(0); + _read_pos = 0; + _write_pos = 0; + _queued = 0; } PacketBuffer() { diff --git a/modules/websocket/register_types.cpp b/modules/websocket/register_types.cpp index faa7021b2f..c55a651ab0 100644 --- a/modules/websocket/register_types.cpp +++ b/modules/websocket/register_types.cpp @@ -31,18 +31,18 @@ #include "register_types.h" #include "core/config/project_settings.h" +#include "core/debugger/engine_debugger.h" #include "core/error/error_macros.h" -#include "websocket_client.h" -#include "websocket_server.h" +#include "websocket_multiplayer_peer.h" +#include "websocket_peer.h" + +#include "remote_debugger_peer_websocket.h" #ifdef WEB_ENABLED -#include "emscripten.h" -#include "emws_client.h" #include "emws_peer.h" #else -#include "wsl_client.h" -#include "wsl_server.h" +#include "wsl_peer.h" #endif #ifdef TOOLS_ENABLED @@ -58,20 +58,18 @@ static void _editor_init_callback() { #endif void initialize_websocket_module(ModuleInitializationLevel p_level) { - if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + if (p_level == MODULE_INITIALIZATION_LEVEL_CORE) { #ifdef WEB_ENABLED - EMWSPeer::make_default(); - EMWSClient::make_default(); + EMWSPeer::initialize(); #else - WSLPeer::make_default(); - WSLClient::make_default(); - WSLServer::make_default(); + WSLPeer::initialize(); #endif - GDREGISTER_ABSTRACT_CLASS(WebSocketMultiplayerPeer); - ClassDB::register_custom_instance_class<WebSocketServer>(); - ClassDB::register_custom_instance_class<WebSocketClient>(); + GDREGISTER_CLASS(WebSocketMultiplayerPeer); ClassDB::register_custom_instance_class<WebSocketPeer>(); + + EngineDebugger::register_uri_handler("ws://", RemoteDebuggerPeerWebSocket::create); + EngineDebugger::register_uri_handler("wss://", RemoteDebuggerPeerWebSocket::create); } #ifdef TOOLS_ENABLED @@ -82,7 +80,10 @@ void initialize_websocket_module(ModuleInitializationLevel p_level) { } void uninitialize_websocket_module(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + if (p_level != MODULE_INITIALIZATION_LEVEL_CORE) { return; } +#ifndef WEB_ENABLED + WSLPeer::deinitialize(); +#endif } diff --git a/modules/websocket/remote_debugger_peer_websocket.cpp b/modules/websocket/remote_debugger_peer_websocket.cpp index f703873cbf..58adb76208 100644 --- a/modules/websocket/remote_debugger_peer_websocket.cpp +++ b/modules/websocket/remote_debugger_peer_websocket.cpp @@ -33,30 +33,39 @@ #include "core/config/project_settings.h" Error RemoteDebuggerPeerWebSocket::connect_to_host(const String &p_uri) { + ws_peer = Ref<WebSocketPeer>(WebSocketPeer::create()); + ERR_FAIL_COND_V(ws_peer.is_null(), ERR_BUG); + Vector<String> protocols; protocols.push_back("binary"); // Compatibility for emscripten TCP-to-WebSocket. - ws_client->connect_to_url(p_uri, protocols); - ws_client->poll(); + ws_peer->set_supported_protocols(protocols); + ws_peer->set_max_queued_packets(max_queued_messages); + ws_peer->set_inbound_buffer_size((1 << 23) - 1); + ws_peer->set_outbound_buffer_size((1 << 23) - 1); + + Error err = ws_peer->connect_to_url(p_uri); + ERR_FAIL_COND_V(err != OK, err); - if (ws_client->get_connection_status() == WebSocketClient::CONNECTION_DISCONNECTED) { - ERR_PRINT("Remote Debugger: Unable to connect. Status: " + String::num(ws_client->get_connection_status()) + "."); + ws_peer->poll(); + WebSocketPeer::State ready_state = ws_peer->get_ready_state(); + if (ready_state != WebSocketPeer::STATE_CONNECTING && ready_state != WebSocketPeer::STATE_OPEN) { + ERR_PRINT(vformat("Remote Debugger: Unable to connect. State: %s.", ws_peer->get_ready_state())); return FAILED; } - ws_peer = ws_client->get_peer(1); - return OK; } bool RemoteDebuggerPeerWebSocket::is_peer_connected() { - return ws_peer.is_valid() && ws_peer->is_connected_to_host(); + return ws_peer.is_valid() && (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN || ws_peer->get_ready_state() == WebSocketPeer::STATE_CONNECTING); } void RemoteDebuggerPeerWebSocket::poll() { - ws_client->poll(); + ERR_FAIL_COND(ws_peer.is_null()); + ws_peer->poll(); - while (ws_peer->is_connected_to_host() && ws_peer->get_available_packet_count() > 0 && in_queue.size() < max_queued_messages) { + while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && ws_peer->get_available_packet_count() > 0 && in_queue.size() < max_queued_messages) { Variant var; Error err = ws_peer->get_var(var); ERR_CONTINUE(err != OK); @@ -64,7 +73,7 @@ void RemoteDebuggerPeerWebSocket::poll() { in_queue.push_back(var); } - while (ws_peer->is_connected_to_host() && out_queue.size() > 0) { + while (ws_peer->get_ready_state() == WebSocketPeer::STATE_OPEN && out_queue.size() > 0) { Array var = out_queue[0]; Error err = ws_peer->put_var(var); ERR_BREAK(err != OK); // Peer buffer full? @@ -73,7 +82,8 @@ void RemoteDebuggerPeerWebSocket::poll() { } int RemoteDebuggerPeerWebSocket::get_max_message_size() const { - return 8 << 20; // 8 Mib + ERR_FAIL_COND_V(ws_peer.is_null(), 0); + return ws_peer->get_max_packet_size(); } bool RemoteDebuggerPeerWebSocket::has_message() { @@ -99,7 +109,6 @@ void RemoteDebuggerPeerWebSocket::close() { if (ws_peer.is_valid()) { ws_peer.unref(); } - ws_client->disconnect_from_host(); } bool RemoteDebuggerPeerWebSocket::can_block() const { @@ -111,14 +120,13 @@ bool RemoteDebuggerPeerWebSocket::can_block() const { } RemoteDebuggerPeerWebSocket::RemoteDebuggerPeerWebSocket(Ref<WebSocketPeer> p_peer) { -#ifdef WEB_ENABLED - ws_client = Ref<WebSocketClient>(memnew(EMWSClient)); -#else - ws_client = Ref<WebSocketClient>(memnew(WSLClient)); -#endif max_queued_messages = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages"); - ws_client->set_buffers(8192, max_queued_messages, 8192, max_queued_messages); ws_peer = p_peer; + if (ws_peer.is_valid()) { + ws_peer->set_max_queued_packets(max_queued_messages); + ws_peer->set_inbound_buffer_size((1 << 23) - 1); + ws_peer->set_outbound_buffer_size((1 << 23) - 1); + } } RemoteDebuggerPeer *RemoteDebuggerPeerWebSocket::create(const String &p_uri) { diff --git a/modules/websocket/remote_debugger_peer_websocket.h b/modules/websocket/remote_debugger_peer_websocket.h index 0292de68ad..4d496ae891 100644 --- a/modules/websocket/remote_debugger_peer_websocket.h +++ b/modules/websocket/remote_debugger_peer_websocket.h @@ -33,14 +33,9 @@ #include "core/debugger/remote_debugger_peer.h" -#ifdef WEB_ENABLED -#include "emws_client.h" -#else -#include "wsl_client.h" -#endif +#include "websocket_peer.h" class RemoteDebuggerPeerWebSocket : public RemoteDebuggerPeer { - Ref<WebSocketClient> ws_client; Ref<WebSocketPeer> ws_peer; List<Array> in_queue; List<Array> out_queue; diff --git a/modules/websocket/websocket_client.cpp b/modules/websocket/websocket_client.cpp deleted file mode 100644 index 0b2d5d1918..0000000000 --- a/modules/websocket/websocket_client.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/*************************************************************************/ -/* websocket_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#include "websocket_client.h" - -GDCINULL(WebSocketClient); - -WebSocketClient::WebSocketClient() { -} - -WebSocketClient::~WebSocketClient() { -} - -Error WebSocketClient::connect_to_url(String p_url, const Vector<String> p_protocols, bool gd_mp_api, const Vector<String> p_custom_headers) { - _is_multiplayer = gd_mp_api; - - String host = p_url; - String path; - String scheme; - int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); - ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); - - bool tls = false; - if (scheme == "wss://") { - tls = true; - } - if (port == 0) { - port = tls ? 443 : 80; - } - if (path.is_empty()) { - path = "/"; - } - return connect_to_host(host, path, port, tls, p_protocols, p_custom_headers); -} - -void WebSocketClient::set_verify_tls_enabled(bool p_verify_tls) { - verify_tls = p_verify_tls; -} - -bool WebSocketClient::is_verify_tls_enabled() const { - return verify_tls; -} - -Ref<X509Certificate> WebSocketClient::get_trusted_tls_certificate() const { - return tls_cert; -} - -void WebSocketClient::set_trusted_tls_certificate(Ref<X509Certificate> p_cert) { - ERR_FAIL_COND(get_connection_status() != CONNECTION_DISCONNECTED); - tls_cert = p_cert; -} - -bool WebSocketClient::is_server() const { - return false; -} - -void WebSocketClient::_on_peer_packet() { - if (_is_multiplayer) { - _process_multiplayer(get_peer(1), 1); - } else { - emit_signal(SNAME("data_received")); - } -} - -void WebSocketClient::_on_connect(String p_protocol) { - if (_is_multiplayer) { - // need to wait for ID confirmation... - } else { - emit_signal(SNAME("connection_established"), p_protocol); - } -} - -void WebSocketClient::_on_close_request(int p_code, String p_reason) { - emit_signal(SNAME("server_close_request"), p_code, p_reason); -} - -void WebSocketClient::_on_disconnect(bool p_was_clean) { - if (_is_multiplayer) { - emit_signal(SNAME("connection_failed")); - } else { - emit_signal(SNAME("connection_closed"), p_was_clean); - } -} - -void WebSocketClient::_on_error() { - if (_is_multiplayer) { - emit_signal(SNAME("connection_failed")); - } else { - emit_signal(SNAME("connection_error")); - } -} - -void WebSocketClient::_bind_methods() { - ClassDB::bind_method(D_METHOD("connect_to_url", "url", "protocols", "gd_mp_api", "custom_headers"), &WebSocketClient::connect_to_url, DEFVAL(Vector<String>()), DEFVAL(false), DEFVAL(Vector<String>())); - ClassDB::bind_method(D_METHOD("disconnect_from_host", "code", "reason"), &WebSocketClient::disconnect_from_host, DEFVAL(1000), DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketClient::get_connected_host); - ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketClient::get_connected_port); - ClassDB::bind_method(D_METHOD("set_verify_tls_enabled", "enabled"), &WebSocketClient::set_verify_tls_enabled); - ClassDB::bind_method(D_METHOD("is_verify_tls_enabled"), &WebSocketClient::is_verify_tls_enabled); - - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "verify_tls", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_verify_tls_enabled", "is_verify_tls_enabled"); - - ClassDB::bind_method(D_METHOD("get_trusted_tls_certificate"), &WebSocketClient::get_trusted_tls_certificate); - ClassDB::bind_method(D_METHOD("set_trusted_tls_certificate", "cert"), &WebSocketClient::set_trusted_tls_certificate); - - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "trusted_tls_certificate", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_trusted_tls_certificate", "get_trusted_tls_certificate"); - - ADD_SIGNAL(MethodInfo("data_received")); - ADD_SIGNAL(MethodInfo("connection_established", PropertyInfo(Variant::STRING, "protocol"))); - ADD_SIGNAL(MethodInfo("server_close_request", PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); - ADD_SIGNAL(MethodInfo("connection_closed", PropertyInfo(Variant::BOOL, "was_clean_close"))); - ADD_SIGNAL(MethodInfo("connection_error")); -} diff --git a/modules/websocket/websocket_multiplayer_peer.cpp b/modules/websocket/websocket_multiplayer_peer.cpp index 7a3bbf1c47..0202c5bac7 100644 --- a/modules/websocket/websocket_multiplayer_peer.cpp +++ b/modules/websocket/websocket_multiplayer_peer.cpp @@ -33,72 +33,130 @@ #include "core/os/os.h" WebSocketMultiplayerPeer::WebSocketMultiplayerPeer() { + peer_config = Ref<WebSocketPeer>(WebSocketPeer::create()); } WebSocketMultiplayerPeer::~WebSocketMultiplayerPeer() { _clear(); } +Ref<WebSocketPeer> WebSocketMultiplayerPeer::_create_peer() { + Ref<WebSocketPeer> peer = Ref<WebSocketPeer>(WebSocketPeer::create()); + peer->set_supported_protocols(get_supported_protocols()); + peer->set_handshake_headers(get_handshake_headers()); + peer->set_inbound_buffer_size(get_inbound_buffer_size()); + peer->set_outbound_buffer_size(get_outbound_buffer_size()); + peer->set_max_queued_packets(get_max_queued_packets()); + return peer; +} + void WebSocketMultiplayerPeer::_clear() { - _peer_map.clear(); - if (_current_packet.data != nullptr) { - memfree(_current_packet.data); + connection_status = CONNECTION_DISCONNECTED; + unique_id = 0; + peers_map.clear(); + use_tls = false; + tcp_server.unref(); + pending_peers.clear(); + tls_certificate.unref(); + tls_key.unref(); + if (current_packet.data != nullptr) { + memfree(current_packet.data); + current_packet.data = nullptr; } - for (Packet &E : _incoming_packets) { + for (Packet &E : incoming_packets) { memfree(E.data); E.data = nullptr; } - _incoming_packets.clear(); + incoming_packets.clear(); } void WebSocketMultiplayerPeer::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_buffers", "input_buffer_size_kb", "input_max_packets", "output_buffer_size_kb", "output_max_packets"), &WebSocketMultiplayerPeer::set_buffers); + ClassDB::bind_method(D_METHOD("create_client", "url", "verify_tls", "tls_certificate"), &WebSocketMultiplayerPeer::create_client, DEFVAL(true), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("create_server", "port", "bind_address", "tls_key", "tls_certificate"), &WebSocketMultiplayerPeer::create_server, DEFVAL("*"), DEFVAL(Ref<CryptoKey>()), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("close"), &WebSocketMultiplayerPeer::close); + ClassDB::bind_method(D_METHOD("get_peer", "peer_id"), &WebSocketMultiplayerPeer::get_peer); + ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketMultiplayerPeer::get_peer_address); + ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketMultiplayerPeer::get_peer_port); + ClassDB::bind_method(D_METHOD("disconnect_peer", "id", "code", "reason"), &WebSocketMultiplayerPeer::disconnect_peer, DEFVAL(1000), DEFVAL("")); + + ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketMultiplayerPeer::get_supported_protocols); + ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketMultiplayerPeer::set_supported_protocols); + + ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketMultiplayerPeer::get_handshake_headers); + ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketMultiplayerPeer::set_handshake_headers); + + ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketMultiplayerPeer::get_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_inbound_buffer_size); + + ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketMultiplayerPeer::get_outbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketMultiplayerPeer::set_outbound_buffer_size); + + ClassDB::bind_method(D_METHOD("get_handshake_timeout"), &WebSocketMultiplayerPeer::get_handshake_timeout); + ClassDB::bind_method(D_METHOD("set_handshake_timeout", "timeout"), &WebSocketMultiplayerPeer::set_handshake_timeout); + + ClassDB::bind_method(D_METHOD("set_max_queued_packets", "max_queued_packets"), &WebSocketMultiplayerPeer::set_max_queued_packets); + ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketMultiplayerPeer::get_max_queued_packets); + + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size"); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "handshake_timeout"), "set_handshake_timeout", "get_handshake_timeout"); - ADD_SIGNAL(MethodInfo("peer_packet", PropertyInfo(Variant::INT, "peer_source"))); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets"); } // // PacketPeer // int WebSocketMultiplayerPeer::get_available_packet_count() const { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, 0, "Please use get_peer(ID).get_available_packet_count to get available packet count from peers when not using the MultiplayerAPI."); - - return _incoming_packets.size(); + return incoming_packets.size(); } Error WebSocketMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, ERR_UNCONFIGURED, "Please use get_peer(ID).get_packet/var to communicate with peers when not using the MultiplayerAPI."); + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED); r_buffer_size = 0; - if (_current_packet.data != nullptr) { - memfree(_current_packet.data); - _current_packet.data = nullptr; + if (current_packet.data != nullptr) { + memfree(current_packet.data); + current_packet.data = nullptr; } - ERR_FAIL_COND_V(_incoming_packets.size() == 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V(incoming_packets.size() == 0, ERR_UNAVAILABLE); - _current_packet = _incoming_packets.front()->get(); - _incoming_packets.pop_front(); + current_packet = incoming_packets.front()->get(); + incoming_packets.pop_front(); - *r_buffer = _current_packet.data; - r_buffer_size = _current_packet.size; + *r_buffer = current_packet.data; + r_buffer_size = current_packet.size; return OK; } Error WebSocketMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, ERR_UNCONFIGURED, "Please use get_peer(ID).put_packet/var to communicate with peers when not using the MultiplayerAPI."); - - Vector<uint8_t> buffer = _make_pkt(SYS_NONE, get_unique_id(), _target_peer, p_buffer, p_buffer_size); + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_CONNECTED, ERR_UNCONFIGURED); if (is_server()) { - return _server_relay(1, _target_peer, &(buffer.ptr()[0]), buffer.size()); + if (target_peer > 0) { + ERR_FAIL_COND_V_MSG(!peers_map.has(target_peer), ERR_INVALID_PARAMETER, "Peer not found: " + itos(target_peer)); + get_peer(target_peer)->put_packet(p_buffer, p_buffer_size); + } else { + for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { + if (target_peer && -target_peer == E.key) { + continue; // Excluded. + } + E.value->put_packet(p_buffer, p_buffer_size); + } + } + return OK; } else { - return get_peer(1)->put_packet(&(buffer.ptr()[0]), buffer.size()); + return get_peer(1)->put_packet(p_buffer, p_buffer_size); } } @@ -106,185 +164,335 @@ Error WebSocketMultiplayerPeer::put_packet(const uint8_t *p_buffer, int p_buffer // MultiplayerPeer // void WebSocketMultiplayerPeer::set_target_peer(int p_target_peer) { - _target_peer = p_target_peer; + target_peer = p_target_peer; } int WebSocketMultiplayerPeer::get_packet_peer() const { - ERR_FAIL_COND_V_MSG(!_is_multiplayer, 1, "This function is not available when not using the MultiplayerAPI."); - ERR_FAIL_COND_V(_incoming_packets.size() == 0, 1); + ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); - return _incoming_packets.front()->get().source; + return incoming_packets.front()->get().source; } int WebSocketMultiplayerPeer::get_unique_id() const { - return _peer_id; + return unique_id; } -void WebSocketMultiplayerPeer::_send_sys(Ref<WebSocketPeer> p_peer, uint8_t p_type, int32_t p_peer_id) { - ERR_FAIL_COND(!p_peer.is_valid()); - ERR_FAIL_COND(!p_peer->is_connected_to_host()); - - Vector<uint8_t> message = _make_pkt(p_type, 1, 0, (uint8_t *)&p_peer_id, 4); - p_peer->put_packet(&(message.ptr()[0]), message.size()); +int WebSocketMultiplayerPeer::get_max_packet_size() const { + return get_outbound_buffer_size() - PROTO_SIZE; } -Vector<uint8_t> WebSocketMultiplayerPeer::_make_pkt(uint8_t p_type, int32_t p_from, int32_t p_to, const uint8_t *p_data, uint32_t p_data_size) { - Vector<uint8_t> out; - out.resize(PROTO_SIZE + p_data_size); - - uint8_t *w = out.ptrw(); - memcpy(&w[0], &p_type, 1); - memcpy(&w[1], &p_from, 4); - memcpy(&w[5], &p_to, 4); - memcpy(&w[PROTO_SIZE], p_data, p_data_size); - - return out; +Error WebSocketMultiplayerPeer::create_server(int p_port, IPAddress p_bind_ip, Ref<CryptoKey> p_tls_key, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE); + _clear(); + tcp_server.instantiate(); + Error err = tcp_server->listen(p_port, p_bind_ip); + if (err != OK) { + tcp_server.unref(); + return err; + } + unique_id = 1; + connection_status = CONNECTION_CONNECTED; + // TLS config + tls_key = p_tls_key; + tls_certificate = p_tls_certificate; + if (tls_key.is_valid() && tls_certificate.is_valid()) { + use_tls = true; + } + return OK; } -void WebSocketMultiplayerPeer::_send_add(int32_t p_peer_id) { - // First of all, confirm the ID! - _send_sys(get_peer(p_peer_id), SYS_ID, p_peer_id); +Error WebSocketMultiplayerPeer::create_client(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate) { + ERR_FAIL_COND_V(get_connection_status() != CONNECTION_DISCONNECTED, ERR_ALREADY_IN_USE); + _clear(); + Ref<WebSocketPeer> peer = _create_peer(); + Error err = peer->connect_to_url(p_url, p_verify_tls, p_tls_certificate); + if (err != OK) { + return err; + } + PendingPeer pending; + pending.time = OS::get_singleton()->get_ticks_msec(); + pending_peers[1] = pending; + peers_map[1] = peer; + connection_status = CONNECTION_CONNECTING; + return OK; +} - // Then send the server peer (which will trigger connection_succeded in client) - _send_sys(get_peer(p_peer_id), SYS_ADD, 1); +bool WebSocketMultiplayerPeer::is_server() const { + return tcp_server.is_valid(); +} - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - int32_t id = E.key; - if (p_peer_id == id) { - continue; // Skip the newly added peer (already confirmed) +void WebSocketMultiplayerPeer::_poll_client() { + ERR_FAIL_COND(connection_status == CONNECTION_DISCONNECTED); // Bug. + ERR_FAIL_COND(!peers_map.has(1) || peers_map[1].is_null()); // Bug. + Ref<WebSocketPeer> peer = peers_map[1]; + peer->poll(); // Update state and fetch packets. + WebSocketPeer::State ready_state = peer->get_ready_state(); + if (ready_state == WebSocketPeer::STATE_OPEN) { + if (connection_status == CONNECTION_CONNECTING) { + if (peer->get_available_packet_count() > 0) { + const uint8_t *in_buffer; + int size = 0; + Error err = peer->get_packet(&in_buffer, size); + if (err != OK || size != 4) { + peer->close(); // Will cause connection error on next poll. + ERR_FAIL_MSG("Invalid ID received from server"); + } + unique_id = *((int32_t *)in_buffer); + if (unique_id < 2) { + peer->close(); // Will cause connection error on next poll. + ERR_FAIL_MSG("Invalid ID received from server"); + } + connection_status = CONNECTION_CONNECTED; + emit_signal("peer_connected", 1); + emit_signal("connection_succeeded"); + } else { + return; // Still waiting for an ID. + } } - - // Send new peer to others - _send_sys(get_peer(id), SYS_ADD, p_peer_id); - // Send others to new peer - _send_sys(get_peer(p_peer_id), SYS_ADD, id); + int pkts = peer->get_available_packet_count(); + while (pkts > 0 && peer->get_ready_state() == WebSocketPeer::STATE_OPEN) { + const uint8_t *in_buffer; + int size = 0; + Error err = peer->get_packet(&in_buffer, size); + ERR_FAIL_COND(err != OK); + ERR_FAIL_COND(size <= 0); + Packet packet; + packet.data = (uint8_t *)memalloc(size); + memcpy(packet.data, in_buffer, size); + packet.size = size; + packet.source = 1; + incoming_packets.push_back(packet); + pkts--; + } + } else if (peer->get_ready_state() == WebSocketPeer::STATE_CLOSED) { + if (connection_status == CONNECTION_CONNECTED) { + emit_signal(SNAME("server_disconnected")); + } else { + emit_signal(SNAME("connection_failed")); + } + _clear(); + return; } -} - -void WebSocketMultiplayerPeer::_send_del(int32_t p_peer_id) { - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - int32_t id = E.key; - if (p_peer_id != id) { - _send_sys(get_peer(id), SYS_DEL, p_peer_id); + if (connection_status == CONNECTION_CONNECTING) { + // Still connecting + ERR_FAIL_COND(!pending_peers.has(1)); // Bug. + if (OS::get_singleton()->get_ticks_msec() - pending_peers[1].time > handshake_timeout) { + print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001)); + emit_signal(SNAME("connection_failed")); + _clear(); + return; } } } -void WebSocketMultiplayerPeer::_store_pkt(int32_t p_source, int32_t p_dest, const uint8_t *p_data, uint32_t p_data_size) { - Packet packet; - packet.data = (uint8_t *)memalloc(p_data_size); - packet.size = p_data_size; - packet.source = p_source; - packet.destination = p_dest; - memcpy(packet.data, &p_data[PROTO_SIZE], p_data_size); - _incoming_packets.push_back(packet); - emit_signal(SNAME("peer_packet"), p_source); -} +void WebSocketMultiplayerPeer::_poll_server() { + ERR_FAIL_COND(connection_status != CONNECTION_CONNECTED); // Bug. + ERR_FAIL_COND(tcp_server.is_null() || !tcp_server->is_listening()); // Bug. + + // Accept new connections. + if (!is_refusing_new_connections() && tcp_server->is_connection_available()) { + PendingPeer peer; + peer.time = OS::get_singleton()->get_ticks_msec(); + peer.tcp = tcp_server->take_connection(); + peer.connection = peer.tcp; + pending_peers[generate_unique_id()] = peer; + } -Error WebSocketMultiplayerPeer::_server_relay(int32_t p_from, int32_t p_to, const uint8_t *p_buffer, uint32_t p_buffer_size) { - if (p_to == 1) { - return OK; // Will not send to self + // Process pending peers. + HashSet<int> to_remove; + for (KeyValue<int, PendingPeer> &E : pending_peers) { + PendingPeer &peer = E.value; + int id = E.key; - } else if (p_to == 0) { - for (KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - if (E.key != p_from) { - E.value->put_packet(p_buffer, p_buffer_size); + if (OS::get_singleton()->get_ticks_msec() - peer.time > handshake_timeout) { + print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", handshake_timeout * 0.001)); + to_remove.insert(id); + continue; + } + + if (peer.ws.is_valid()) { + peer.ws->poll(); + WebSocketPeer::State state = peer.ws->get_ready_state(); + if (state == WebSocketPeer::STATE_OPEN) { + // Connected. + to_remove.insert(id); + if (is_refusing_new_connections()) { + // The user does not want new connections, dropping it. + continue; + } + int32_t peer_id = id; + Error err = peer.ws->put_packet((const uint8_t *)&peer_id, sizeof(peer_id)); + if (err == OK) { + peers_map[id] = peer.ws; + emit_signal("peer_connected", id); + } else { + ERR_PRINT("Failed to send ID to newly connected peer."); + } + continue; + } else if (state == WebSocketPeer::STATE_CONNECTING) { + continue; // Still connecting. + } + to_remove.insert(id); // Error. + continue; + } + if (peer.tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + to_remove.insert(id); // Error. + continue; + } + if (!use_tls) { + peer.ws = _create_peer(); + peer.ws->accept_stream(peer.tcp); + continue; + } else { + if (peer.connection == peer.tcp) { + Ref<StreamPeerTLS> tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); + Error err = tls->accept_stream(peer.tcp, tls_key, tls_certificate); + if (err != OK) { + to_remove.insert(id); + continue; + } + } + Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(peer.connection); + tls->poll(); + if (tls->get_status() == StreamPeerTLS::STATUS_CONNECTED) { + peer.ws = _create_peer(); + peer.ws->accept_stream(peer.connection); + continue; + } else if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + // Still connecting. + continue; + } else { + // Error + to_remove.insert(id); } } - return OK; // Sent to all but sender + } - } else if (p_to < 0) { - for (KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - if (E.key != p_from && E.key != -p_to) { - E.value->put_packet(p_buffer, p_buffer_size); + // Remove disconnected pending peers. + for (const int &pid : to_remove) { + pending_peers.erase(pid); + } + to_remove.clear(); + + // Process connected peers. + for (KeyValue<int, Ref<WebSocketPeer>> &E : peers_map) { + Ref<WebSocketPeer> ws = E.value; + int id = E.key; + ws->poll(); + if (ws->get_ready_state() != WebSocketPeer::STATE_OPEN) { + to_remove.insert(id); // Disconnected. + continue; + } + // Fetch packets + int pkts = ws->get_available_packet_count(); + while (pkts > 0 && ws->get_ready_state() == WebSocketPeer::STATE_OPEN) { + const uint8_t *in_buffer; + int size = 0; + Error err = ws->get_packet(&in_buffer, size); + if (err != OK || size <= 0) { + break; } + Packet packet; + packet.data = (uint8_t *)memalloc(size); + memcpy(packet.data, in_buffer, size); + packet.size = size; + packet.source = E.key; + incoming_packets.push_back(packet); + pkts--; } - return OK; // Sent to all but sender and excluded + } + + // Remove disconnected peers. + for (const int &pid : to_remove) { + emit_signal(SNAME("peer_disconnected"), pid); + peers_map.erase(pid); + } +} +void WebSocketMultiplayerPeer::poll() { + if (connection_status == CONNECTION_DISCONNECTED) { + return; + } + if (is_server()) { + _poll_server(); } else { - ERR_FAIL_COND_V(p_to == p_from, FAILED); + _poll_client(); + } +} - Ref<WebSocketPeer> peer_to = get_peer(p_to); - ERR_FAIL_COND_V(peer_to.is_null(), FAILED); +MultiplayerPeer::ConnectionStatus WebSocketMultiplayerPeer::get_connection_status() const { + return connection_status; +} - return peer_to->put_packet(p_buffer, p_buffer_size); // Sending to specific peer - } +Ref<WebSocketPeer> WebSocketMultiplayerPeer::get_peer(int p_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_id), Ref<WebSocketPeer>()); + return peers_map[p_id]; } -void WebSocketMultiplayerPeer::_process_multiplayer(Ref<WebSocketPeer> p_peer, uint32_t p_peer_id) { - ERR_FAIL_COND(!p_peer.is_valid()); +void WebSocketMultiplayerPeer::set_supported_protocols(const Vector<String> &p_protocols) { + peer_config->set_supported_protocols(p_protocols); +} - const uint8_t *in_buffer; - int size = 0; - int data_size = 0; +Vector<String> WebSocketMultiplayerPeer::get_supported_protocols() const { + return peer_config->get_supported_protocols(); +} - Error err = p_peer->get_packet(&in_buffer, size); +void WebSocketMultiplayerPeer::set_handshake_headers(const Vector<String> &p_headers) { + peer_config->set_handshake_headers(p_headers); +} - ERR_FAIL_COND(err != OK); - ERR_FAIL_COND(size < PROTO_SIZE); +Vector<String> WebSocketMultiplayerPeer::get_handshake_headers() const { + return peer_config->get_handshake_headers(); +} - data_size = size - PROTO_SIZE; +void WebSocketMultiplayerPeer::set_outbound_buffer_size(int p_buffer_size) { + peer_config->set_outbound_buffer_size(p_buffer_size); +} - uint8_t type = 0; - uint32_t from = 0; - int32_t to = 0; - memcpy(&type, in_buffer, 1); - memcpy(&from, &in_buffer[1], 4); - memcpy(&to, &in_buffer[5], 4); +int WebSocketMultiplayerPeer::get_outbound_buffer_size() const { + return peer_config->get_outbound_buffer_size(); +} - if (is_server()) { // Server can resend +void WebSocketMultiplayerPeer::set_inbound_buffer_size(int p_buffer_size) { + peer_config->set_inbound_buffer_size(p_buffer_size); +} - ERR_FAIL_COND(type != SYS_NONE); // Only server sends sys messages - ERR_FAIL_COND(from != p_peer_id); // Someone is cheating +int WebSocketMultiplayerPeer::get_inbound_buffer_size() const { + return peer_config->get_inbound_buffer_size(); +} - if (to == 1) { // This is for the server +void WebSocketMultiplayerPeer::set_max_queued_packets(int p_max_queued_packets) { + peer_config->set_max_queued_packets(p_max_queued_packets); +} - _store_pkt(from, to, in_buffer, data_size); +int WebSocketMultiplayerPeer::get_max_queued_packets() const { + return peer_config->get_max_queued_packets(); +} - } else if (to == 0) { - // Broadcast, for us too - _store_pkt(from, to, in_buffer, data_size); +float WebSocketMultiplayerPeer::get_handshake_timeout() const { + return handshake_timeout / 1000.0; +} - } else if (to < 0) { - // All but one, for us if not excluded - if (_peer_id != -(int32_t)p_peer_id) { - _store_pkt(from, to, in_buffer, data_size); - } - } - // Relay if needed (i.e. "to" includes a peer that is not the server) - _server_relay(from, to, in_buffer, size); +void WebSocketMultiplayerPeer::set_handshake_timeout(float p_timeout) { + ERR_FAIL_COND(p_timeout <= 0.0); + handshake_timeout = p_timeout * 1000; +} - } else { - if (type == SYS_NONE) { // Payload message +IPAddress WebSocketMultiplayerPeer::get_peer_address(int p_peer_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_peer_id), IPAddress()); + return peers_map[p_peer_id]->get_connected_host(); +} - _store_pkt(from, to, in_buffer, data_size); - return; - } +int WebSocketMultiplayerPeer::get_peer_port(int p_peer_id) const { + ERR_FAIL_COND_V(!peers_map.has(p_peer_id), 0); + return peers_map[p_peer_id]->get_connected_port(); +} - // System message - ERR_FAIL_COND(data_size < 4); - int id = 0; - memcpy(&id, &in_buffer[PROTO_SIZE], 4); - - switch (type) { - case SYS_ADD: // Add peer - _peer_map[id] = Ref<WebSocketPeer>(); - emit_signal(SNAME("peer_connected"), id); - if (id == 1) { // We just connected to the server - emit_signal(SNAME("connection_succeeded")); - } - break; +void WebSocketMultiplayerPeer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { + ERR_FAIL_COND(!peers_map.has(p_peer_id)); + peers_map[p_peer_id]->close(p_code, p_reason); +} - case SYS_DEL: // Remove peer - _peer_map.erase(id); - emit_signal(SNAME("peer_disconnected"), id); - break; - case SYS_ID: // Hello, server assigned ID - _peer_id = id; - break; - default: - ERR_FAIL_MSG("Invalid multiplayer message."); - break; - } - } +void WebSocketMultiplayerPeer::close() { + _clear(); } diff --git a/modules/websocket/websocket_multiplayer_peer.h b/modules/websocket/websocket_multiplayer_peer.h index 3259e78b3b..ebe013a7bf 100644 --- a/modules/websocket/websocket_multiplayer_peer.h +++ b/modules/websocket/websocket_multiplayer_peer.h @@ -32,6 +32,8 @@ #define WEBSOCKET_MULTIPLAYER_PEER_H #include "core/error/error_list.h" +#include "core/io/stream_peer_tls.h" +#include "core/io/tcp_server.h" #include "core/templates/list.h" #include "scene/main/multiplayer_peer.h" #include "websocket_peer.h" @@ -40,9 +42,7 @@ class WebSocketMultiplayerPeer : public MultiplayerPeer { GDCLASS(WebSocketMultiplayerPeer, MultiplayerPeer); private: - Vector<uint8_t> _make_pkt(uint8_t p_type, int32_t p_from, int32_t p_to, const uint8_t *p_data, uint32_t p_data_size); - void _store_pkt(int32_t p_source, int32_t p_dest, const uint8_t *p_data, uint32_t p_data_size); - Error _server_relay(int32_t p_from, int32_t p_to, const uint8_t *p_buffer, uint32_t p_buffer_size); + Ref<WebSocketPeer> _create_peer(); protected: enum { @@ -56,30 +56,53 @@ protected: struct Packet { int source = 0; - int destination = 0; uint8_t *data = nullptr; uint32_t size = 0; }; - List<Packet> _incoming_packets; - HashMap<int, Ref<WebSocketPeer>> _peer_map; - Packet _current_packet; + struct PendingPeer { + uint64_t time = 0; + Ref<StreamPeerTCP> tcp; + Ref<StreamPeer> connection; + Ref<WebSocketPeer> ws; + }; + + uint64_t handshake_timeout = 3000; + Ref<WebSocketPeer> peer_config; + HashMap<int, PendingPeer> pending_peers; + Ref<TCPServer> tcp_server; + bool use_tls = false; + Ref<X509Certificate> tls_certificate; + Ref<CryptoKey> tls_key; + + ConnectionStatus connection_status = CONNECTION_DISCONNECTED; + + List<Packet> incoming_packets; + HashMap<int, Ref<WebSocketPeer>> peers_map; + Packet current_packet; - bool _is_multiplayer = false; - int _target_peer = 0; - int _peer_id = 0; + int target_peer = 0; + int unique_id = 0; static void _bind_methods(); - void _send_add(int32_t p_peer_id); - void _send_sys(Ref<WebSocketPeer> p_peer, uint8_t p_type, int32_t p_peer_id); - void _send_del(int32_t p_peer_id); + void _poll_client(); + void _poll_server(); + void _clear(); public: /* MultiplayerPeer */ - void set_target_peer(int p_target_peer) override; - int get_packet_peer() const override; - int get_unique_id() const override; + virtual void set_target_peer(int p_target_peer) override; + virtual int get_packet_peer() const override; + virtual int get_packet_channel() const override { return 0; } + virtual TransferMode get_packet_mode() const override { return TRANSFER_MODE_RELIABLE; } + virtual int get_unique_id() const override; + virtual bool is_server_relay_supported() const override { return true; } + + virtual int get_max_packet_size() const override; + virtual bool is_server() const override; + virtual void poll() override; + virtual ConnectionStatus get_connection_status() const override; /* PacketPeer */ virtual int get_available_packet_count() const override; @@ -87,11 +110,33 @@ public: virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; /* WebSocketPeer */ - virtual Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) = 0; - virtual Ref<WebSocketPeer> get_peer(int p_peer_id) const = 0; + virtual Ref<WebSocketPeer> get_peer(int p_peer_id) const; - void _process_multiplayer(Ref<WebSocketPeer> p_peer, uint32_t p_peer_id); - void _clear(); + Error create_client(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_tls_certificate); + Error create_server(int p_port, IPAddress p_bind_ip, Ref<CryptoKey> p_tls_key, Ref<X509Certificate> p_tls_certificate); + + void set_supported_protocols(const Vector<String> &p_protocols); + Vector<String> get_supported_protocols() const; + + void set_handshake_headers(const Vector<String> &p_headers); + Vector<String> get_handshake_headers() const; + + void set_outbound_buffer_size(int p_buffer_size); + int get_outbound_buffer_size() const; + + void set_inbound_buffer_size(int p_buffer_size); + int get_inbound_buffer_size() const; + + float get_handshake_timeout() const; + void set_handshake_timeout(float p_timeout); + + IPAddress get_peer_address(int p_peer_id) const; + int get_peer_port(int p_peer_id) const; + void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = ""); + void close(); + + void set_max_queued_packets(int p_max_queued_packets); + int get_max_queued_packets() const; WebSocketMultiplayerPeer(); ~WebSocketMultiplayerPeer(); diff --git a/modules/websocket/websocket_peer.cpp b/modules/websocket/websocket_peer.cpp index a0af9303b8..b46b20bef2 100644 --- a/modules/websocket/websocket_peer.cpp +++ b/modules/websocket/websocket_peer.cpp @@ -30,7 +30,7 @@ #include "websocket_peer.h" -GDCINULL(WebSocketPeer); +WebSocketPeer *(*WebSocketPeer::_create)() = nullptr; WebSocketPeer::WebSocketPeer() { } @@ -39,16 +39,115 @@ WebSocketPeer::~WebSocketPeer() { } void WebSocketPeer::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_write_mode"), &WebSocketPeer::get_write_mode); - ClassDB::bind_method(D_METHOD("set_write_mode", "mode"), &WebSocketPeer::set_write_mode); - ClassDB::bind_method(D_METHOD("is_connected_to_host"), &WebSocketPeer::is_connected_to_host); + ClassDB::bind_method(D_METHOD("connect_to_url", "url", "verify_tls", "trusted_tls_certificate"), &WebSocketPeer::connect_to_url, DEFVAL(true), DEFVAL(Ref<X509Certificate>())); + ClassDB::bind_method(D_METHOD("accept_stream", "stream"), &WebSocketPeer::accept_stream); + ClassDB::bind_method(D_METHOD("send", "message", "write_mode"), &WebSocketPeer::_send_bind, DEFVAL(WRITE_MODE_BINARY)); + ClassDB::bind_method(D_METHOD("send_text", "message"), &WebSocketPeer::send_text); ClassDB::bind_method(D_METHOD("was_string_packet"), &WebSocketPeer::was_string_packet); + ClassDB::bind_method(D_METHOD("poll"), &WebSocketPeer::poll); ClassDB::bind_method(D_METHOD("close", "code", "reason"), &WebSocketPeer::close, DEFVAL(1000), DEFVAL("")); ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketPeer::get_connected_host); ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketPeer::get_connected_port); + ClassDB::bind_method(D_METHOD("get_selected_protocol"), &WebSocketPeer::get_selected_protocol); + ClassDB::bind_method(D_METHOD("get_requested_url"), &WebSocketPeer::get_requested_url); ClassDB::bind_method(D_METHOD("set_no_delay", "enabled"), &WebSocketPeer::set_no_delay); ClassDB::bind_method(D_METHOD("get_current_outbound_buffered_amount"), &WebSocketPeer::get_current_outbound_buffered_amount); + ClassDB::bind_method(D_METHOD("get_ready_state"), &WebSocketPeer::get_ready_state); + ClassDB::bind_method(D_METHOD("get_close_code"), &WebSocketPeer::get_close_code); + ClassDB::bind_method(D_METHOD("get_close_reason"), &WebSocketPeer::get_close_reason); + + ClassDB::bind_method(D_METHOD("get_supported_protocols"), &WebSocketPeer::_get_supported_protocols); + ClassDB::bind_method(D_METHOD("set_supported_protocols", "protocols"), &WebSocketPeer::set_supported_protocols); + ClassDB::bind_method(D_METHOD("get_handshake_headers"), &WebSocketPeer::_get_handshake_headers); + ClassDB::bind_method(D_METHOD("set_handshake_headers", "protocols"), &WebSocketPeer::set_handshake_headers); + + ClassDB::bind_method(D_METHOD("get_inbound_buffer_size"), &WebSocketPeer::get_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_inbound_buffer_size", "buffer_size"), &WebSocketPeer::set_inbound_buffer_size); + ClassDB::bind_method(D_METHOD("get_outbound_buffer_size"), &WebSocketPeer::get_outbound_buffer_size); + ClassDB::bind_method(D_METHOD("set_outbound_buffer_size", "buffer_size"), &WebSocketPeer::set_outbound_buffer_size); + + ClassDB::bind_method(D_METHOD("set_max_queued_packets", "buffer_size"), &WebSocketPeer::set_max_queued_packets); + ClassDB::bind_method(D_METHOD("get_max_queued_packets"), &WebSocketPeer::get_max_queued_packets); + + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "supported_protocols"), "set_supported_protocols", "get_supported_protocols"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "handshake_headers"), "set_handshake_headers", "get_handshake_headers"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "inbound_buffer_size"), "set_inbound_buffer_size", "get_inbound_buffer_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "outbound_buffer_size"), "set_outbound_buffer_size", "get_outbound_buffer_size"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_queued_packets"), "set_max_queued_packets", "get_max_queued_packets"); + BIND_ENUM_CONSTANT(WRITE_MODE_TEXT); BIND_ENUM_CONSTANT(WRITE_MODE_BINARY); + + BIND_ENUM_CONSTANT(STATE_CONNECTING); + BIND_ENUM_CONSTANT(STATE_OPEN); + BIND_ENUM_CONSTANT(STATE_CLOSING); + BIND_ENUM_CONSTANT(STATE_CLOSED); +} + +Error WebSocketPeer::_send_bind(const PackedByteArray &p_message, WriteMode p_mode) { + return send(p_message.ptr(), p_message.size(), p_mode); +} + +Error WebSocketPeer::send_text(const String &p_text) { + const CharString cs = p_text.utf8(); + return send((const uint8_t *)cs.ptr(), cs.length(), WRITE_MODE_TEXT); +} + +void WebSocketPeer::set_supported_protocols(const Vector<String> &p_protocols) { + // Strip edges from protocols. + supported_protocols.resize(p_protocols.size()); + for (int i = 0; i < p_protocols.size(); i++) { + supported_protocols.write[i] = p_protocols[i].strip_edges(); + } +} + +const Vector<String> WebSocketPeer::get_supported_protocols() const { + return supported_protocols; +} + +Vector<String> WebSocketPeer::_get_supported_protocols() const { + Vector<String> out; + out.append_array(supported_protocols); + return out; +} + +void WebSocketPeer::set_handshake_headers(const Vector<String> &p_headers) { + handshake_headers = p_headers; +} + +const Vector<String> WebSocketPeer::get_handshake_headers() const { + return handshake_headers; +} + +Vector<String> WebSocketPeer::_get_handshake_headers() const { + Vector<String> out; + out.append_array(handshake_headers); + return out; +} + +void WebSocketPeer::set_outbound_buffer_size(int p_buffer_size) { + outbound_buffer_size = p_buffer_size; +} + +int WebSocketPeer::get_outbound_buffer_size() const { + return outbound_buffer_size; +} + +void WebSocketPeer::set_inbound_buffer_size(int p_buffer_size) { + inbound_buffer_size = p_buffer_size; +} + +int WebSocketPeer::get_inbound_buffer_size() const { + return inbound_buffer_size; +} + +void WebSocketPeer::set_max_queued_packets(int p_max_queued_packets) { + max_queued_packets = p_max_queued_packets; +} + +int WebSocketPeer::get_max_queued_packets() const { + return max_queued_packets; } diff --git a/modules/websocket/websocket_peer.h b/modules/websocket/websocket_peer.h index 22099f7258..db969dd08e 100644 --- a/modules/websocket/websocket_peer.h +++ b/modules/websocket/websocket_peer.h @@ -31,40 +31,97 @@ #ifndef WEBSOCKET_PEER_H #define WEBSOCKET_PEER_H +#include "core/crypto/crypto.h" #include "core/error/error_list.h" #include "core/io/packet_peer.h" -#include "websocket_macros.h" class WebSocketPeer : public PacketPeer { GDCLASS(WebSocketPeer, PacketPeer); - GDCICLASS(WebSocketPeer); public: + enum State { + STATE_CONNECTING, + STATE_OPEN, + STATE_CLOSING, + STATE_CLOSED + }; + enum WriteMode { WRITE_MODE_TEXT, WRITE_MODE_BINARY, }; + enum { + DEFAULT_BUFFER_SIZE = 65535, + }; + +private: + virtual Error _send_bind(const PackedByteArray &p_data, WriteMode p_mode = WRITE_MODE_BINARY); + protected: + static WebSocketPeer *(*_create)(); + static void _bind_methods(); + Vector<String> supported_protocols; + Vector<String> handshake_headers; + + Vector<String> _get_supported_protocols() const; + Vector<String> _get_handshake_headers() const; + + int outbound_buffer_size = DEFAULT_BUFFER_SIZE; + int inbound_buffer_size = DEFAULT_BUFFER_SIZE; + int max_queued_packets = 2048; + public: - virtual WriteMode get_write_mode() const = 0; - virtual void set_write_mode(WriteMode p_mode) = 0; + static WebSocketPeer *create() { + if (!_create) { + return nullptr; + } + return _create(); + } + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) { return ERR_UNAVAILABLE; }; + virtual Error accept_stream(Ref<StreamPeer> p_stream) = 0; + + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) = 0; virtual void close(int p_code = 1000, String p_reason = "") = 0; - virtual bool is_connected_to_host() const = 0; virtual IPAddress get_connected_host() const = 0; virtual uint16_t get_connected_port() const = 0; virtual bool was_string_packet() const = 0; virtual void set_no_delay(bool p_enabled) = 0; virtual int get_current_outbound_buffered_amount() const = 0; + virtual String get_selected_protocol() const = 0; + virtual String get_requested_url() const = 0; + + virtual void poll() = 0; + virtual State get_ready_state() const = 0; + virtual int get_close_code() const = 0; + virtual String get_close_reason() const = 0; + + Error send_text(const String &p_text); + + void set_supported_protocols(const Vector<String> &p_protocols); + const Vector<String> get_supported_protocols() const; + + void set_handshake_headers(const Vector<String> &p_headers); + const Vector<String> get_handshake_headers() const; + + void set_outbound_buffer_size(int p_buffer_size); + int get_outbound_buffer_size() const; + + void set_inbound_buffer_size(int p_buffer_size); + int get_inbound_buffer_size() const; + + void set_max_queued_packets(int p_max_queued_packets); + int get_max_queued_packets() const; WebSocketPeer(); ~WebSocketPeer(); }; VARIANT_ENUM_CAST(WebSocketPeer::WriteMode); +VARIANT_ENUM_CAST(WebSocketPeer::State); #endif // WEBSOCKET_PEER_H diff --git a/modules/websocket/websocket_server.cpp b/modules/websocket/websocket_server.cpp deleted file mode 100644 index 25a6e420fc..0000000000 --- a/modules/websocket/websocket_server.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/*************************************************************************/ -/* websocket_server.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#include "websocket_server.h" - -GDCINULL(WebSocketServer); - -WebSocketServer::WebSocketServer() { - _peer_id = 1; - bind_ip = IPAddress("*"); -} - -WebSocketServer::~WebSocketServer() { -} - -void WebSocketServer::_bind_methods() { - ClassDB::bind_method(D_METHOD("is_listening"), &WebSocketServer::is_listening); - ClassDB::bind_method(D_METHOD("set_extra_headers", "headers"), &WebSocketServer::set_extra_headers, DEFVAL(Vector<String>())); - ClassDB::bind_method(D_METHOD("listen", "port", "protocols", "gd_mp_api"), &WebSocketServer::listen, DEFVAL(Vector<String>()), DEFVAL(false)); - ClassDB::bind_method(D_METHOD("stop"), &WebSocketServer::stop); - ClassDB::bind_method(D_METHOD("has_peer", "id"), &WebSocketServer::has_peer); - ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketServer::get_peer_address); - ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketServer::get_peer_port); - ClassDB::bind_method(D_METHOD("disconnect_peer", "id", "code", "reason"), &WebSocketServer::disconnect_peer, DEFVAL(1000), DEFVAL("")); - - ClassDB::bind_method(D_METHOD("get_bind_ip"), &WebSocketServer::get_bind_ip); - ClassDB::bind_method(D_METHOD("set_bind_ip", "ip"), &WebSocketServer::set_bind_ip); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "bind_ip"), "set_bind_ip", "get_bind_ip"); - - ClassDB::bind_method(D_METHOD("get_private_key"), &WebSocketServer::get_private_key); - ClassDB::bind_method(D_METHOD("set_private_key", "key"), &WebSocketServer::set_private_key); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "private_key", PROPERTY_HINT_RESOURCE_TYPE, "CryptoKey", PROPERTY_USAGE_NONE), "set_private_key", "get_private_key"); - - ClassDB::bind_method(D_METHOD("get_tls_certificate"), &WebSocketServer::get_tls_certificate); - ClassDB::bind_method(D_METHOD("set_tls_certificate", "cert"), &WebSocketServer::set_tls_certificate); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tls_certificate", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_tls_certificate", "get_tls_certificate"); - - ClassDB::bind_method(D_METHOD("get_ca_chain"), &WebSocketServer::get_ca_chain); - ClassDB::bind_method(D_METHOD("set_ca_chain", "ca_chain"), &WebSocketServer::set_ca_chain); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "ca_chain", PROPERTY_HINT_RESOURCE_TYPE, "X509Certificate", PROPERTY_USAGE_NONE), "set_ca_chain", "get_ca_chain"); - - ClassDB::bind_method(D_METHOD("get_handshake_timeout"), &WebSocketServer::get_handshake_timeout); - ClassDB::bind_method(D_METHOD("set_handshake_timeout", "timeout"), &WebSocketServer::set_handshake_timeout); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "handshake_timeout"), "set_handshake_timeout", "get_handshake_timeout"); - - ADD_SIGNAL(MethodInfo("client_close_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); - ADD_SIGNAL(MethodInfo("client_disconnected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::BOOL, "was_clean_close"))); - ADD_SIGNAL(MethodInfo("client_connected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "protocol"), PropertyInfo(Variant::STRING, "resource_name"))); - ADD_SIGNAL(MethodInfo("data_received", PropertyInfo(Variant::INT, "id"))); -} - -IPAddress WebSocketServer::get_bind_ip() const { - return bind_ip; -} - -void WebSocketServer::set_bind_ip(const IPAddress &p_bind_ip) { - ERR_FAIL_COND(is_listening()); - ERR_FAIL_COND(!p_bind_ip.is_valid() && !p_bind_ip.is_wildcard()); - bind_ip = p_bind_ip; -} - -Ref<CryptoKey> WebSocketServer::get_private_key() const { - return private_key; -} - -void WebSocketServer::set_private_key(Ref<CryptoKey> p_key) { - ERR_FAIL_COND(is_listening()); - private_key = p_key; -} - -Ref<X509Certificate> WebSocketServer::get_tls_certificate() const { - return tls_cert; -} - -void WebSocketServer::set_tls_certificate(Ref<X509Certificate> p_cert) { - ERR_FAIL_COND(is_listening()); - tls_cert = p_cert; -} - -Ref<X509Certificate> WebSocketServer::get_ca_chain() const { - return ca_chain; -} - -void WebSocketServer::set_ca_chain(Ref<X509Certificate> p_ca_chain) { - ERR_FAIL_COND(is_listening()); - ca_chain = p_ca_chain; -} - -float WebSocketServer::get_handshake_timeout() const { - return handshake_timeout / 1000.0; -} - -void WebSocketServer::set_handshake_timeout(float p_timeout) { - ERR_FAIL_COND(p_timeout <= 0.0); - handshake_timeout = p_timeout * 1000; -} - -MultiplayerPeer::ConnectionStatus WebSocketServer::get_connection_status() const { - if (is_listening()) { - return CONNECTION_CONNECTED; - } - - return CONNECTION_DISCONNECTED; -} - -bool WebSocketServer::is_server() const { - return true; -} - -void WebSocketServer::_on_peer_packet(int32_t p_peer_id) { - if (_is_multiplayer) { - _process_multiplayer(get_peer(p_peer_id), p_peer_id); - } else { - emit_signal(SNAME("data_received"), p_peer_id); - } -} - -void WebSocketServer::_on_connect(int32_t p_peer_id, String p_protocol, String p_resource_name) { - if (_is_multiplayer) { - // Send add to clients - _send_add(p_peer_id); - emit_signal(SNAME("peer_connected"), p_peer_id); - } else { - emit_signal(SNAME("client_connected"), p_peer_id, p_protocol, p_resource_name); - } -} - -void WebSocketServer::_on_disconnect(int32_t p_peer_id, bool p_was_clean) { - if (_is_multiplayer) { - // Send delete to clients - _send_del(p_peer_id); - emit_signal(SNAME("peer_disconnected"), p_peer_id); - } else { - emit_signal(SNAME("client_disconnected"), p_peer_id, p_was_clean); - } -} - -void WebSocketServer::_on_close_request(int32_t p_peer_id, int p_code, String p_reason) { - emit_signal(SNAME("client_close_request"), p_peer_id, p_code, p_reason); -} diff --git a/modules/websocket/websocket_server.h b/modules/websocket/websocket_server.h deleted file mode 100644 index de23ee884d..0000000000 --- a/modules/websocket/websocket_server.h +++ /dev/null @@ -1,90 +0,0 @@ -/*************************************************************************/ -/* websocket_server.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEBSOCKET_SERVER_H -#define WEBSOCKET_SERVER_H - -#include "core/crypto/crypto.h" -#include "core/object/ref_counted.h" -#include "websocket_multiplayer_peer.h" -#include "websocket_peer.h" - -class WebSocketServer : public WebSocketMultiplayerPeer { - GDCLASS(WebSocketServer, WebSocketMultiplayerPeer); - GDCICLASS(WebSocketServer); - - IPAddress bind_ip; - -protected: - static void _bind_methods(); - - Ref<CryptoKey> private_key; - Ref<X509Certificate> tls_cert; - Ref<X509Certificate> ca_chain; - uint32_t handshake_timeout = 3000; - -public: - virtual void set_extra_headers(const Vector<String> &p_headers) = 0; - virtual Error listen(int p_port, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false) = 0; - virtual void stop() = 0; - virtual bool is_listening() const = 0; - virtual bool has_peer(int p_id) const = 0; - virtual bool is_server() const override; - ConnectionStatus get_connection_status() const override; - - virtual IPAddress get_peer_address(int p_peer_id) const = 0; - virtual int get_peer_port(int p_peer_id) const = 0; - virtual void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = "") = 0; - - void _on_peer_packet(int32_t p_peer_id); - void _on_connect(int32_t p_peer_id, String p_protocol, String p_resource_name); - void _on_disconnect(int32_t p_peer_id, bool p_was_clean); - void _on_close_request(int32_t p_peer_id, int p_code, String p_reason); - - IPAddress get_bind_ip() const; - void set_bind_ip(const IPAddress &p_bind_ip); - - Ref<CryptoKey> get_private_key() const; - void set_private_key(Ref<CryptoKey> p_key); - - Ref<X509Certificate> get_tls_certificate() const; - void set_tls_certificate(Ref<X509Certificate> p_cert); - - Ref<X509Certificate> get_ca_chain() const; - void set_ca_chain(Ref<X509Certificate> p_ca_chain); - - float get_handshake_timeout() const; - void set_handshake_timeout(float p_timeout); - - WebSocketServer(); - ~WebSocketServer(); -}; - -#endif // WEBSOCKET_SERVER_H diff --git a/modules/websocket/wsl_client.cpp b/modules/websocket/wsl_client.cpp deleted file mode 100644 index 50ef53e267..0000000000 --- a/modules/websocket/wsl_client.cpp +++ /dev/null @@ -1,407 +0,0 @@ -/*************************************************************************/ -/* wsl_client.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEB_ENABLED - -#include "wsl_client.h" -#include "core/config/project_settings.h" -#include "core/io/ip.h" - -void WSLClient::_do_handshake() { - if (_requested < _request.size() - 1) { - int sent = 0; - Error err = _connection->put_partial_data(((const uint8_t *)_request.get_data() + _requested), _request.size() - _requested - 1, sent); - // Sending handshake failed - if (err != OK) { - disconnect_from_host(); - _on_error(); - return; - } - _requested += sent; - - } else { - int read = 0; - while (true) { - if (_resp_pos >= WSL_MAX_HEADER_SIZE) { - // Header is too big - disconnect_from_host(); - _on_error(); - ERR_FAIL_MSG("Response headers too big."); - } - Error err = _connection->get_partial_data(&_resp_buf[_resp_pos], 1, read); - if (err == ERR_FILE_EOF) { - // We got a disconnect. - disconnect_from_host(); - _on_error(); - return; - } else if (err != OK) { - // Got some error. - disconnect_from_host(); - _on_error(); - return; - } else if (read != 1) { - // Busy, wait next poll. - break; - } - // Check "\r\n\r\n" header terminator - char *r = (char *)_resp_buf; - int l = _resp_pos; - if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { - r[l - 3] = '\0'; - String protocol; - // Response is over, verify headers and create peer. - if (!_verify_headers(protocol)) { - disconnect_from_host(); - _on_error(); - ERR_FAIL_MSG("Invalid response headers."); - } - // Create peer. - WSLPeer::PeerData *data = memnew(struct WSLPeer::PeerData); - data->obj = this; - data->conn = _connection; - data->tcp = _tcp; - data->is_server = false; - data->id = 1; - _peer->make_context(data, _in_buf_size, _in_pkt_size, _out_buf_size, _out_pkt_size); - _peer->set_no_delay(true); - _status = CONNECTION_CONNECTED; - _on_connect(protocol); - break; - } - _resp_pos += 1; - } - } -} - -bool WSLClient::_verify_headers(String &r_protocol) { - String s = (char *)_resp_buf; - Vector<String> psa = s.split("\r\n"); - int len = psa.size(); - ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers. Got: " + itos(len) + ", expected >= 4."); - - Vector<String> req = psa[0].split(" ", false); - ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code. Got '" + psa[0] + "', expected 'HTTP/1.1 101'."); - - // Wrong protocol - ERR_FAIL_COND_V_MSG(req[0] != "HTTP/1.1", false, "Invalid protocol. Got: '" + req[0] + "', expected 'HTTP/1.1'."); - ERR_FAIL_COND_V_MSG(req[1] != "101", false, "Invalid status code. Got: '" + req[1] + "', expected '101'."); - - HashMap<String, String> headers; - for (int i = 1; i < len; i++) { - Vector<String> header = psa[i].split(":", false, 1); - ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i] + "."); - String name = header[0].to_lower(); - String value = header[1].strip_edges(); - if (headers.has(name)) { - headers[name] += "," + value; - } else { - headers[name] = value; - } - } - -#define WSL_CHECK(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); -#define WSL_CHECK_NC(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME] != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); - WSL_CHECK("connection", "upgrade"); - WSL_CHECK("upgrade", "websocket"); - WSL_CHECK_NC("sec-websocket-accept", WSLPeer::compute_key_response(_key)); -#undef WSL_CHECK_NC -#undef WSL_CHECK - if (_protocols.size() == 0) { - // We didn't request a custom protocol - ERR_FAIL_COND_V_MSG(headers.has("sec-websocket-protocol"), false, "Received unrequested sub-protocol -> " + headers["sec-websocket-protocol"]); - } else { - // We requested at least one custom protocol but didn't receive one - ERR_FAIL_COND_V_MSG(!headers.has("sec-websocket-protocol"), false, "Requested sub-protocol(s) but received none."); - // Check received sub-protocol was one of those requested. - r_protocol = headers["sec-websocket-protocol"]; - bool valid = false; - for (int i = 0; i < _protocols.size(); i++) { - if (_protocols[i] != r_protocol) { - continue; - } - valid = true; - break; - } - if (!valid) { - ERR_FAIL_V_MSG(false, "Received unrequested sub-protocol -> " + r_protocol); - return false; - } - } - return true; -} - -Error WSLClient::connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocols, const Vector<String> p_custom_headers) { - ERR_FAIL_COND_V(_connection.is_valid(), ERR_ALREADY_IN_USE); - ERR_FAIL_COND_V(p_path.is_empty(), ERR_INVALID_PARAMETER); - - _peer = Ref<WSLPeer>(memnew(WSLPeer)); - - if (p_host.is_valid_ip_address()) { - _ip_candidates.push_back(IPAddress(p_host)); - } else { - // Queue hostname for resolution. - _resolver_id = IP::get_singleton()->resolve_hostname_queue_item(p_host); - ERR_FAIL_COND_V(_resolver_id == IP::RESOLVER_INVALID_ID, ERR_INVALID_PARAMETER); - // Check if it was found in cache. - IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(_resolver_id); - if (ip_status == IP::RESOLVER_STATUS_DONE) { - _ip_candidates = IP::get_singleton()->get_resolve_item_addresses(_resolver_id); - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - } - } - - // We assume OK while hostname resolution is pending. - Error err = _resolver_id != IP::RESOLVER_INVALID_ID ? OK : FAILED; - while (_ip_candidates.size()) { - err = _tcp->connect_to_host(_ip_candidates.pop_front(), p_port); - if (err == OK) { - break; - } - } - if (err != OK) { - _tcp->disconnect_from_host(); - _on_error(); - return err; - } - _connection = _tcp; - _use_tls = p_tls; - _host = p_host; - _port = p_port; - // Strip edges from protocols. - _protocols.resize(p_protocols.size()); - String *pw = _protocols.ptrw(); - for (int i = 0; i < p_protocols.size(); i++) { - pw[i] = p_protocols[i].strip_edges(); - } - - _key = WSLPeer::generate_key(); - String request = "GET " + p_path + " HTTP/1.1\r\n"; - String port = ""; - if ((p_port != 80 && !p_tls) || (p_port != 443 && p_tls)) { - port = ":" + itos(p_port); - } - request += "Host: " + p_host + port + "\r\n"; - request += "Upgrade: websocket\r\n"; - request += "Connection: Upgrade\r\n"; - request += "Sec-WebSocket-Key: " + _key + "\r\n"; - request += "Sec-WebSocket-Version: 13\r\n"; - if (p_protocols.size() > 0) { - request += "Sec-WebSocket-Protocol: "; - for (int i = 0; i < p_protocols.size(); i++) { - if (i != 0) { - request += ","; - } - request += p_protocols[i]; - } - request += "\r\n"; - } - for (int i = 0; i < p_custom_headers.size(); i++) { - request += p_custom_headers[i] + "\r\n"; - } - request += "\r\n"; - _request = request.utf8(); - _status = CONNECTION_CONNECTING; - - return OK; -} - -int WSLClient::get_max_packet_size() const { - return (1 << _out_buf_size) - PROTO_SIZE; -} - -void WSLClient::poll() { - if (_resolver_id != IP::RESOLVER_INVALID_ID) { - IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(_resolver_id); - if (ip_status == IP::RESOLVER_STATUS_WAITING) { - return; - } - // Anything else is either a candidate or a failure. - Error err = FAILED; - if (ip_status == IP::RESOLVER_STATUS_DONE) { - _ip_candidates = IP::get_singleton()->get_resolve_item_addresses(_resolver_id); - while (_ip_candidates.size()) { - err = _tcp->connect_to_host(_ip_candidates.pop_front(), _port); - if (err == OK) { - break; - } - } - } - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - if (err != OK) { - disconnect_from_host(); - _on_error(); - return; - } - } - if (_peer->is_connected_to_host()) { - _peer->poll(); - if (!_peer->is_connected_to_host()) { - disconnect_from_host(); - _on_disconnect(_peer->close_code != -1); - } - return; - } - - if (_connection.is_null()) { - return; // Not connected. - } - - _tcp->poll(); - switch (_tcp->get_status()) { - case StreamPeerTCP::STATUS_NONE: - // Clean close - disconnect_from_host(); - _on_error(); - break; - case StreamPeerTCP::STATUS_CONNECTED: { - _ip_candidates.clear(); - Ref<StreamPeerTLS> tls; - if (_use_tls) { - if (_connection == _tcp) { - // Start SSL handshake - tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); - ERR_FAIL_COND_MSG(tls.is_null(), "SSL is not available in this build."); - tls->set_blocking_handshake_enabled(false); - if (tls->connect_to_stream(_tcp, verify_tls, _host, tls_cert) != OK) { - disconnect_from_host(); - _on_error(); - return; - } - _connection = tls; - } else { - tls = static_cast<Ref<StreamPeerTLS>>(_connection); - ERR_FAIL_COND(tls.is_null()); // Bug? - tls->poll(); - } - if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { - return; // Need more polling. - } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { - disconnect_from_host(); - _on_error(); - return; // Error. - } - } - // Do websocket handshake. - _do_handshake(); - } break; - case StreamPeerTCP::STATUS_ERROR: - while (_ip_candidates.size() > 0) { - _tcp->disconnect_from_host(); - if (_tcp->connect_to_host(_ip_candidates.pop_front(), _port) == OK) { - return; - } - } - disconnect_from_host(); - _on_error(); - break; - case StreamPeerTCP::STATUS_CONNECTING: - break; // Wait for connection - } -} - -Ref<WebSocketPeer> WSLClient::get_peer(int p_peer_id) const { - ERR_FAIL_COND_V(p_peer_id != 1, nullptr); - - return _peer; -} - -MultiplayerPeer::ConnectionStatus WSLClient::get_connection_status() const { - // This is surprising, but keeps the current behaviour to allow clean close requests. - // TODO Refactor WebSocket and split Client/Server/Multiplayer like done in other peers. - if (_peer->is_connected_to_host()) { - return CONNECTION_CONNECTED; - } - return _status; -} - -void WSLClient::disconnect_from_host(int p_code, String p_reason) { - _peer->close(p_code, p_reason); - _connection = Ref<StreamPeer>(nullptr); - _tcp = Ref<StreamPeerTCP>(memnew(StreamPeerTCP)); - _status = CONNECTION_DISCONNECTED; - - _key = ""; - _host = ""; - _protocols.clear(); - _use_tls = false; - - _request = ""; - _requested = 0; - - memset(_resp_buf, 0, sizeof(_resp_buf)); - _resp_pos = 0; - - if (_resolver_id != IP::RESOLVER_INVALID_ID) { - IP::get_singleton()->erase_resolve_item(_resolver_id); - _resolver_id = IP::RESOLVER_INVALID_ID; - } - - _ip_candidates.clear(); -} - -IPAddress WSLClient::get_connected_host() const { - ERR_FAIL_COND_V(!_peer->is_connected_to_host(), IPAddress()); - return _peer->get_connected_host(); -} - -uint16_t WSLClient::get_connected_port() const { - ERR_FAIL_COND_V(!_peer->is_connected_to_host(), 0); - return _peer->get_connected_port(); -} - -Error WSLClient::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - ERR_FAIL_COND_V_MSG(_connection.is_valid(), FAILED, "Buffers sizes can only be set before listening or connecting."); - - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - _out_pkt_size = nearest_shift(p_out_packets - 1); - return OK; -} - -WSLClient::WSLClient() { - _peer.instantiate(); - _tcp.instantiate(); - disconnect_from_host(); -} - -WSLClient::~WSLClient() { - _peer->close_now(); - _peer->invalidate(); - disconnect_from_host(); -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/wsl_peer.cpp b/modules/websocket/wsl_peer.cpp index 97bd87a526..84e022182e 100644 --- a/modules/websocket/wsl_peer.cpp +++ b/modules/websocket/wsl_peer.cpp @@ -32,71 +32,537 @@ #include "wsl_peer.h" -#include "wsl_client.h" -#include "wsl_server.h" +#include "wsl_peer.h" -#include "core/crypto/crypto_core.h" -#include "core/math/random_number_generator.h" -#include "core/os/os.h" +#include "core/io/stream_peer_tls.h" -String WSLPeer::generate_key() { - // Random key - RandomNumberGenerator rng; - rng.set_seed(OS::get_singleton()->get_unix_time()); - Vector<uint8_t> bkey; - int len = 16; // 16 bytes, as per RFC - bkey.resize(len); - uint8_t *w = bkey.ptrw(); - for (int i = 0; i < len; i++) { - w[i] = (uint8_t)rng.randi_range(0, 255); +CryptoCore::RandomGenerator *WSLPeer::_static_rng = nullptr; + +void WSLPeer::initialize() { + WebSocketPeer::_create = WSLPeer::_create; + _static_rng = memnew(CryptoCore::RandomGenerator); + _static_rng->init(); +} + +void WSLPeer::deinitialize() { + if (_static_rng) { + memdelete(_static_rng); + _static_rng = nullptr; } - return CryptoCore::b64_encode_str(&w[0], len); } -String WSLPeer::compute_key_response(String p_key) { - String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC - Vector<uint8_t> sha = key.sha1_buffer(); - return CryptoCore::b64_encode_str(sha.ptr(), sha.size()); +/// +/// Resolver +/// +void WSLPeer::Resolver::start(const String &p_host, int p_port) { + stop(); + + port = p_port; + if (p_host.is_valid_ip_address()) { + ip_candidates.push_back(IPAddress(p_host)); + } else { + // Queue hostname for resolution. + resolver_id = IP::get_singleton()->resolve_hostname_queue_item(p_host); + ERR_FAIL_COND(resolver_id == IP::RESOLVER_INVALID_ID); + // Check if it was found in cache. + IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id); + if (ip_status == IP::RESOLVER_STATUS_DONE) { + ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id); + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; + } + } } -void WSLPeer::_wsl_destroy(struct PeerData **p_data) { - if (!p_data || !(*p_data)) { - return; +void WSLPeer::Resolver::stop() { + if (resolver_id != IP::RESOLVER_INVALID_ID) { + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; + } + port = 0; +} + +void WSLPeer::Resolver::try_next_candidate(Ref<StreamPeerTCP> &p_tcp) { + // Check if we still need resolving. + if (resolver_id != IP::RESOLVER_INVALID_ID) { + IP::ResolverStatus ip_status = IP::get_singleton()->get_resolve_item_status(resolver_id); + if (ip_status == IP::RESOLVER_STATUS_WAITING) { + return; + } + if (ip_status == IP::RESOLVER_STATUS_DONE) { + ip_candidates = IP::get_singleton()->get_resolve_item_addresses(resolver_id); + } + IP::get_singleton()->erase_resolve_item(resolver_id); + resolver_id = IP::RESOLVER_INVALID_ID; } - struct PeerData *data = *p_data; - if (data->polling) { - data->destroy = true; + + // Try the current candidate if we have one. + if (p_tcp->get_status() != StreamPeerTCP::STATUS_NONE) { + p_tcp->poll(); + StreamPeerTCP::Status status = p_tcp->get_status(); + if (status == StreamPeerTCP::STATUS_CONNECTED) { + p_tcp->set_no_delay(true); + ip_candidates.clear(); + return; + } else if (status == StreamPeerTCP::STATUS_CONNECTING) { + return; // Keep connecting. + } else { + p_tcp->disconnect_from_host(); + } + } + + // Keep trying next candidate. + while (ip_candidates.size()) { + Error err = p_tcp->connect_to_host(ip_candidates.pop_front(), port); + if (err == OK) { + return; + } else { + p_tcp->disconnect_from_host(); + } + } +} + +/// +/// Server functions +/// +Error WSLPeer::accept_stream(Ref<StreamPeer> p_stream) { + ERR_FAIL_COND_V(wsl_ctx || tcp.is_valid(), ERR_ALREADY_IN_USE); + ERR_FAIL_COND_V(p_stream.is_null(), ERR_INVALID_PARAMETER); + + _clear(); + + if (p_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static())) { + tcp = p_stream; + connection = p_stream; + use_tls = false; + } else if (p_stream->is_class_ptr(StreamPeerTLS::get_class_ptr_static())) { + Ref<StreamPeer> base_stream = static_cast<Ref<StreamPeerTLS>>(p_stream)->get_stream(); + ERR_FAIL_COND_V(base_stream.is_null() || !base_stream->is_class_ptr(StreamPeerTCP::get_class_ptr_static()), ERR_INVALID_PARAMETER); + tcp = static_cast<Ref<StreamPeerTCP>>(base_stream); + connection = p_stream; + use_tls = true; + } + ERR_FAIL_COND_V(connection.is_null() || tcp.is_null(), ERR_INVALID_PARAMETER); + is_server = true; + ready_state = STATE_CONNECTING; + handshake_buffer->resize(WSL_MAX_HEADER_SIZE); + handshake_buffer->seek(0); + return OK; +} + +bool WSLPeer::_parse_client_request() { + Vector<String> psa = String((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4).split("\r\n"); + int len = psa.size(); + ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers, got: " + itos(len) + ", expected >= 4."); + + Vector<String> req = psa[0].split(" ", false); + ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code."); + + // Wrong protocol + ERR_FAIL_COND_V_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", false, "Invalid method or HTTP version."); + + HashMap<String, String> headers; + for (int i = 1; i < len; i++) { + Vector<String> header = psa[i].split(":", false, 1); + ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i]); + String name = header[0].to_lower(); + String value = header[1].strip_edges(); + if (headers.has(name)) { + headers[name] += "," + value; + } else { + headers[name] = value; + } + } + requested_host = headers.has("host") ? headers.get("host") : ""; + requested_url = (use_tls ? "wss://" : "ws://") + requested_host + req[1]; +#define WSL_CHECK(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); +#define WSL_CHECK_EX(NAME) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME), false, "Missing header '" + String(NAME) + "'."); + WSL_CHECK("upgrade", "websocket"); + WSL_CHECK("sec-websocket-version", "13"); + WSL_CHECK_EX("sec-websocket-key"); + WSL_CHECK_EX("connection"); +#undef WSL_CHECK_EX +#undef WSL_CHECK + session_key = headers["sec-websocket-key"]; + if (headers.has("sec-websocket-protocol")) { + Vector<String> protos = headers["sec-websocket-protocol"].split(","); + for (int i = 0; i < protos.size(); i++) { + String proto = protos[i].strip_edges(); + // Check if we have the given protocol + for (int j = 0; j < supported_protocols.size(); j++) { + if (proto != supported_protocols[j]) { + continue; + } + selected_protocol = proto; + break; + } + // Found a protocol + if (!selected_protocol.is_empty()) { + break; + } + } + if (selected_protocol.is_empty()) { // Invalid protocol(s) requested + return false; + } + } else if (supported_protocols.size() > 0) { // No protocol requested, but we need one + return false; + } + return true; +} + +Error WSLPeer::_do_server_handshake() { + if (use_tls) { + Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(connection); + if (tls.is_null()) { + ERR_FAIL_V_MSG(ERR_BUG, "Couldn't get StreamPeerTLS for WebSocket handshake."); + close(-1); + return FAILED; + } + tls->poll(); + if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + return OK; // Pending handshake + } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { + print_verbose(vformat("WebSocket SSL connection error during handshake (StreamPeerTLS status code %d).", tls->get_status())); + close(-1); + return FAILED; + } + } + + if (pending_request) { + int read = 0; + while (true) { + ERR_FAIL_COND_V_MSG(handshake_buffer->get_available_bytes() < 1, ERR_OUT_OF_MEMORY, "WebSocket response headers are too big."); + int pos = handshake_buffer->get_position(); + uint8_t byte; + Error err = connection->get_partial_data(&byte, 1, read); + if (err != OK) { // Got an error + print_verbose(vformat("WebSocket error while getting partial data (StreamPeer error code %d).", err)); + close(-1); + return FAILED; + } else if (read != 1) { // Busy, wait next poll + return OK; + } + handshake_buffer->put_u8(byte); + const char *r = (const char *)handshake_buffer->get_data_array().ptr(); + int l = pos; + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + if (!_parse_client_request()) { + close(-1); + return FAILED; + } + String s = "HTTP/1.1 101 Switching Protocols\r\n"; + s += "Upgrade: websocket\r\n"; + s += "Connection: Upgrade\r\n"; + s += "Sec-WebSocket-Accept: " + _compute_key_response(session_key) + "\r\n"; + if (!selected_protocol.is_empty()) { + s += "Sec-WebSocket-Protocol: " + selected_protocol + "\r\n"; + } + for (int i = 0; i < handshake_headers.size(); i++) { + s += handshake_headers[i] + "\r\n"; + } + s += "\r\n"; + CharString cs = s.utf8(); + handshake_buffer->clear(); + handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length()); + handshake_buffer->seek(0); + pending_request = false; + break; + } + } + } + + if (pending_request) { // Still pending. + return OK; + } + + int left = handshake_buffer->get_available_bytes(); + if (left) { + Vector<uint8_t> data = handshake_buffer->get_data_array(); + int pos = handshake_buffer->get_position(); + int sent = 0; + Error err = connection->put_partial_data(data.ptr() + pos, left, sent); + if (err != OK) { + print_verbose(vformat("WebSocket error while putting partial data (StreamPeer error code %d).", err)); + close(-1); + return err; + } + handshake_buffer->seek(pos + sent); + left -= sent; + if (left == 0) { + resolver.stop(); + // Response sent, initialize wslay context. + wslay_event_context_server_init(&wsl_ctx, &_wsl_callbacks, this); + wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size); + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_OPEN; + } + } + + return OK; +} + +/// +/// Client functions +/// +void WSLPeer::_do_client_handshake() { + ERR_FAIL_COND(tcp.is_null()); + + // Try to connect to candidates. + if (resolver.has_more_candidates()) { + resolver.try_next_candidate(tcp); + if (resolver.has_more_candidates()) { + return; // Still pending. + } + } + + tcp->poll(); + if (tcp->get_status() == StreamPeerTCP::STATUS_CONNECTING) { + return; // Keep connecting. + } else if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + close(-1); // Failed to connect. return; } - wslay_event_context_free(data->ctx); - memdelete(data); - *p_data = nullptr; + + if (use_tls) { + Ref<StreamPeerTLS> tls; + if (connection == tcp) { + // Start SSL handshake + tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); + ERR_FAIL_COND_MSG(tls.is_null(), "SSL is not available in this build."); + tls->set_blocking_handshake_enabled(false); + if (tls->connect_to_stream(tcp, verify_tls, requested_host, tls_cert) != OK) { + close(-1); + return; // Error. + } + connection = tls; + } else { + tls = static_cast<Ref<StreamPeerTLS>>(connection); + ERR_FAIL_COND(tls.is_null()); + tls->poll(); + } + if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { + return; // Need more polling. + } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { + close(-1); + return; // Error. + } + } + + // Do websocket handshake. + if (pending_request) { + int left = handshake_buffer->get_available_bytes(); + int pos = handshake_buffer->get_position(); + const Vector<uint8_t> data = handshake_buffer->get_data_array(); + int sent = 0; + Error err = connection->put_partial_data(data.ptr() + pos, left, sent); + // Sending handshake failed + if (err != OK) { + close(-1); + return; // Error. + } + handshake_buffer->seek(pos + sent); + if (handshake_buffer->get_available_bytes() == 0) { + pending_request = false; + handshake_buffer->clear(); + handshake_buffer->resize(WSL_MAX_HEADER_SIZE); + handshake_buffer->seek(0); + } + } else { + int read = 0; + while (true) { + int left = handshake_buffer->get_available_bytes(); + int pos = handshake_buffer->get_position(); + if (left == 0) { + // Header is too big + close(-1); + ERR_FAIL_MSG("Response headers too big."); + return; + } + + uint8_t byte; + Error err = connection->get_partial_data(&byte, 1, read); + if (err != OK) { + // Got some error. + close(-1); + return; + } else if (read != 1) { + // Busy, wait next poll. + break; + } + handshake_buffer->put_u8(byte); + + // Check "\r\n\r\n" header terminator + const char *r = (const char *)handshake_buffer->get_data_array().ptr(); + int l = pos; + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + // Response is over, verify headers and initialize wslay context/ + if (!_verify_server_response()) { + close(-1); + ERR_FAIL_MSG("Invalid response headers."); + return; + } + wslay_event_context_client_init(&wsl_ctx, &_wsl_callbacks, this); + wslay_event_config_set_max_recv_msg_length(wsl_ctx, inbound_buffer_size); + in_buffer.resize(nearest_shift(inbound_buffer_size), max_queued_packets); + packet_buffer.resize(inbound_buffer_size); + ready_state = STATE_OPEN; + break; + } + } + } } -bool WSLPeer::_wsl_poll(struct PeerData *p_data) { - p_data->polling = true; - int err = 0; - if ((err = wslay_event_recv(p_data->ctx)) != 0 || (err = wslay_event_send(p_data->ctx)) != 0) { - print_verbose("Websocket (wslay) poll error: " + itos(err)); - p_data->destroy = true; +bool WSLPeer::_verify_server_response() { + Vector<String> psa = String((const char *)handshake_buffer->get_data_array().ptr(), handshake_buffer->get_position() - 4).split("\r\n"); + int len = psa.size(); + ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers. Got: " + itos(len) + ", expected >= 4."); + + Vector<String> req = psa[0].split(" ", false); + ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code. Got '" + psa[0] + "', expected 'HTTP/1.1 101'."); + + // Wrong protocol + ERR_FAIL_COND_V_MSG(req[0] != "HTTP/1.1", false, "Invalid protocol. Got: '" + req[0] + "', expected 'HTTP/1.1'."); + ERR_FAIL_COND_V_MSG(req[1] != "101", false, "Invalid status code. Got: '" + req[1] + "', expected '101'."); + + HashMap<String, String> headers; + for (int i = 1; i < len; i++) { + Vector<String> header = psa[i].split(":", false, 1); + ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i] + "."); + String name = header[0].to_lower(); + String value = header[1].strip_edges(); + if (headers.has(name)) { + headers[name] += "," + value; + } else { + headers[name] = value; + } } - p_data->polling = false; - if (p_data->destroy || (wslay_event_get_close_sent(p_data->ctx) && wslay_event_get_close_received(p_data->ctx))) { - bool valid = p_data->valid; - _wsl_destroy(&p_data); - return valid; +#define WSL_CHECK(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); +#define WSL_CHECK_NC(NAME, VALUE) \ + ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME] != VALUE, false, \ + "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); + WSL_CHECK("connection", "upgrade"); + WSL_CHECK("upgrade", "websocket"); + WSL_CHECK_NC("sec-websocket-accept", _compute_key_response(session_key)); +#undef WSL_CHECK_NC +#undef WSL_CHECK + if (supported_protocols.size() == 0) { + // We didn't request a custom protocol + ERR_FAIL_COND_V_MSG(headers.has("sec-websocket-protocol"), false, "Received unrequested sub-protocol -> " + headers["sec-websocket-protocol"]); + } else { + // We requested at least one custom protocol but didn't receive one + ERR_FAIL_COND_V_MSG(!headers.has("sec-websocket-protocol"), false, "Requested sub-protocol(s) but received none."); + // Check received sub-protocol was one of those requested. + selected_protocol = headers["sec-websocket-protocol"]; + bool valid = false; + for (int i = 0; i < supported_protocols.size(); i++) { + if (supported_protocols[i] != selected_protocol) { + continue; + } + valid = true; + break; + } + if (!valid) { + ERR_FAIL_V_MSG(false, "Received unrequested sub-protocol -> " + selected_protocol); + return false; + } + } + return true; +} + +Error WSLPeer::connect_to_url(const String &p_url, bool p_verify_tls, Ref<X509Certificate> p_cert) { + ERR_FAIL_COND_V(wsl_ctx || tcp.is_valid(), ERR_ALREADY_IN_USE); + ERR_FAIL_COND_V(p_url.is_empty(), ERR_INVALID_PARAMETER); + + _clear(); + + String host; + String path; + String scheme; + int port = 0; + Error err = p_url.parse_url(scheme, host, port, path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); + if (scheme.is_empty()) { + scheme = "ws://"; + } + ERR_FAIL_COND_V_MSG(scheme != "ws://" && scheme != "wss://", ERR_INVALID_PARAMETER, vformat("Invalid protocol: \"%s\" (must be either \"ws://\" or \"wss://\").", scheme)); + + use_tls = false; + if (scheme == "wss://") { + use_tls = true; + } + if (port == 0) { + port = use_tls ? 443 : 80; + } + if (path.is_empty()) { + path = "/"; + } + + requested_url = p_url; + requested_host = host; + verify_tls = p_verify_tls; + tls_cert = p_cert; + tcp.instantiate(); + + resolver.start(host, port); + resolver.try_next_candidate(tcp); + + if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTING && tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED && !resolver.has_more_candidates()) { + _clear(); + return FAILED; + } + connection = tcp; + + // Prepare handshake request. + session_key = _generate_key(); + String request = "GET " + path + " HTTP/1.1\r\n"; + String port_string; + if ((port != 80 && !use_tls) || (port != 443 && use_tls)) { + port_string = ":" + itos(port); + } + request += "Host: " + host + port_string + "\r\n"; + request += "Upgrade: websocket\r\n"; + request += "Connection: Upgrade\r\n"; + request += "Sec-WebSocket-Key: " + session_key + "\r\n"; + request += "Sec-WebSocket-Version: 13\r\n"; + if (supported_protocols.size() > 0) { + request += "Sec-WebSocket-Protocol: "; + for (int i = 0; i < supported_protocols.size(); i++) { + if (i != 0) { + request += ","; + } + request += supported_protocols[i]; + } + request += "\r\n"; } - return false; + for (int i = 0; i < handshake_headers.size(); i++) { + request += handshake_headers[i] + "\r\n"; + } + request += "\r\n"; + CharString cs = request.utf8(); + handshake_buffer->put_data((const uint8_t *)cs.get_data(), cs.length()); + handshake_buffer->seek(0); + ready_state = STATE_CONNECTING; + is_server = false; + return OK; } -ssize_t wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid) { +/// +/// Callback functions. +/// +ssize_t WSLPeer::_wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + Ref<StreamPeer> conn = peer->connection; + if (conn.is_null()) { wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); return -1; } - Ref<StreamPeer> conn = peer_data->conn; int read = 0; Error err = conn->get_partial_data(data, len, read); if (err != OK) { @@ -111,13 +577,13 @@ ssize_t wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len return read; } -ssize_t wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid) { +ssize_t WSLPeer::_wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + Ref<StreamPeer> conn = peer->connection; + if (conn.is_null()) { wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); return -1; } - Ref<StreamPeer> conn = peer_data->conn; int sent = 0; Error err = conn->put_partial_data(data, len, sent); if (err != OK) { @@ -131,144 +597,142 @@ ssize_t wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size return sent; } -int wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) { - RandomNumberGenerator rng; - // TODO maybe use crypto in the future? - rng.set_seed(OS::get_singleton()->get_unix_time()); - for (unsigned int i = 0; i < len; i++) { - buf[i] = (uint8_t)rng.randi_range(0, 255); - } +int WSLPeer::_wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) { + ERR_FAIL_COND_V(!_static_rng, WSLAY_ERR_CALLBACK_FAILURE); + Error err = _static_rng->get_random_bytes(buf, len); + ERR_FAIL_COND_V(err != OK, WSLAY_ERR_CALLBACK_FAILURE); return 0; } -void wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { - struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data; - if (!peer_data->valid || peer_data->closing) { +void WSLPeer::_wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { + WSLPeer *peer = (WSLPeer *)user_data; + uint8_t op = arg->opcode; + + if (op == WSLAY_CONNECTION_CLOSE) { + // Close request or confirmation. + peer->close_code = arg->status_code; + size_t len = arg->msg_length; + peer->close_reason = ""; + if (len > 2 /* first 2 bytes = close code */) { + peer->close_reason.parse_utf8((char *)arg->msg + 2, len - 2); + } + if (peer->ready_state == STATE_OPEN) { + peer->ready_state = STATE_CLOSING; + } return; } - WSLPeer *peer = static_cast<WSLPeer *>(peer_data->peer); - if (peer->parse_message(arg) != OK) { + if (peer->ready_state == STATE_CLOSING) { return; } - if (peer_data->is_server) { - WSLServer *helper = static_cast<WSLServer *>(peer_data->obj); - helper->_on_peer_packet(peer_data->id); - } else { - WSLClient *helper = static_cast<WSLClient *>(peer_data->obj); - helper->_on_peer_packet(); + if (op == WSLAY_TEXT_FRAME || op == WSLAY_BINARY_FRAME) { + // Message. + uint8_t is_string = arg->opcode == WSLAY_TEXT_FRAME ? 1 : 0; + peer->in_buffer.write_packet(arg->msg, arg->msg_length, &is_string); } + // Ping or pong. } -wslay_event_callbacks wsl_callbacks = { - wsl_recv_callback, - wsl_send_callback, - wsl_genmask_callback, +wslay_event_callbacks WSLPeer::_wsl_callbacks = { + _wsl_recv_callback, + _wsl_send_callback, + _wsl_genmask_callback, nullptr, /* on_frame_recv_start_callback */ nullptr, /* on_frame_recv_callback */ nullptr, /* on_frame_recv_end_callback */ - wsl_msg_recv_callback + _wsl_msg_recv_callback }; -Error WSLPeer::parse_message(const wslay_event_on_msg_recv_arg *arg) { - uint8_t is_string = 0; - if (arg->opcode == WSLAY_TEXT_FRAME) { - is_string = 1; - } else if (arg->opcode == WSLAY_CONNECTION_CLOSE) { - close_code = arg->status_code; - size_t len = arg->msg_length; - close_reason = ""; - if (len > 2 /* first 2 bytes = close code */) { - close_reason.parse_utf8((char *)arg->msg + 2, len - 2); - } - if (!wslay_event_get_close_sent(_data->ctx)) { - if (_data->is_server) { - WSLServer *helper = static_cast<WSLServer *>(_data->obj); - helper->_on_close_request(_data->id, close_code, close_reason); - } else { - WSLClient *helper = static_cast<WSLClient *>(_data->obj); - helper->_on_close_request(close_code, close_reason); - } - } - return ERR_FILE_EOF; - } else if (arg->opcode != WSLAY_BINARY_FRAME) { - // Ping or pong - return ERR_SKIP; - } - _in_buffer.write_packet(arg->msg, arg->msg_length, &is_string); - return OK; -} - -void WSLPeer::make_context(PeerData *p_data, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size, unsigned int p_out_pkt_size) { - ERR_FAIL_COND(_data != nullptr); - ERR_FAIL_COND(p_data == nullptr); - - _in_buffer.resize(p_in_pkt_size, p_in_buf_size); - _packet_buffer.resize(1 << p_in_buf_size); - _out_buf_size = p_out_buf_size; - _out_pkt_size = p_out_pkt_size; - - _data = p_data; - _data->peer = this; - _data->valid = true; - - if (_data->is_server) { - wslay_event_context_server_init(&(_data->ctx), &wsl_callbacks, _data); - } else { - wslay_event_context_client_init(&(_data->ctx), &wsl_callbacks, _data); - } - wslay_event_config_set_max_recv_msg_length(_data->ctx, (1ULL << p_in_buf_size)); -} - -void WSLPeer::set_write_mode(WriteMode p_mode) { - write_mode = p_mode; +String WSLPeer::_generate_key() { + // Random key + Vector<uint8_t> bkey; + int len = 16; // 16 bytes, as per RFC + bkey.resize(len); + _wsl_genmask_callback(nullptr, bkey.ptrw(), len, nullptr); + return CryptoCore::b64_encode_str(bkey.ptrw(), len); } -WSLPeer::WriteMode WSLPeer::get_write_mode() const { - return write_mode; +String WSLPeer::_compute_key_response(String p_key) { + String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC + Vector<uint8_t> sha = key.sha1_buffer(); + return CryptoCore::b64_encode_str(sha.ptr(), sha.size()); } void WSLPeer::poll() { - if (!_data) { + // Nothing to do. + if (ready_state == STATE_CLOSED) { return; } - if (_wsl_poll(_data)) { - _data = nullptr; + if (ready_state == STATE_CONNECTING) { + if (is_server) { + _do_server_handshake(); + } else { + _do_client_handshake(); + } + } + + if (ready_state == STATE_OPEN || ready_state == STATE_CLOSING) { + ERR_FAIL_COND(!wsl_ctx); + int err = 0; + if ((err = wslay_event_recv(wsl_ctx)) != 0 || (err = wslay_event_send(wsl_ctx)) != 0) { + // Error close. + print_verbose("Websocket (wslay) poll error: " + itos(err)); + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + close(-1); + return; + } + if (wslay_event_get_close_sent(wsl_ctx) && wslay_event_get_close_received(wsl_ctx)) { + // Clean close. + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + close(-1); + return; + } } } -Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { - ERR_FAIL_COND_V(!is_connected_to_host(), FAILED); - ERR_FAIL_COND_V(_out_pkt_size && (wslay_event_get_queued_msg_count(_data->ctx) >= (1ULL << _out_pkt_size)), ERR_OUT_OF_MEMORY); - ERR_FAIL_COND_V(_out_buf_size && (wslay_event_get_queued_msg_length(_data->ctx) + p_buffer_size >= (1ULL << _out_buf_size)), ERR_OUT_OF_MEMORY); +Error WSLPeer::_send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode) { + ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED); + ERR_FAIL_COND_V(wslay_event_get_queued_msg_count(wsl_ctx) >= (uint32_t)max_queued_packets, ERR_OUT_OF_MEMORY); + ERR_FAIL_COND_V(outbound_buffer_size > 0 && (wslay_event_get_queued_msg_length(wsl_ctx) + p_buffer_size > (uint32_t)outbound_buffer_size), ERR_OUT_OF_MEMORY); struct wslay_event_msg msg; - msg.opcode = write_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME; + msg.opcode = p_opcode; msg.msg = p_buffer; msg.msg_length = p_buffer_size; // Queue & send message. - if (wslay_event_queue_msg(_data->ctx, &msg) != 0 || wslay_event_send(_data->ctx) != 0) { - close_now(); + if (wslay_event_queue_msg(wsl_ctx, &msg) != 0 || wslay_event_send(wsl_ctx) != 0) { + close(-1); return FAILED; } return OK; } +Error WSLPeer::send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) { + wslay_opcode opcode = p_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME; + return _send(p_buffer, p_buffer_size, opcode); +} + +Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { + return _send(p_buffer, p_buffer_size, WSLAY_BINARY_FRAME); +} + Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { r_buffer_size = 0; - ERR_FAIL_COND_V(!is_connected_to_host(), FAILED); + ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED); - if (_in_buffer.packets_left() == 0) { + if (in_buffer.packets_left() == 0) { return ERR_UNAVAILABLE; } int read = 0; - uint8_t *rw = _packet_buffer.ptrw(); - _in_buffer.read_packet(rw, _packet_buffer.size(), &_is_string, read); + uint8_t *rw = packet_buffer.ptrw(); + in_buffer.read_packet(rw, packet_buffer.size(), &was_string, read); *r_buffer = rw; r_buffer_size = read; @@ -277,75 +741,106 @@ Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { } int WSLPeer::get_available_packet_count() const { - if (!is_connected_to_host()) { + if (ready_state != STATE_OPEN) { return 0; } - return _in_buffer.packets_left(); + return in_buffer.packets_left(); } int WSLPeer::get_current_outbound_buffered_amount() const { - ERR_FAIL_COND_V(!_data, 0); - - return wslay_event_get_queued_msg_length(_data->ctx); -} - -bool WSLPeer::was_string_packet() const { - return _is_string; -} - -bool WSLPeer::is_connected_to_host() const { - return _data != nullptr; -} + if (ready_state != STATE_OPEN) { + return 0; + } -void WSLPeer::close_now() { - close(1000, ""); - _wsl_destroy(&_data); + return wslay_event_get_queued_msg_length(wsl_ctx); } void WSLPeer::close(int p_code, String p_reason) { - if (_data && !wslay_event_get_close_sent(_data->ctx)) { + if (p_code < 0) { + // Force immediate close. + ready_state = STATE_CLOSED; + } + + if (ready_state == STATE_OPEN && !wslay_event_get_close_sent(wsl_ctx)) { CharString cs = p_reason.utf8(); - wslay_event_queue_close(_data->ctx, p_code, (uint8_t *)cs.ptr(), cs.size()); - wslay_event_send(_data->ctx); - _data->closing = true; + wslay_event_queue_close(wsl_ctx, p_code, (uint8_t *)cs.ptr(), cs.length()); + wslay_event_send(wsl_ctx); + ready_state = STATE_CLOSING; + } else if (ready_state == STATE_CONNECTING || ready_state == STATE_CLOSED) { + ready_state = STATE_CLOSED; + connection.unref(); + if (tcp.is_valid()) { + tcp->disconnect_from_host(); + tcp.unref(); + } } - _in_buffer.clear(); - _packet_buffer.resize(0); + in_buffer.clear(); + packet_buffer.resize(0); } IPAddress WSLPeer::get_connected_host() const { - ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), IPAddress()); - - return _data->tcp->get_connected_host(); + ERR_FAIL_COND_V(tcp.is_null(), IPAddress()); + return tcp->get_connected_host(); } uint16_t WSLPeer::get_connected_port() const { - ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), 0); + ERR_FAIL_COND_V(tcp.is_null(), 0); + return tcp->get_connected_port(); +} + +String WSLPeer::get_selected_protocol() const { + return selected_protocol; +} - return _data->tcp->get_connected_port(); +String WSLPeer::get_requested_url() const { + return requested_url; } void WSLPeer::set_no_delay(bool p_enabled) { - ERR_FAIL_COND(!is_connected_to_host() || _data->tcp.is_null()); - _data->tcp->set_no_delay(p_enabled); + ERR_FAIL_COND(tcp.is_null()); + tcp->set_no_delay(p_enabled); } -void WSLPeer::invalidate() { - if (_data) { - _data->valid = false; +void WSLPeer::_clear() { + // Connection info. + ready_state = STATE_CLOSED; + is_server = false; + connection.unref(); + if (tcp.is_valid()) { + tcp->disconnect_from_host(); + tcp.unref(); } + if (wsl_ctx) { + wslay_event_context_free(wsl_ctx); + wsl_ctx = nullptr; + } + + resolver.stop(); + requested_url.clear(); + requested_host.clear(); + pending_request = true; + handshake_buffer->clear(); + selected_protocol.clear(); + session_key.clear(); + + // Pending packets info. + was_string = 0; + in_buffer.clear(); + packet_buffer.clear(); + + // Close code info. + close_code = -1; + close_reason.clear(); } WSLPeer::WSLPeer() { + handshake_buffer.instantiate(); } WSLPeer::~WSLPeer() { - close(); - invalidate(); - _wsl_destroy(&_data); - _data = nullptr; + close(-1); } #endif // WEB_ENABLED diff --git a/modules/websocket/wsl_peer.h b/modules/websocket/wsl_peer.h index 92672eb2c4..379002739c 100644 --- a/modules/websocket/wsl_peer.h +++ b/modules/websocket/wsl_peer.h @@ -33,79 +33,123 @@ #ifndef WEB_ENABLED +#include "websocket_peer.h" + +#include "packet_buffer.h" + +#include "core/crypto/crypto_core.h" #include "core/error/error_list.h" #include "core/io/packet_peer.h" #include "core/io/stream_peer_tcp.h" #include "core/templates/ring_buffer.h" -#include "packet_buffer.h" -#include "websocket_peer.h" #include "wslay/wslay.h" #define WSL_MAX_HEADER_SIZE 4096 class WSLPeer : public WebSocketPeer { - GDCIIMPL(WSLPeer, WebSocketPeer); - -public: - struct PeerData { - bool polling = false; - bool destroy = false; - bool valid = false; - bool is_server = false; - bool closing = false; - void *obj = nullptr; - void *peer = nullptr; - Ref<StreamPeer> conn; - Ref<StreamPeerTCP> tcp; - int id = 1; - wslay_event_context_ptr ctx = nullptr; +private: + static CryptoCore::RandomGenerator *_static_rng; + static WebSocketPeer *_create() { return memnew(WSLPeer); } + + // Callbacks. + static ssize_t _wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data); + static ssize_t _wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data); + static int _wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data); + static void _wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); + + static wslay_event_callbacks _wsl_callbacks; + + // Helpers + static String _compute_key_response(String p_key); + static String _generate_key(); + + // Client IP resolver. + class Resolver { + Array ip_candidates; + IP::ResolverID resolver_id = IP::RESOLVER_INVALID_ID; + int port = 0; + + public: + bool has_more_candidates() { + return ip_candidates.size() > 0 || resolver_id != IP::RESOLVER_INVALID_ID; + } + + void try_next_candidate(Ref<StreamPeerTCP> &p_tcp); + void start(const String &p_host, int p_port); + void stop(); + Resolver() {} }; - static String compute_key_response(String p_key); - static String generate_key(); + Resolver resolver; -private: - static bool _wsl_poll(struct PeerData *p_data); - static void _wsl_destroy(struct PeerData **p_data); + // WebSocket connection state. + WebSocketPeer::State ready_state = WebSocketPeer::STATE_CLOSED; + bool is_server = false; + Ref<StreamPeerTCP> tcp; + Ref<StreamPeer> connection; + wslay_event_context_ptr wsl_ctx = nullptr; + + String requested_url; + String requested_host; + bool pending_request = true; + Ref<StreamPeerBuffer> handshake_buffer; + String selected_protocol; + String session_key; - struct PeerData *_data = nullptr; - uint8_t _is_string = 0; + int close_code = -1; + String close_reason; + uint8_t was_string = 0; + + // WebSocket configuration. + bool use_tls = true; + bool verify_tls = true; + Ref<X509Certificate> tls_cert; + + // Packet buffers. + Vector<uint8_t> packet_buffer; // Our packet info is just a boolean (is_string), using uint8_t for it. - PacketBuffer<uint8_t> _in_buffer; + PacketBuffer<uint8_t> in_buffer; - Vector<uint8_t> _packet_buffer; + Error _send(const uint8_t *p_buffer, int p_buffer_size, wslay_opcode p_opcode); - WriteMode write_mode = WRITE_MODE_BINARY; + Error _do_server_handshake(); + bool _parse_client_request(); - int _out_buf_size = 0; - int _out_pkt_size = 0; + void _do_client_handshake(); + bool _verify_server_response(); + + void _clear(); public: - int close_code = -1; - String close_reason; - void poll(); // Used by client and server. + static void initialize(); + static void deinitialize(); + // PacketPeer virtual int get_available_packet_count() const override; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override; virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override; - virtual int get_max_packet_size() const override { return _packet_buffer.size(); }; - virtual int get_current_outbound_buffered_amount() const override; + virtual int get_max_packet_size() const override { return packet_buffer.size(); }; - virtual void close_now(); + // WebSocketPeer + virtual Error send(const uint8_t *p_buffer, int p_buffer_size, WriteMode p_mode) override; + virtual Error connect_to_url(const String &p_url, bool p_verify_tls = true, Ref<X509Certificate> p_cert = Ref<X509Certificate>()) override; + virtual Error accept_stream(Ref<StreamPeer> p_stream) override; virtual void close(int p_code = 1000, String p_reason = "") override; - virtual bool is_connected_to_host() const override; + virtual void poll() override; + + virtual State get_ready_state() const override { return ready_state; } + virtual int get_close_code() const override { return close_code; } + virtual String get_close_reason() const override { return close_reason; } + virtual int get_current_outbound_buffered_amount() const override; + virtual IPAddress get_connected_host() const override; virtual uint16_t get_connected_port() const override; + virtual String get_selected_protocol() const override; + virtual String get_requested_url() const override; - virtual WriteMode get_write_mode() const override; - virtual void set_write_mode(WriteMode p_mode) override; - virtual bool was_string_packet() const override; + virtual bool was_string_packet() const override { return was_string; } virtual void set_no_delay(bool p_enabled) override; - void make_context(PeerData *p_data, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size, unsigned int p_out_pkt_size); - Error parse_message(const wslay_event_on_msg_recv_arg *arg); - void invalidate(); - WSLPeer(); ~WSLPeer(); }; diff --git a/modules/websocket/wsl_server.cpp b/modules/websocket/wsl_server.cpp deleted file mode 100644 index 01dcd53839..0000000000 --- a/modules/websocket/wsl_server.cpp +++ /dev/null @@ -1,329 +0,0 @@ -/*************************************************************************/ -/* wsl_server.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WEB_ENABLED - -#include "wsl_server.h" -#include "core/config/project_settings.h" -#include "core/os/os.h" - -bool WSLServer::PendingPeer::_parse_request(const Vector<String> p_protocols, String &r_resource_name) { - Vector<String> psa = String((char *)req_buf).split("\r\n"); - int len = psa.size(); - ERR_FAIL_COND_V_MSG(len < 4, false, "Not enough response headers, got: " + itos(len) + ", expected >= 4."); - - Vector<String> req = psa[0].split(" ", false); - ERR_FAIL_COND_V_MSG(req.size() < 2, false, "Invalid protocol or status code."); - - // Wrong protocol - ERR_FAIL_COND_V_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", false, "Invalid method or HTTP version."); - - r_resource_name = req[1]; - HashMap<String, String> headers; - for (int i = 1; i < len; i++) { - Vector<String> header = psa[i].split(":", false, 1); - ERR_FAIL_COND_V_MSG(header.size() != 2, false, "Invalid header -> " + psa[i]); - String name = header[0].to_lower(); - String value = header[1].strip_edges(); - if (headers.has(name)) { - headers[name] += "," + value; - } else { - headers[name] = value; - } - } -#define WSL_CHECK(NAME, VALUE) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME) || headers[NAME].to_lower() != VALUE, false, \ - "Missing or invalid header '" + String(NAME) + "'. Expected value '" + VALUE + "'."); -#define WSL_CHECK_EX(NAME) \ - ERR_FAIL_COND_V_MSG(!headers.has(NAME), false, "Missing header '" + String(NAME) + "'."); - WSL_CHECK("upgrade", "websocket"); - WSL_CHECK("sec-websocket-version", "13"); - WSL_CHECK_EX("sec-websocket-key"); - WSL_CHECK_EX("connection"); -#undef WSL_CHECK_EX -#undef WSL_CHECK - key = headers["sec-websocket-key"]; - if (headers.has("sec-websocket-protocol")) { - Vector<String> protos = headers["sec-websocket-protocol"].split(","); - for (int i = 0; i < protos.size(); i++) { - String proto = protos[i].strip_edges(); - // Check if we have the given protocol - for (int j = 0; j < p_protocols.size(); j++) { - if (proto != p_protocols[j]) { - continue; - } - protocol = proto; - break; - } - // Found a protocol - if (!protocol.is_empty()) { - break; - } - } - if (protocol.is_empty()) { // Invalid protocol(s) requested - return false; - } - } else if (p_protocols.size() > 0) { // No protocol requested, but we need one - return false; - } - return true; -} - -Error WSLServer::PendingPeer::do_handshake(const Vector<String> p_protocols, uint64_t p_timeout, String &r_resource_name, const Vector<String> &p_extra_headers) { - if (OS::get_singleton()->get_ticks_msec() - time > p_timeout) { - print_verbose(vformat("WebSocket handshake timed out after %.3f seconds.", p_timeout * 0.001)); - return ERR_TIMEOUT; - } - - if (use_tls) { - Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(connection); - if (tls.is_null()) { - ERR_FAIL_V_MSG(ERR_BUG, "Couldn't get StreamPeerTLS for WebSocket handshake."); - } - tls->poll(); - if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) { - return ERR_BUSY; - } else if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) { - print_verbose(vformat("WebSocket SSL connection error during handshake (StreamPeerTLS status code %d).", tls->get_status())); - return FAILED; - } - } - - if (!has_request) { - int read = 0; - while (true) { - ERR_FAIL_COND_V_MSG(req_pos >= WSL_MAX_HEADER_SIZE, ERR_OUT_OF_MEMORY, "WebSocket response headers are too big."); - Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); - if (err != OK) { // Got an error - print_verbose(vformat("WebSocket error while getting partial data (StreamPeer error code %d).", err)); - return FAILED; - } else if (read != 1) { // Busy, wait next poll - return ERR_BUSY; - } - char *r = (char *)req_buf; - int l = req_pos; - if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { - r[l - 3] = '\0'; - if (!_parse_request(p_protocols, r_resource_name)) { - return FAILED; - } - String s = "HTTP/1.1 101 Switching Protocols\r\n"; - s += "Upgrade: websocket\r\n"; - s += "Connection: Upgrade\r\n"; - s += "Sec-WebSocket-Accept: " + WSLPeer::compute_key_response(key) + "\r\n"; - if (!protocol.is_empty()) { - s += "Sec-WebSocket-Protocol: " + protocol + "\r\n"; - } - for (int i = 0; i < p_extra_headers.size(); i++) { - s += p_extra_headers[i] + "\r\n"; - } - s += "\r\n"; - response = s.utf8(); - has_request = true; - break; - } - req_pos += 1; - } - } - - if (has_request && response_sent < response.size() - 1) { - int sent = 0; - Error err = connection->put_partial_data((const uint8_t *)response.get_data() + response_sent, response.size() - response_sent - 1, sent); - if (err != OK) { - print_verbose(vformat("WebSocket error while putting partial data (StreamPeer error code %d).", err)); - return err; - } - response_sent += sent; - } - - if (response_sent < response.size() - 1) { - return ERR_BUSY; - } - - return OK; -} - -void WSLServer::set_extra_headers(const Vector<String> &p_headers) { - _extra_headers = p_headers; -} - -Error WSLServer::listen(int p_port, const Vector<String> p_protocols, bool gd_mp_api) { - ERR_FAIL_COND_V(is_listening(), ERR_ALREADY_IN_USE); - - _is_multiplayer = gd_mp_api; - // Strip edges from protocols. - _protocols.resize(p_protocols.size()); - String *pw = _protocols.ptrw(); - for (int i = 0; i < p_protocols.size(); i++) { - pw[i] = p_protocols[i].strip_edges(); - } - return _server->listen(p_port, bind_ip); -} - -void WSLServer::poll() { - List<int> remove_ids; - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - Ref<WSLPeer> peer = const_cast<WSLPeer *>(static_cast<const WSLPeer *>(E.value.ptr())); - peer->poll(); - if (!peer->is_connected_to_host()) { - _on_disconnect(E.key, peer->close_code != -1); - remove_ids.push_back(E.key); - } - } - for (int &E : remove_ids) { - _peer_map.erase(E); - } - remove_ids.clear(); - - List<Ref<PendingPeer>> remove_peers; - for (const Ref<PendingPeer> &E : _pending) { - String resource_name; - Ref<PendingPeer> ppeer = E; - Error err = ppeer->do_handshake(_protocols, handshake_timeout, resource_name, _extra_headers); - if (err == ERR_BUSY) { - continue; - } else if (err != OK) { - remove_peers.push_back(ppeer); - continue; - } - // Creating new peer - int32_t id = generate_unique_id(); - - WSLPeer::PeerData *data = memnew(struct WSLPeer::PeerData); - data->obj = this; - data->conn = ppeer->connection; - data->tcp = ppeer->tcp; - data->is_server = true; - data->id = id; - - Ref<WSLPeer> ws_peer = memnew(WSLPeer); - ws_peer->make_context(data, _in_buf_size, _in_pkt_size, _out_buf_size, _out_pkt_size); - ws_peer->set_no_delay(true); - - _peer_map[id] = ws_peer; - remove_peers.push_back(ppeer); - _on_connect(id, ppeer->protocol, resource_name); - } - for (const Ref<PendingPeer> &E : remove_peers) { - _pending.erase(E); - } - remove_peers.clear(); - - if (!_server->is_listening()) { - return; - } - - while (_server->is_connection_available()) { - Ref<StreamPeerTCP> conn = _server->take_connection(); - if (is_refusing_new_connections()) { - continue; // Conn will go out-of-scope and be closed. - } - - Ref<PendingPeer> peer = memnew(PendingPeer); - if (private_key.is_valid() && tls_cert.is_valid()) { - Ref<StreamPeerTLS> tls = Ref<StreamPeerTLS>(StreamPeerTLS::create()); - tls->set_blocking_handshake_enabled(false); - tls->accept_stream(conn, private_key, tls_cert, ca_chain); - peer->connection = tls; - peer->use_tls = true; - } else { - peer->connection = conn; - } - peer->tcp = conn; - peer->time = OS::get_singleton()->get_ticks_msec(); - _pending.push_back(peer); - } -} - -bool WSLServer::is_listening() const { - return _server->is_listening(); -} - -int WSLServer::get_max_packet_size() const { - return (1 << _out_buf_size) - PROTO_SIZE; -} - -void WSLServer::stop() { - _server->stop(); - for (const KeyValue<int, Ref<WebSocketPeer>> &E : _peer_map) { - Ref<WSLPeer> peer = const_cast<WSLPeer *>(static_cast<const WSLPeer *>(E.value.ptr())); - peer->close_now(); - } - _pending.clear(); - _peer_map.clear(); - _protocols.clear(); -} - -bool WSLServer::has_peer(int p_id) const { - return _peer_map.has(p_id); -} - -Ref<WebSocketPeer> WSLServer::get_peer(int p_id) const { - ERR_FAIL_COND_V(!has_peer(p_id), nullptr); - return _peer_map[p_id]; -} - -IPAddress WSLServer::get_peer_address(int p_peer_id) const { - ERR_FAIL_COND_V(!has_peer(p_peer_id), IPAddress()); - - return _peer_map[p_peer_id]->get_connected_host(); -} - -int WSLServer::get_peer_port(int p_peer_id) const { - ERR_FAIL_COND_V(!has_peer(p_peer_id), 0); - - return _peer_map[p_peer_id]->get_connected_port(); -} - -void WSLServer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { - ERR_FAIL_COND(!has_peer(p_peer_id)); - - get_peer(p_peer_id)->close(p_code, p_reason); -} - -Error WSLServer::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) { - ERR_FAIL_COND_V_MSG(_server->is_listening(), FAILED, "Buffers sizes can only be set before listening or connecting."); - - _in_buf_size = nearest_shift(p_in_buffer - 1) + 10; - _in_pkt_size = nearest_shift(p_in_packets - 1); - _out_buf_size = nearest_shift(p_out_buffer - 1) + 10; - _out_pkt_size = nearest_shift(p_out_packets - 1); - return OK; -} - -WSLServer::WSLServer() { - _server.instantiate(); -} - -WSLServer::~WSLServer() { - stop(); -} - -#endif // WEB_ENABLED diff --git a/modules/websocket/wsl_server.h b/modules/websocket/wsl_server.h deleted file mode 100644 index df0c1dc68a..0000000000 --- a/modules/websocket/wsl_server.h +++ /dev/null @@ -1,98 +0,0 @@ -/*************************************************************************/ -/* wsl_server.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -#ifndef WSL_SERVER_H -#define WSL_SERVER_H - -#ifndef WEB_ENABLED - -#include "websocket_server.h" -#include "wsl_peer.h" - -#include "core/io/stream_peer_tcp.h" -#include "core/io/stream_peer_tls.h" -#include "core/io/tcp_server.h" - -class WSLServer : public WebSocketServer { - GDCIIMPL(WSLServer, WebSocketServer); - -private: - class PendingPeer : public RefCounted { - private: - bool _parse_request(const Vector<String> p_protocols, String &r_resource_name); - - public: - Ref<StreamPeerTCP> tcp; - Ref<StreamPeer> connection; - bool use_tls = false; - - uint64_t time = 0; - uint8_t req_buf[WSL_MAX_HEADER_SIZE] = {}; - int req_pos = 0; - String key; - String protocol; - bool has_request = false; - CharString response; - int response_sent = 0; - - Error do_handshake(const Vector<String> p_protocols, uint64_t p_timeout, String &r_resource_name, const Vector<String> &p_extra_headers); - }; - - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; - int _out_pkt_size = DEF_PKT_SHIFT; - - List<Ref<PendingPeer>> _pending; - Ref<TCPServer> _server; - Vector<String> _protocols; - Vector<String> _extra_headers; - -public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - void set_extra_headers(const Vector<String> &p_headers) override; - Error listen(int p_port, const Vector<String> p_protocols = Vector<String>(), bool gd_mp_api = false) override; - void stop() override; - bool is_listening() const override; - int get_max_packet_size() const override; - bool has_peer(int p_id) const override; - Ref<WebSocketPeer> get_peer(int p_id) const override; - IPAddress get_peer_address(int p_peer_id) const override; - int get_peer_port(int p_peer_id) const override; - void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = "") override; - virtual void poll() override; - - WSLServer(); - ~WSLServer(); -}; - -#endif // WEB_ENABLED - -#endif // WSL_SERVER_H diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index 49ff454f07..49dd9f7318 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -18,16 +18,16 @@ func _ready(): # We assume this node has a button as a child. # This button is for the user to consent to entering immersive VR mode. - $Button.connect("pressed", self, "_on_Button_pressed") + $Button.pressed.connect(self._on_Button_pressed) webxr_interface = XRServer.find_interface("WebXR") if webxr_interface: # WebXR uses a lot of asynchronous callbacks, so we connect to various # signals in order to receive them. - webxr_interface.connect("session_supported", self, "_webxr_session_supported") - webxr_interface.connect("session_started", self, "_webxr_session_started") - webxr_interface.connect("session_ended", self, "_webxr_session_ended") - webxr_interface.connect("session_failed", self, "_webxr_session_failed") + webxr_interface.session_supported.connect(self._webxr_session_supported) + webxr_interface.session_started.connect(self._webxr_session_started) + webxr_interface.session_ended.connect(self._webxr_session_ended) + webxr_interface.session_failed.connect(self._webxr_session_failed) # This returns immediately - our _webxr_session_supported() method # (which we connected to the "session_supported" signal above) will diff --git a/modules/webxr/godot_webxr.h b/modules/webxr/godot_webxr.h index 34d068be3e..d8d5bd99cc 100644 --- a/modules/webxr/godot_webxr.h +++ b/modules/webxr/godot_webxr.h @@ -65,7 +65,7 @@ extern int godot_webxr_get_view_count(); extern int *godot_webxr_get_render_target_size(); extern float *godot_webxr_get_transform_for_eye(int p_eye); extern float *godot_webxr_get_projection_for_eye(int p_eye); -extern void godot_webxr_commit_for_eye(int p_eye, unsigned int p_destination_fbo); +extern void godot_webxr_commit(unsigned int p_texture); extern void godot_webxr_sample_controller_data(); extern int godot_webxr_get_controller_count(); diff --git a/modules/webxr/native/library_godot_webxr.js b/modules/webxr/native/library_godot_webxr.js index 9b75796ee5..c476a54c59 100644 --- a/modules/webxr/native/library_godot_webxr.js +++ b/modules/webxr/native/library_godot_webxr.js @@ -337,20 +337,18 @@ const GodotWebXR = { return buf; }, - godot_webxr_commit_for_eye__proxy: 'sync', - godot_webxr_commit_for_eye__sig: 'vii', - godot_webxr_commit_for_eye: function (p_eye, p_destination_fbo) { + godot_webxr_commit__proxy: 'sync', + godot_webxr_commit__sig: 'vi', + godot_webxr_commit: function (p_texture) { if (!GodotWebXR.session || !GodotWebXR.pose) { return; } - const view_index = (p_eye === 2 /* ARVRInterface::EYE_RIGHT */) ? 1 : 0; const glLayer = GodotWebXR.session.renderState.baseLayer; - const view = GodotWebXR.pose.views[view_index]; - const viewport = glLayer.getViewport(view); + const views = GodotWebXR.pose.views; const gl = GodotWebXR.gl; - const framebuffer = GL.framebuffers[p_destination_fbo]; + const texture = GL.textures[p_texture]; const orig_framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); const orig_read_framebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING); @@ -359,14 +357,27 @@ const GodotWebXR = { // Copy from Godot render target into framebuffer from WebXR. gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer); - gl.readBuffer(gl.COLOR_ATTACHMENT0); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, glLayer.framebuffer); - - // Flip Y upside down on destination. - gl.blitFramebuffer(0, 0, viewport.width, viewport.height, - viewport.x, viewport.height, viewport.width, viewport.y, - gl.COLOR_BUFFER_BIT, gl.NEAREST); + for (let i = 0; i < views.length; i++) { + const viewport = glLayer.getViewport(views[i]); + + const read_fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read_fbo); + if (views.length > 1) { + gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture, 0, i); + } else { + gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + } + gl.readBuffer(gl.COLOR_ATTACHMENT0); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, glLayer.framebuffer); + + // Flip Y upside down on destination. + gl.blitFramebuffer(0, 0, viewport.width, viewport.height, + viewport.x, viewport.y + viewport.height, viewport.x + viewport.width, viewport.y, + gl.COLOR_BUFFER_BIT, gl.NEAREST); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + gl.deleteFramebuffer(read_fbo); + } // Restore state. gl.bindFramebuffer(gl.FRAMEBUFFER, orig_framebuffer); diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index 6b671c1660..f6ed9f027e 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -415,8 +415,7 @@ Vector<BlitToScreen> WebXRInterfaceJS::post_draw_viewport(RID p_render_target, c GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target); - // @todo Support multiple eyes! - godot_webxr_commit_for_eye(1, rt->fbo); + godot_webxr_commit(rt->color); return blit_to_screen; }; diff --git a/modules/zip/SCsub b/modules/zip/SCsub new file mode 100644 index 0000000000..b7710123fd --- /dev/null +++ b/modules/zip/SCsub @@ -0,0 +1,9 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_zip = env_modules.Clone() + +# Module files +env_zip.add_source_files(env.modules_sources, "*.cpp") diff --git a/modules/zip/config.py b/modules/zip/config.py new file mode 100644 index 0000000000..96cd2fc5bd --- /dev/null +++ b/modules/zip/config.py @@ -0,0 +1,17 @@ +def can_build(env, platform): + return env["minizip"] + + +def configure(env): + pass + + +def get_doc_classes(): + return [ + "ZIPReader", + "ZIPPacker", + ] + + +def get_doc_path(): + return "doc_classes" diff --git a/modules/zip/doc_classes/ZIPPacker.xml b/modules/zip/doc_classes/ZIPPacker.xml new file mode 100644 index 0000000000..95d7ef50f9 --- /dev/null +++ b/modules/zip/doc_classes/ZIPPacker.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ZIPPacker" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + Allows the creation of zip files. + </brief_description> + <description> + This class implements a writer that allows storing the multiple blobs in a zip archive. + [codeblock] + func write_zip_file(): + var writer := ZIPPacker.new() + var err := writer.open("user://archive.zip") + if err != OK: + return err + writer.start_file("hello.txt") + writer.write_file("Hello World".to_utf8_buffer()) + writer.close_file() + + writer.close() + return OK + [/codeblock] + </description> + <tutorials> + </tutorials> + <methods> + <method name="close"> + <return type="int" enum="Error" /> + <description> + Closes the underlying resources used by this instance. + </description> + </method> + <method name="close_file"> + <return type="int" enum="Error" /> + <description> + Stops writing to a file within the archive. + It will fail if there is no open file. + </description> + </method> + <method name="open"> + <return type="int" enum="Error" /> + <param index="0" name="path" type="String" /> + <param index="1" name="append" type="int" enum="ZIPPacker.ZipAppend" default="0" /> + <description> + Opens a zip file for writing at the given path using the specified write mode. + This must be called before everything else. + </description> + </method> + <method name="start_file"> + <return type="int" enum="Error" /> + <param index="0" name="path" type="String" /> + <description> + Starts writing to a file within the archive. Only one file can be written at the same time. + Must be called after [method open]. + </description> + </method> + <method name="write_file"> + <return type="int" enum="Error" /> + <param index="0" name="data" type="PackedByteArray" /> + <description> + Write the given [param data] to the file. + Needs to be called after [method start_file]. + </description> + </method> + </methods> + <constants> + <constant name="APPEND_CREATE" value="0" enum="ZipAppend"> + </constant> + <constant name="APPEND_CREATEAFTER" value="1" enum="ZipAppend"> + </constant> + <constant name="APPEND_ADDINZIP" value="2" enum="ZipAppend"> + </constant> + </constants> +</class> diff --git a/modules/zip/doc_classes/ZIPReader.xml b/modules/zip/doc_classes/ZIPReader.xml new file mode 100644 index 0000000000..717116a531 --- /dev/null +++ b/modules/zip/doc_classes/ZIPReader.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ZIPReader" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + Allows reading the content of a zip file. + </brief_description> + <description> + This class implements a reader that can extract the content of individual files inside a zip archive. + [codeblock] + func read_zip_file(): + var reader := ZIPReader.new() + var err := reader.open("user://archive.zip") + if err == OK: + return PackedByteArray() + var res := reader.read_file("hello.txt") + reader.close() + return res + [/codeblock] + </description> + <tutorials> + </tutorials> + <methods> + <method name="close"> + <return type="int" enum="Error" /> + <description> + Closes the underlying resources used by this instance. + </description> + </method> + <method name="get_files"> + <return type="PackedStringArray" /> + <description> + Returns the list of names of all files in the loaded archive. + Must be called after [method open]. + </description> + </method> + <method name="open"> + <return type="int" enum="Error" /> + <param index="0" name="path" type="String" /> + <description> + Opens the zip archive at the given [param path] and reads its file index. + </description> + </method> + <method name="read_file"> + <return type="PackedByteArray" /> + <param index="0" name="path" type="String" /> + <param index="1" name="case_sensitive" type="bool" default="true" /> + <description> + Loads the whole content of a file in the loaded zip archive into memory and returns it. + Must be called after [method open]. + </description> + </method> + </methods> +</class> diff --git a/modules/websocket/wsl_client.h b/modules/zip/register_types.cpp index dfb989fdd3..20fb484cfe 100644 --- a/modules/websocket/wsl_client.h +++ b/modules/zip/register_types.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* wsl_client.h */ +/* register_types.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,64 +28,23 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef WSL_CLIENT_H -#define WSL_CLIENT_H +#include "register_types.h" -#ifndef WEB_ENABLED +#include "core/object/class_db.h" +#include "zip_packer.h" +#include "zip_reader.h" -#include "core/error/error_list.h" -#include "core/io/stream_peer_tcp.h" -#include "core/io/stream_peer_tls.h" -#include "websocket_client.h" -#include "wsl_peer.h" -#include "wslay/wslay.h" +void initialize_zip_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } -class WSLClient : public WebSocketClient { - GDCIIMPL(WSLClient, WebSocketClient); + GDREGISTER_CLASS(ZIPPacker); + GDREGISTER_CLASS(ZIPReader); +} -private: - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; - int _out_pkt_size = DEF_PKT_SHIFT; - - Ref<WSLPeer> _peer; - Ref<StreamPeerTCP> _tcp; - Ref<StreamPeer> _connection; - ConnectionStatus _status = CONNECTION_DISCONNECTED; - - CharString _request; - int _requested = 0; - - uint8_t _resp_buf[WSL_MAX_HEADER_SIZE]; - int _resp_pos = 0; - - String _key; - String _host; - uint16_t _port = 0; - Array _ip_candidates; - Vector<String> _protocols; - bool _use_tls = false; - IP::ResolverID _resolver_id = IP::RESOLVER_INVALID_ID; - - void _do_handshake(); - bool _verify_headers(String &r_protocol); - -public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) override; - int get_max_packet_size() const override; - Ref<WebSocketPeer> get_peer(int p_peer_id) const override; - void disconnect_from_host(int p_code = 1000, String p_reason = "") override; - IPAddress get_connected_host() const override; - uint16_t get_connected_port() const override; - virtual ConnectionStatus get_connection_status() const override; - virtual void poll() override; - - WSLClient(); - ~WSLClient(); -}; - -#endif // WEB_ENABLED - -#endif // WSL_CLIENT_H +void uninitialize_zip_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } +} diff --git a/modules/zip/register_types.h b/modules/zip/register_types.h new file mode 100644 index 0000000000..2640be12b8 --- /dev/null +++ b/modules/zip/register_types.h @@ -0,0 +1,39 @@ +/*************************************************************************/ +/* register_types.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef ZIP_REGISTER_TYPES_H +#define ZIP_REGISTER_TYPES_H + +#include "modules/register_module_types.h" + +void initialize_zip_module(ModuleInitializationLevel p_level); +void uninitialize_zip_module(ModuleInitializationLevel p_level); + +#endif // ZIP_REGISTER_TYPES_H diff --git a/modules/zip/zip_packer.cpp b/modules/zip/zip_packer.cpp new file mode 100644 index 0000000000..c37fc0945e --- /dev/null +++ b/modules/zip/zip_packer.cpp @@ -0,0 +1,108 @@ +/*************************************************************************/ +/* zip_packer.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "zip_packer.h" + +#include "core/io/zip_io.h" +#include "core/os/os.h" + +Error ZIPPacker::open(String p_path, ZipAppend p_append) { + if (fa.is_valid()) { + close(); + } + + zlib_filefunc_def io = zipio_create_io(&fa); + zf = zipOpen2(p_path.utf8().get_data(), p_append, NULL, &io); + return zf != NULL ? OK : FAILED; +} + +Error ZIPPacker::close() { + ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker cannot be closed because it is not open."); + + return zipClose(zf, NULL) == ZIP_OK ? OK : FAILED; +} + +Error ZIPPacker::start_file(String p_path) { + ERR_FAIL_COND_V_MSG(zf != NULL, FAILED, "ZIPPacker is already in use."); + ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker must be opened before use."); + + zip_fileinfo zipfi; + + OS::DateTime time = OS::get_singleton()->get_datetime(); + + zipfi.tmz_date.tm_hour = time.hour; + zipfi.tmz_date.tm_mday = time.day; + zipfi.tmz_date.tm_min = time.minute; + zipfi.tmz_date.tm_mon = time.month - 1; + zipfi.tmz_date.tm_sec = time.second; + zipfi.tmz_date.tm_year = time.year; + zipfi.dosDate = 0; + zipfi.external_fa = 0; + zipfi.internal_fa = 0; + + int ret = zipOpenNewFileInZip(zf, p_path.utf8().get_data(), &zipfi, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION); + return ret == ZIP_OK ? OK : FAILED; +} + +Error ZIPPacker::write_file(Vector<uint8_t> p_data) { + ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker must be opened before use."); + + return zipWriteInFileInZip(zf, p_data.ptr(), p_data.size()) == ZIP_OK ? OK : FAILED; +} + +Error ZIPPacker::close_file() { + ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker must be opened before use."); + + Error err = zipCloseFileInZip(zf) == ZIP_OK ? OK : FAILED; + if (err == OK) { + zf = NULL; + } + return err; +} + +void ZIPPacker::_bind_methods() { + ClassDB::bind_method(D_METHOD("open", "path", "append"), &ZIPPacker::open, DEFVAL(Variant(APPEND_CREATE))); + ClassDB::bind_method(D_METHOD("start_file", "path"), &ZIPPacker::start_file); + ClassDB::bind_method(D_METHOD("write_file", "data"), &ZIPPacker::write_file); + ClassDB::bind_method(D_METHOD("close_file"), &ZIPPacker::close_file); + ClassDB::bind_method(D_METHOD("close"), &ZIPPacker::close); + + BIND_ENUM_CONSTANT(APPEND_CREATE); + BIND_ENUM_CONSTANT(APPEND_CREATEAFTER); + BIND_ENUM_CONSTANT(APPEND_ADDINZIP); +} + +ZIPPacker::ZIPPacker() {} + +ZIPPacker::~ZIPPacker() { + if (fa.is_valid()) { + close(); + } +} diff --git a/modules/websocket/websocket_macros.h b/modules/zip/zip_packer.h index b03bd8f45c..23e96b5ad2 100644 --- a/modules/websocket/websocket_macros.h +++ b/modules/zip/zip_packer.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* websocket_macros.h */ +/* zip_packer.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,39 +28,41 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef WEBSOCKET_MACROS_H -#define WEBSOCKET_MACROS_H +#ifndef ZIP_PACKER_H +#define ZIP_PACKER_H -// Defaults per peer buffers, 1024 packets with a shared 65536 bytes payload. -#define DEF_PKT_SHIFT 10 -#define DEF_BUF_SHIFT 16 +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" -#define GDCICLASS(CNAME) \ -public: \ - static CNAME *(*_create)(); \ - \ - static Ref<CNAME> create_ref() { \ - if (!_create) \ - return Ref<CNAME>(); \ - return Ref<CNAME>(_create()); \ - } \ - \ - static CNAME *create() { \ - if (!_create) \ - return nullptr; \ - return _create(); \ - } \ - \ -protected: +#include "thirdparty/minizip/zip.h" + +class ZIPPacker : public RefCounted { + GDCLASS(ZIPPacker, RefCounted); -#define GDCINULL(CNAME) \ - CNAME *(*CNAME::_create)() = nullptr; + Ref<FileAccess> fa; + zipFile zf; -#define GDCIIMPL(IMPNAME, CNAME) \ -public: \ - static CNAME *_create() { return memnew(IMPNAME); } \ - static void make_default() { CNAME::_create = IMPNAME::_create; } \ - \ protected: + static void _bind_methods(); + +public: + enum ZipAppend { + APPEND_CREATE = 0, + APPEND_CREATEAFTER = 1, + APPEND_ADDINZIP = 2, + }; + + Error open(String p_path, ZipAppend p_append); + Error close(); + + Error start_file(String p_path); + Error write_file(Vector<uint8_t> p_data); + Error close_file(); + + ZIPPacker(); + ~ZIPPacker(); +}; + +VARIANT_ENUM_CAST(ZIPPacker::ZipAppend) -#endif // WEBSOCKET_MACROS_H +#endif // ZIP_PACKER_H diff --git a/modules/zip/zip_reader.cpp b/modules/zip/zip_reader.cpp new file mode 100644 index 0000000000..f35b947cef --- /dev/null +++ b/modules/zip/zip_reader.cpp @@ -0,0 +1,123 @@ +/*************************************************************************/ +/* zip_reader.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "zip_reader.h" + +#include "core/error/error_macros.h" +#include "core/io/zip_io.h" + +Error ZIPReader::open(String p_path) { + if (fa.is_valid()) { + close(); + } + + zlib_filefunc_def io = zipio_create_io(&fa); + uzf = unzOpen2(p_path.utf8().get_data(), &io); + return uzf != NULL ? OK : FAILED; +} + +Error ZIPReader::close() { + ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPReader cannot be closed because it is not open."); + + return unzClose(uzf) == UNZ_OK ? OK : FAILED; +} + +PackedStringArray ZIPReader::get_files() { + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedStringArray(), "ZIPReader must be opened before use."); + + List<String> s; + + if (unzGoToFirstFile(uzf) != UNZ_OK) { + return PackedStringArray(); + } + + do { + unz_file_info64 file_info; + char filename[256]; // Note filename is a path ! + int err = unzGetCurrentFileInfo64(uzf, &file_info, filename, sizeof(filename), NULL, 0, NULL, 0); + if (err == UNZ_OK) { + s.push_back(filename); + } else { + // Assume filename buffer was too small + char *long_filename_buff = (char *)memalloc(file_info.size_filename); + int err2 = unzGetCurrentFileInfo64(uzf, NULL, long_filename_buff, sizeof(long_filename_buff), NULL, 0, NULL, 0); + if (err2 == UNZ_OK) { + s.push_back(long_filename_buff); + memfree(long_filename_buff); + } + } + } while (unzGoToNextFile(uzf) == UNZ_OK); + + PackedStringArray arr; + arr.resize(s.size()); + int idx = 0; + for (const List<String>::Element *E = s.front(); E; E = E->next()) { + arr.set(idx++, E->get()); + } + return arr; +} + +PackedByteArray ZIPReader::read_file(String p_path, bool p_case_sensitive) { + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "ZIPReader must be opened before use."); + + int cs = p_case_sensitive ? 1 : 2; + if (unzLocateFile(uzf, p_path.utf8().get_data(), cs) != UNZ_OK) { + ERR_FAIL_V_MSG(PackedByteArray(), "File does not exist in zip archive: " + p_path); + } + if (unzOpenCurrentFile(uzf) != UNZ_OK) { + ERR_FAIL_V_MSG(PackedByteArray(), "Could not open file within zip archive."); + } + + unz_file_info info; + unzGetCurrentFileInfo(uzf, &info, NULL, 0, NULL, 0, NULL, 0); + PackedByteArray data; + data.resize(info.uncompressed_size); + + uint8_t *w = data.ptrw(); + unzReadCurrentFile(uzf, &w[0], info.uncompressed_size); + + unzCloseCurrentFile(uzf); + return data; +} + +ZIPReader::ZIPReader() {} + +ZIPReader::~ZIPReader() { + if (fa.is_valid()) { + close(); + } +} + +void ZIPReader::_bind_methods() { + ClassDB::bind_method(D_METHOD("open", "path"), &ZIPReader::open); + ClassDB::bind_method(D_METHOD("close"), &ZIPReader::close); + ClassDB::bind_method(D_METHOD("get_files"), &ZIPReader::get_files); + ClassDB::bind_method(D_METHOD("read_file", "path", "case_sensitive"), &ZIPReader::read_file, DEFVAL(Variant(true))); +} diff --git a/modules/websocket/emws_client.h b/modules/zip/zip_reader.h index cdcec31e19..fbc2fc0409 100644 --- a/modules/websocket/emws_client.h +++ b/modules/zip/zip_reader.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* emws_client.h */ +/* zip_reader.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,44 +28,32 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef EMWS_CLIENT_H -#define EMWS_CLIENT_H +#ifndef ZIP_READER_H +#define ZIP_READER_H -#ifdef WEB_ENABLED +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" -#include "core/error/error_list.h" -#include "emws_peer.h" -#include "websocket_client.h" +#include "thirdparty/minizip/unzip.h" -class EMWSClient : public WebSocketClient { - GDCIIMPL(EMWSClient, WebSocketClient); +class ZIPReader : public RefCounted { + GDCLASS(ZIPReader, RefCounted) -private: - int _js_id = 0; - bool _is_connecting = false; - int _in_buf_size = DEF_BUF_SHIFT; - int _in_pkt_size = DEF_PKT_SHIFT; - int _out_buf_size = DEF_BUF_SHIFT; + Ref<FileAccess> fa; + unzFile uzf; - static void _esws_on_connect(void *obj, char *proto); - static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); - static void _esws_on_error(void *obj); - static void _esws_on_close(void *obj, int code, const char *reason, int was_clean); +protected: + static void _bind_methods(); public: - Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets) override; - Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_tls, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()) override; - Ref<WebSocketPeer> get_peer(int p_peer_id) const override; - void disconnect_from_host(int p_code = 1000, String p_reason = "") override; - IPAddress get_connected_host() const override; - uint16_t get_connected_port() const override; - virtual ConnectionStatus get_connection_status() const override; - int get_max_packet_size() const override; - virtual void poll() override; - EMWSClient(); - ~EMWSClient(); -}; + Error open(String p_path); + Error close(); + + PackedStringArray get_files(); + PackedByteArray read_file(String p_path, bool p_case_sensitive); -#endif // WEB_ENABLED + ZIPReader(); + ~ZIPReader(); +}; -#endif // EMWS_CLIENT_H +#endif // ZIP_READER_H |