└── addons └── nodewebsockets ├── plugin.cfg ├── icon.svg.import ├── nws_client.svg.import ├── nws_server.svg.import ├── about.gd ├── plugin_nodewebsockets.gd ├── about.tscn ├── plugin_nws_inspector.gd ├── nws_client.svg ├── nws_server.svg ├── icon.svg ├── websocket_client.gd └── websocket_server.gd /addons/nodewebsockets/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="NodeWebSockets" 4 | description="It gives you two new nodes, a WebSocketServer and a WebSocketClient." 5 | author="IcterusGames" 6 | version="1.0.0" 7 | script="plugin_nodewebsockets.gd" 8 | -------------------------------------------------------------------------------- /addons/nodewebsockets/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bqe7syv8xnfh5" 6 | path="res://.godot/imported/icon.svg-f15976e9315d73946aad92fcf6b4e15d.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/nodewebsockets/icon.svg" 15 | dest_files=["res://.godot/imported/icon.svg-f15976e9315d73946aad92fcf6b4e15d.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=3.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/nodewebsockets/nws_client.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://7mxdmyuq37nl" 6 | path="res://.godot/imported/nws_client.svg-d388f53fe70592a252fad1a3f1f7e611.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/nodewebsockets/nws_client.svg" 15 | dest_files=["res://.godot/imported/nws_client.svg-d388f53fe70592a252fad1a3f1f7e611.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=1.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/nodewebsockets/nws_server.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://rth7ripftbdp" 6 | path="res://.godot/imported/nws_server.svg-80f5cde4c4a63371ff6386f77c3bd15e.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/nodewebsockets/nws_server.svg" 15 | dest_files=["res://.godot/imported/nws_server.svg-80f5cde4c4a63371ff6386f77c3bd15e.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=1.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/nodewebsockets/about.gd: -------------------------------------------------------------------------------- 1 | # about.gd 2 | # This file is part of: NodeWebSockets 3 | # Copyright (c) 2023 IcterusGames 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | @tool 25 | extends AcceptDialog 26 | 27 | 28 | func _ready(): 29 | name = "NodeWebSocketsHelpAbout" 30 | get_ok_button().custom_minimum_size.x = 100 31 | 32 | 33 | func _on_rich_text_label_meta_clicked(meta): 34 | OS.shell_open(str(meta)) 35 | -------------------------------------------------------------------------------- /addons/nodewebsockets/plugin_nodewebsockets.gd: -------------------------------------------------------------------------------- 1 | # plugin_nodewebsockets.gd 2 | # This file is part of: NodeWebSockets 3 | # Copyright (c) 2023 IcterusGames 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | @tool 24 | extends EditorPlugin 25 | 26 | var _win_about = null 27 | var _inspector_plugin : EditorInspectorPlugin = null 28 | 29 | 30 | func _enter_tree(): 31 | _win_about = load("res://addons/nodewebsockets/about.tscn").instantiate() 32 | _win_about.visible = false 33 | get_editor_interface().get_base_control().get_window().call_deferred(StringName("add_child"), _win_about) 34 | _inspector_plugin = load("res://addons/nodewebsockets/plugin_nws_inspector.gd").new() 35 | _inspector_plugin.about_pressed.connect(_on_about_pressed) 36 | add_inspector_plugin(_inspector_plugin) 37 | 38 | 39 | func _exit_tree(): 40 | if _win_about != null: 41 | _win_about.queue_free() 42 | if _inspector_plugin != null: 43 | remove_inspector_plugin(_inspector_plugin) 44 | 45 | 46 | func _on_about_pressed(): 47 | if _win_about: 48 | _win_about.popup_centered() 49 | -------------------------------------------------------------------------------- /addons/nodewebsockets/about.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://dwruptugn8p1u"] 2 | 3 | [ext_resource type="Script" path="res://addons/nodewebsockets/about.gd" id="1_5uicp"] 4 | [ext_resource type="Texture2D" uid="uid://bqe7syv8xnfh5" path="res://addons/nodewebsockets/icon.svg" id="1_mi0g5"] 5 | 6 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hnw4u"] 7 | 8 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lwftf"] 9 | 10 | [node name="NodeWebSocketsHelpAbout" type="AcceptDialog"] 11 | title = "About" 12 | size = Vector2i(520, 240) 13 | visible = true 14 | min_size = Vector2i(520, 200) 15 | max_size = Vector2i(1280, 720) 16 | script = ExtResource("1_5uicp") 17 | 18 | [node name="MarginContainer" type="MarginContainer" parent="."] 19 | offset_left = 8.0 20 | offset_top = 8.0 21 | offset_right = 512.0 22 | offset_bottom = 191.0 23 | theme_override_constants/margin_left = 10 24 | theme_override_constants/margin_top = 10 25 | theme_override_constants/margin_right = 10 26 | theme_override_constants/margin_bottom = 10 27 | 28 | [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] 29 | layout_mode = 2 30 | theme_override_constants/separation = 20 31 | 32 | [node name="TextureRect" type="TextureRect" parent="MarginContainer/HBoxContainer"] 33 | custom_minimum_size = Vector2(48, 48) 34 | layout_mode = 2 35 | texture = ExtResource("1_mi0g5") 36 | stretch_mode = 4 37 | 38 | [node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/HBoxContainer"] 39 | layout_mode = 2 40 | size_flags_horizontal = 3 41 | theme_override_styles/focus = SubResource("StyleBoxEmpty_hnw4u") 42 | theme_override_styles/normal = SubResource("StyleBoxEmpty_lwftf") 43 | bbcode_enabled = true 44 | text = "NodeWebSockets Plugin 45 | v. 1.0.0 46 | by IcterusGames 47 | [font_size=7] [/font_size] 48 | [b]Support me on:[/b] 49 | [indent][url]https://icterusgames.itch.io/[/url][/indent] 50 | [indent][url]https://www.patreon.com/IcterusGames[/url][/indent] 51 | [font_size=7] [/font_size] 52 | [b]Source code on:[/b] 53 | [indent][url]https://github.com/IcterusGames/NodeWebSockets[/url][/indent] 54 | " 55 | 56 | [connection signal="meta_clicked" from="MarginContainer/HBoxContainer/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"] 57 | -------------------------------------------------------------------------------- /addons/nodewebsockets/plugin_nws_inspector.gd: -------------------------------------------------------------------------------- 1 | # plugin_nws_inspector.gd 2 | # This file is part of: NodeWebSockets 3 | # Copyright (c) 2023 IcterusGames 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | extends EditorInspectorPlugin 25 | 26 | signal about_pressed 27 | 28 | 29 | func _can_handle(object: Object) -> bool: 30 | if object != null: 31 | if object is WebSocketClient or object is WebSocketServer: 32 | return true 33 | return false 34 | 35 | 36 | func _parse_category(object: Object, category: String): 37 | if category != "websocket_client.gd" and category != "websocket_server.gd": 38 | return 39 | var hbox = HBoxContainer.new() 40 | var label := RichTextLabel.new() 41 | var button := Button.new() 42 | label.fit_content = true 43 | label.bbcode_enabled = true 44 | label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 45 | label.text = "[b]by IcterusGames:[/b]" 46 | button.text = "About" 47 | button.size_flags_horizontal = Control.SIZE_EXPAND_FILL 48 | button.pressed.connect(_on_button_about_pressed) 49 | hbox.add_child(label) 50 | hbox.add_child(button) 51 | add_custom_control(hbox) 52 | 53 | 54 | func _on_button_about_pressed(): 55 | about_pressed.emit() 56 | -------------------------------------------------------------------------------- /addons/nodewebsockets/nws_client.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | NodeWebSocketsClient 21 | 47 | 55 | 56 | 58 | 62 | 67 | 74 | 79 | 86 | 87 | 89 | 90 | 92 | 93 | 94 | IcterusGames 95 | 96 | 97 | NodeWebSocketsClient 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /addons/nodewebsockets/nws_server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | NodeWebSocketsServer 21 | 47 | 55 | 56 | 58 | 62 | 67 | 72 | 77 | 82 | 87 | 94 | 99 | 104 | 109 | 114 | 119 | 126 | 133 | 140 | 141 | 143 | 144 | 146 | 147 | 148 | IcterusGames 149 | 150 | 151 | NodeWebSocketsServer 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /addons/nodewebsockets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | NodeWebSockets 21 | 47 | 55 | 56 | 58 | 62 | 75 | 88 | 101 | 106 | 111 | 118 | 123 | 128 | 141 | 154 | 167 | 168 | 170 | 171 | 173 | 174 | 175 | IcterusGames 176 | 177 | 178 | NodeWebSockets 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /addons/nodewebsockets/websocket_client.gd: -------------------------------------------------------------------------------- 1 | # websocket_client.gd 2 | # This file is part of: NodeWebSockets 3 | # Copyright (c) 2023 IcterusGames 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | @icon("res://addons/nodewebsockets/nws_client.svg") 24 | class_name WebSocketClient 25 | extends Node 26 | ## A WebSocket client node implementation. 27 | ## 28 | ## [b]Usage:[/b][br] 29 | ## Simply add [WebSocketClient] to your scene, config the server on the 30 | ## inspector and connect the events that you need.[br] 31 | 32 | 33 | ## Emitted when the connection to the server is closed. [param was_clean_close] 34 | ## will be true if the connection was shutdown cleanly. 35 | signal connection_closed(was_clean_close : bool) 36 | 37 | ## Emitted when the connection to the server fails. 38 | signal connection_error(error : Error) 39 | 40 | ## Emitted when a connection with the server is established, [param protocol] 41 | ## will contain the sub-protocol agreed with the server. 42 | signal connection_established(peer : WebSocketPeer, protocol : String) 43 | 44 | ## Emitted when a message is received. 45 | signal data_received(peer : WebSocketPeer, message, is_string : bool) 46 | 47 | ## Emitted when a message text is received. 48 | signal text_received(peer : WebSocketPeer, message) 49 | 50 | ## Emitted when the server requests a clean close. You should keep polling 51 | ## until you get a [signal connection_closed] signal to achieve the clean 52 | ## close. See [method WebSocketPeer.close] for more details. 53 | signal server_close_request(code : int, reason : String) 54 | 55 | 56 | enum POLL_MODE { 57 | MANUAL, ## You must to call [method poll] regulary to process any request 58 | IDLE, ## poll is called automaticaly on [method Node._process] 59 | PHYSICS, ## poll is called automaticaly on [method Node._physics_process] 60 | } 61 | 62 | 63 | ## If true the client start listening when [method Node._ready] is called, if 64 | ## false you will need to start the client calling [method connect_to_server] 65 | @export var start_on_ready := true 66 | ## Setup the way to call [method poll] 67 | @export var poll_mode : POLL_MODE = POLL_MODE.IDLE 68 | ## URL to WebSockets server[br][br] 69 | ## [b]Note:[/b] For TLS connections remember use a "wss://" prefix 70 | @export var url_server : String = "ws://127.0.0.1:44380" 71 | ## The server sub-protocols allowed during the WebSocket handshake. 72 | @export var protocols := PackedStringArray() 73 | @export_group("Client parameters") 74 | ## The extra HTTP headers to be sent during the WebSocket handshake. 75 | ## [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. 76 | @export var extra_headers := PackedStringArray() 77 | ## Creates an unsafe TLS client configuration where certificate validation is 78 | ## optional. You can optionally provide a valid trusted_chain, but the common 79 | ## name of the certificates will never be checked.[br][br] 80 | ## [b]Using this configuration for purposes other than testing is not 81 | ## recommended.[/b] 82 | @export var trusted_unsafe : bool = false 83 | ## see [method TLSOptions.client] 84 | @export var trusted_chain : X509Certificate = null 85 | ## see [method TLSOptions.client] 86 | @export var trusted_common_name_override : String = "" 87 | ## The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the inbound packets). 88 | @export_range(1, 0x7FFFFFFF) var inbound_buffer_size : int = 65536 89 | ## The maximum amount of packets that will be allowed in the queues (both inbound and outbound). 90 | @export_range(1, 0xFFFF) var max_queued_packets : int = 2048 91 | ## The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the outbound packets). 92 | @export_range(1, 0x7FFFFFFF) var outbound_buffer_size : int = 65536 93 | 94 | var _socket := WebSocketPeer.new() 95 | var _is_connected := false 96 | 97 | 98 | func _ready(): 99 | set_process(false) 100 | set_physics_process(false) 101 | if start_on_ready: 102 | connect_to_server.call_deferred() 103 | 104 | 105 | func _process(_delta): 106 | if poll_mode != POLL_MODE.IDLE: 107 | set_process(false) 108 | return 109 | poll() 110 | 111 | 112 | func _physics_process(_delta): 113 | if poll_mode != POLL_MODE.PHYSICS: 114 | set_physics_process(false) 115 | return 116 | poll() 117 | 118 | 119 | ## Connect to the server given by [member url_server] 120 | func connect_to_server() -> Error: 121 | return connect_to_url(url_server) 122 | 123 | 124 | ## Connects to the given URL requesting one of the given 125 | ## [param array_protocols] as sub-protocol. If the list empty (default), no 126 | ## sub-protocol will be requested.[br] 127 | ## [br] 128 | ## You can optionally pass a list of [param array_headers] to be added to the 129 | ## handshake HTTP request. 130 | func connect_to_url(url : String, array_protocols = null, array_headers = null) -> Error: 131 | if _socket.get_ready_state() != WebSocketPeer.STATE_CLOSED: 132 | connection_error.emit(ERR_ALREADY_IN_USE) 133 | return ERR_ALREADY_IN_USE 134 | url_server = url 135 | _is_connected = false 136 | if array_protocols != null: 137 | protocols = array_protocols 138 | if array_headers != null: 139 | extra_headers = array_headers 140 | _socket.supported_protocols = protocols 141 | _socket.handshake_headers = extra_headers 142 | _socket.inbound_buffer_size = inbound_buffer_size 143 | _socket.max_queued_packets = max_queued_packets 144 | _socket.outbound_buffer_size = outbound_buffer_size 145 | var result : Error = OK 146 | var tls_options = null 147 | if trusted_unsafe: 148 | tls_options = TLSOptions.client_unsafe(trusted_chain) 149 | elif trusted_chain != null or trusted_common_name_override.length() > 0: 150 | tls_options = TLSOptions.client(trusted_chain, trusted_common_name_override) 151 | result = _socket.connect_to_url(url, tls_options) 152 | if result == OK: 153 | match poll_mode: 154 | POLL_MODE.MANUAL: 155 | set_process(false) 156 | set_physics_process(false) 157 | POLL_MODE.IDLE: 158 | set_process(true) 159 | set_physics_process(false) 160 | POLL_MODE.PHYSICS: 161 | set_process(false) 162 | set_physics_process(true) 163 | else: 164 | connection_error.emit(result) 165 | return result 166 | 167 | 168 | ## Return true if is connected to server 169 | func is_listening() -> bool: 170 | return _socket.get_ready_state() != WebSocketPeer.STATE_CLOSED 171 | 172 | 173 | ## Disconnects this client from the connected host.[br] 174 | ## See [method WebSocketPeer.close] for more information. 175 | func disconnect_from_host(code : int = 1000, reason : String = "") -> void: 176 | _socket.close(code, reason) 177 | 178 | 179 | ## Disconnects this client from the connected host.[br] 180 | ## See [method WebSocketPeer.close] for more information. 181 | func close(code : int = 1000, reason : String = "") -> void: 182 | _socket.close(code, reason) 183 | 184 | 185 | ## Return the IP address of the currently connected host. 186 | func get_connected_host() -> String: 187 | return _socket.get_connected_host() 188 | 189 | 190 | ## Return the IP port of the currently connected host. 191 | func get_connected_port() -> int: 192 | return _socket.get_connected_port() 193 | 194 | 195 | ## Return the [class WebSocketPeer] of this client 196 | func get_peer() -> WebSocketPeer: 197 | return _socket 198 | 199 | 200 | func poll() -> void: 201 | _socket.poll() 202 | match _socket.get_ready_state(): 203 | WebSocketPeer.STATE_CONNECTING: 204 | print_verbose("WebSocketClient: connecting to \"", url_server, "\" ...") 205 | 206 | WebSocketPeer.STATE_OPEN: 207 | if not _is_connected: 208 | print_verbose("WebSocketClient: connection established.") 209 | _is_connected = true 210 | connection_established.emit(_socket, _socket.get_selected_protocol()) 211 | while _socket.get_available_packet_count(): 212 | var message = _socket.get_packet() 213 | var is_string = _socket.was_string_packet() 214 | var error = _socket.get_packet_error() 215 | if error != OK: 216 | print_verbose("WebSocketClient: packet recived with error: ", error, " is string: ", is_string, " packet: ", message) 217 | continue 218 | if is_string: 219 | var msg_str = message.get_string_from_utf8() 220 | data_received.emit(_socket, message, is_string) 221 | text_received.emit(_socket, msg_str) 222 | else: 223 | data_received.emit(_socket, message, is_string) 224 | 225 | WebSocketPeer.STATE_CLOSING: 226 | print_verbose("State closing...") 227 | # Keep polling to achieve proper close. 228 | 229 | WebSocketPeer.STATE_CLOSED: 230 | var code = _socket.get_close_code() 231 | var reason = _socket.get_close_reason() 232 | print_verbose("WebSocketClient: closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1]) 233 | server_close_request.emit(code, reason) 234 | connection_closed.emit(code != -1) 235 | set_process(false) 236 | set_physics_process(false) 237 | 238 | 239 | ## Configures the buffer sizes for this [WebSocketPeer].[br] 240 | ## [br] 241 | ## The first two parameters define the size and queued packets limits of the 242 | ## input buffer, the last are for the output buffer.[br] 243 | ## [br] 244 | ## Buffer sizes are expressed in KiB 245 | func set_buffers(_input_buffer_size_kb: int, _input_max_packets: int, _output_buffer_size_kb: int) -> Error: 246 | if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 247 | return ERR_ALREADY_IN_USE 248 | if _input_buffer_size_kb <= 0 or _input_max_packets <= 0 or _output_buffer_size_kb <= 0: 249 | return ERR_PARAMETER_RANGE_ERROR 250 | inbound_buffer_size = _input_buffer_size_kb * 1024 251 | max_queued_packets = _input_max_packets 252 | outbound_buffer_size = _output_buffer_size_kb * 1024 253 | return OK 254 | 255 | 256 | ## see [method WebSocketPeer.send] 257 | func send(message : PackedByteArray, write_mode : WebSocketPeer.WriteMode = 1) -> Error: 258 | return _socket.send(message, write_mode) 259 | 260 | 261 | ## see [method WebSocketPeer.send_text] 262 | func send_text(message : String) -> Error: 263 | return _socket.send_text(message) 264 | -------------------------------------------------------------------------------- /addons/nodewebsockets/websocket_server.gd: -------------------------------------------------------------------------------- 1 | # websocket_server.gd 2 | # This file is part of: NodeWebSockets 3 | # Copyright (c) 2023 IcterusGames 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | @icon("res://addons/nodewebsockets/nws_server.svg") 24 | class_name WebSocketServer 25 | extends Node 26 | ## A WebSocket server node implementation. 27 | ## 28 | ## [b]Usage:[/b][br] 29 | ## Simply add [WebSocketServer] to your scene, config the server on the 30 | ## inspector and connect the events that you need.[br] 31 | 32 | ## Emitted when a client requests a clean close. You should keep polling until 33 | ## you get a [signal client_disconnected] signal with the same [param id] to 34 | ## achieve the clean close. See [method WebSocketPeer.close] for more details. 35 | signal client_close_request(peer : WebSocketPeer, id : int, code : int, reason : String) 36 | 37 | ## Emitted when a new client connects. [param protocol] will be the 38 | ## sub-protocol agreed with the client. 39 | signal client_connected(peer : WebSocketPeer, id : int, protocol : String) 40 | 41 | ## Emitted when a client disconnects. [param was_clean_close] will be true if 42 | ## the connection was shutdown cleanly. 43 | signal client_disconnected(peer : WebSocketPeer, id : int, was_clean_close : bool) 44 | 45 | ## Emitted when an error occurred on [method listen] 46 | signal connection_error(error : Error) 47 | 48 | ## Emitted when the server is started 49 | signal server_listen() 50 | 51 | ## Emitted when the server close 52 | signal server_closed() 53 | 54 | ## Emitted when a new message is received. 55 | signal data_received(peer : WebSocketPeer, id : int, message, is_string : bool) 56 | 57 | ## Emitted when a new text message is received. 58 | signal text_received(peer : WebSocketPeer, id : int, message : String) 59 | 60 | 61 | enum POLL_MODE { 62 | MANUAL, ## You must to call [method poll] regulary to process any request 63 | IDLE, ## poll is called automaticaly on [method Node._process] 64 | PHYSICS, ## poll is called automaticaly on [method Node._physics_process] 65 | } 66 | 67 | 68 | ## If true the server start listening when [method Node._ready] is called, if 69 | ## false you will need to start the server calling [method listen] 70 | @export var start_on_ready := true 71 | ## Setup the way to call [method poll] 72 | @export var poll_mode : POLL_MODE = POLL_MODE.IDLE 73 | ## If [member start_on_ready] is enable the server starts listening on this port 74 | @export_range(1, 65535) var server_port : int = 44380 75 | ## The server sub-protocols allowed during the WebSocket handshake. 76 | @export var protocols := PackedStringArray() 77 | @export_group("Server parameters") 78 | ## When not set to * will restrict incoming connections to the specified IP 79 | ## address. Setting bind_ip to 127.0.0.1 will cause the server to listen only 80 | ## to the local host. 81 | @export var bind_ip := "*" 82 | ## The time in seconds before a pending client (i.e. a client that has not yet 83 | ## finished the HTTP handshake) is considered stale and forcefully disconnected. 84 | @export var handshake_timeout := 3.0 85 | ## If true, this server refuses new connections. 86 | @export var refuse_new_connections := false 87 | ## The extra HTTP headers to be sent during the WebSocket handshake. 88 | ## [b]Note:[/b] Not supported in Web exports due to browsers' restrictions. 89 | @export var extra_headers := PackedStringArray() 90 | ## Certificate requiered to start a TLS server, a valid [member server_key] must be provided too 91 | @export var server_certificate : X509Certificate = null 92 | ## Key requiered to start a TLS server, a valid [member server_certificate] must be provided too 93 | @export var server_key : CryptoKey = null 94 | ## The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the inbound packets). 95 | @export_range(1, 0x7FFFFFFF) var inbound_buffer_size : int = 65536 96 | ## The maximum amount of packets that will be allowed in the queues (both inbound and outbound). 97 | @export_range(1, 0xFFFF) var max_queued_packets : int = 2048 98 | ## The size of the input buffer in bytes (roughly the maximum amount of memory that will be allocated for the outbound packets). 99 | @export_range(1, 0x7FFFFFFF) var outbound_buffer_size : int = 65536 100 | 101 | 102 | class _Client : 103 | enum STATUS { 104 | UNKNOWN, 105 | TCP, 106 | TLS, 107 | CONNECTED, 108 | HANDSHAKE, 109 | } 110 | var socket : WebSocketPeer = null 111 | var stream : StreamPeerTCP = null 112 | var stream_tls : StreamPeerTLS = null 113 | var status : int = STATUS.UNKNOWN 114 | var start_time : int = 0 115 | 116 | var _tcp := TCPServer.new() 117 | var _clients : Dictionary = {} 118 | var _clients_erase : Array[int] = [] 119 | var _target_peer : int = MultiplayerPeer.TARGET_PEER_BROADCAST 120 | var _closed := true 121 | 122 | 123 | func _ready(): 124 | set_process(false) 125 | set_physics_process(false) 126 | if start_on_ready: 127 | listen.call_deferred(server_port, protocols) 128 | 129 | 130 | func _process(_delta): 131 | if poll_mode != 1: 132 | set_process(false) 133 | return 134 | poll() 135 | 136 | 137 | func _physics_process(_delta): 138 | if poll_mode != 2: 139 | set_physics_process(false) 140 | return 141 | poll() 142 | 143 | 144 | ## Starts listening on the given port.[br] 145 | ## [br] 146 | ## You can specify the desired subprotocols via the "protocols" array. 147 | ## If the list empty (default), no sub-protocol will be requested.[br] 148 | func listen(port : int = 0, list_protocols = null) -> Error: 149 | if _tcp.is_listening(): 150 | connection_error.emit(ERR_ALREADY_IN_USE) 151 | return ERR_ALREADY_IN_USE 152 | if port != 0: 153 | server_port = port 154 | if list_protocols != null: 155 | protocols = list_protocols 156 | var res := _tcp.listen(server_port, bind_ip) 157 | if res != OK: 158 | connection_error.emit(res) 159 | else: 160 | _closed = false 161 | match poll_mode: 162 | POLL_MODE.MANUAL: 163 | set_process(false) 164 | set_physics_process(false) 165 | POLL_MODE.IDLE: 166 | set_process(true) 167 | set_physics_process(false) 168 | POLL_MODE.PHYSICS: 169 | set_process(false) 170 | set_physics_process(true) 171 | server_listen.emit() 172 | return res 173 | 174 | 175 | ## This needs to be called in order to have any request processed. 176 | func poll() -> void: 177 | if not _tcp.is_listening(): 178 | if not _closed: 179 | stop() 180 | return 181 | 182 | while _tcp.is_connection_available(): 183 | if refuse_new_connections: 184 | _tcp.take_connection().disconnect_from_host() 185 | continue 186 | print_verbose("WebSocketServer: TCP connection is available...") 187 | var client := _Client.new() 188 | client.status = _Client.STATUS.TCP 189 | client.start_time = Time.get_ticks_msec() 190 | client.stream = _tcp.take_connection() 191 | if server_certificate != null and server_key != null: 192 | client.status = _Client.STATUS.TLS 193 | client.stream_tls = StreamPeerTLS.new() 194 | var result = client.stream_tls.accept_stream(client.stream, TLSOptions.server(server_key, server_certificate)) 195 | if result != OK: 196 | print_verbose("WebSocketServer: TLS accept stream error: ", result) 197 | continue 198 | var id = 0 199 | while id == 0 or id == 1 or _clients.has(id): 200 | id = ((randi() << 8) + Time.get_ticks_msec()) & 0x7FFFFFFF 201 | _clients[id] = client 202 | 203 | for client_id in _clients: 204 | var client : _Client = _clients[client_id] 205 | 206 | match client.status: 207 | _Client.STATUS.TCP: 208 | client.stream.poll() 209 | match client.stream.get_status(): 210 | StreamPeerTCP.STATUS_NONE: 211 | print_verbose("WebSocketServer: TCP status none.") 212 | 213 | StreamPeerTCP.STATUS_CONNECTING: 214 | print_verbose("WebSocketServer: TCP status connecting...") 215 | 216 | StreamPeerTCP.STATUS_CONNECTED: 217 | print_verbose("WebSocketServer: TCP status connected, creating WebSocketPeer...") 218 | client.socket = WebSocketPeer.new() 219 | client.socket.supported_protocols = protocols 220 | client.socket.handshake_headers = extra_headers 221 | client.socket.inbound_buffer_size = inbound_buffer_size 222 | client.socket.max_queued_packets = max_queued_packets 223 | client.socket.outbound_buffer_size = outbound_buffer_size 224 | client.socket.accept_stream(client.stream) 225 | client.status = _Client.STATUS.HANDSHAKE 226 | 227 | StreamPeerTCP.STATUS_ERROR: 228 | print_verbose("WebSocketServer: TCP status error.") 229 | _clients_erase.append(client_id) 230 | continue 231 | 232 | _Client.STATUS.TLS: 233 | client.stream_tls.poll() 234 | match client.stream_tls.get_status(): 235 | StreamPeerTLS.STATUS_HANDSHAKING: 236 | print_verbose("WebSocketServer: TLS handshaking...") 237 | 238 | StreamPeerTLS.STATUS_CONNECTED: 239 | print_verbose("WebSocketServer: TLS status connected, creating WebSocketPeer...") 240 | client.socket = WebSocketPeer.new() 241 | client.socket.supported_protocols = protocols 242 | client.socket.handshake_headers = extra_headers 243 | client.socket.inbound_buffer_size = inbound_buffer_size 244 | client.socket.max_queued_packets = max_queued_packets 245 | client.socket.outbound_buffer_size = outbound_buffer_size 246 | client.socket.accept_stream(client.stream_tls) 247 | client.status = _Client.STATUS.HANDSHAKE 248 | 249 | StreamPeerTLS.STATUS_ERROR: 250 | print_verbose("WebSocketServer: TLS status error.") 251 | _clients_erase.append(client_id) 252 | continue 253 | 254 | StreamPeerTLS.STATUS_DISCONNECTED: 255 | print_verbose("WebSocketServer: TLS status disconnected.") 256 | _clients_erase.append(client_id) 257 | continue 258 | 259 | StreamPeerTLS.STATUS_ERROR_HOSTNAME_MISMATCH: 260 | print_verbose("WebSocketServer: TLS status error hostname mismatch.") 261 | _clients_erase.append(client_id) 262 | continue 263 | 264 | _Client.STATUS.HANDSHAKE: 265 | client.socket.poll() 266 | match client.socket.get_ready_state(): 267 | WebSocketPeer.STATE_CONNECTING: 268 | print_verbose("WebSocketServer: handshake client connecting...") 269 | if Time.get_ticks_msec() - client.start_time > handshake_timeout * 1000: 270 | print_verbose("WebSocketServer: handshake client timeout.") 271 | _clients_erase.append(client_id) 272 | continue 273 | 274 | WebSocketPeer.STATE_OPEN: 275 | print_verbose("WebSocketServer: handshake client connected.") 276 | client.status = _Client.STATUS.CONNECTED 277 | client_connected.emit(client.socket, client_id, client.socket.get_selected_protocol()) 278 | 279 | WebSocketPeer.STATE_CLOSED: 280 | print_verbose("WebSocketServer: handshake client closed.") 281 | _clients_erase.append(client_id) 282 | continue 283 | 284 | _Client.STATUS.CONNECTED: 285 | client.socket.poll() 286 | match client.socket.get_ready_state(): 287 | WebSocketPeer.STATE_CONNECTING: 288 | print_verbose("WebSocketServer: client conecting...") 289 | 290 | WebSocketPeer.STATE_OPEN: 291 | while client.socket.get_available_packet_count(): 292 | var message = client.socket.get_packet() 293 | var is_string = client.socket.was_string_packet() 294 | var error = client.socket.get_packet_error() 295 | if error != OK: 296 | print_verbose("WebSocketServer: packet recived with error: ", error, " is string: ", is_string, " client: ", client_id, " / ", client.socket.get_connected_host(), " packet: ", message) 297 | continue 298 | if is_string: 299 | var msg_str = message.get_string_from_utf8() 300 | data_received.emit(client.socket, client_id, message, is_string) 301 | text_received.emit(client.socket, client_id, msg_str) 302 | else: 303 | data_received.emit(client.socket, client_id, message, is_string) 304 | 305 | WebSocketPeer.STATE_CLOSING: 306 | print_verbose("WebSocketServer: client ", client_id, " / ", client.socket.get_connected_host(), " closing...") 307 | # Keep polling to achieve proper close. 308 | 309 | WebSocketPeer.STATE_CLOSED: 310 | var code = client.socket.get_close_code() 311 | var reason = client.socket.get_close_reason() 312 | print_verbose("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1]) 313 | client_close_request.emit(client.socket, client_id, code, reason) 314 | _clients_erase.append(client_id) 315 | client_disconnected.emit(client.socket, client_id, code != -1) 316 | continue 317 | 318 | for client_id in _clients_erase: 319 | _clients.erase(client_id) 320 | _clients_erase.clear() 321 | 322 | 323 | ## Disconnects the peer identified by [param id] from the server.[br] 324 | ## See [method WebSocketPeer.close] for more information 325 | func disconnect_peer(id : int, code : int = 1000, reason : String = "") -> void: 326 | if id == MultiplayerPeer.TARGET_PEER_BROADCAST: 327 | for client_id in _clients: 328 | _clients[client_id].socket.close(code, reason) 329 | return 330 | if not _clients.has(id): 331 | return 332 | _clients[id].socket.close(code, reason) 333 | 334 | 335 | ## Returns an array with the id of all current connected clients 336 | func get_clients() -> Array[int]: 337 | var result := Array(_clients.keys(), TYPE_INT, "", null) 338 | for client_id in _clients_erase: 339 | result.erase(client_id) 340 | return result 341 | 342 | 343 | ## see [method WebSocketPeer.get_connected_host] 344 | func get_peer_address(id : int) -> String: 345 | if not _clients.has(id): 346 | return "" 347 | return _clients[id].socket.get_connected_host() 348 | 349 | 350 | ## see [method WebSocketPeer.get_connected_port] 351 | func get_peer_port(id : int) -> int: 352 | if not _clients.has(id): 353 | return -1 354 | return _clients[id].socket.get_connected_port() 355 | 356 | 357 | ## Returns true if a peer with the given ID is connected. 358 | func has_peer(id : int) -> bool: 359 | if _clients_erase.has(id): 360 | return false 361 | return _clients.has(id) 362 | 363 | 364 | ## Returns the WebSocketPeer associated to the given [param id], or null if the 365 | ## client is not found. 366 | func get_peer(id : int) -> WebSocketPeer: 367 | if not _clients.has(id): 368 | return null 369 | if _clients_erase.has(id): 370 | return null 371 | return _clients[id].socket 372 | 373 | 374 | ## Returns true if the server is actively listening on a port. 375 | func is_listening() -> bool: 376 | return _tcp.is_listening() 377 | 378 | 379 | ## Configures the buffer sizes for the client [WebSocketPeer].[br] 380 | ## [br] 381 | ## The first two parameters define the size and queued packets limits of the 382 | ## input buffer, the last are for the output buffer.[br] 383 | ## [br] 384 | ## Buffer sizes are expressed in KiB 385 | func set_buffers(_input_buffer_size_kb: int, _input_max_packets: int, _output_buffer_size_kb: int) -> Error: 386 | if _tcp.is_listening(): 387 | return ERR_ALREADY_IN_USE 388 | if _input_buffer_size_kb <= 0 or _input_max_packets <= 0 or _output_buffer_size_kb <= 0: 389 | return ERR_PARAMETER_RANGE_ERROR 390 | inbound_buffer_size = _input_buffer_size_kb * 1024 391 | max_queued_packets = _input_max_packets 392 | outbound_buffer_size = _output_buffer_size_kb * 1024 393 | return OK 394 | 395 | 396 | ## Sets additional headers to be sent to clients during the HTTP handshake. 397 | func set_extra_headers(headers : PackedStringArray = PackedStringArray()) -> void: 398 | extra_headers = headers 399 | 400 | 401 | ## Stops the server and clear its state. 402 | func stop() -> void: 403 | _tcp.stop() 404 | _clients.clear() 405 | _clients_erase.clear() 406 | if not _closed: 407 | _closed = true 408 | server_closed.emit() 409 | 410 | 411 | ## Sets the peer to which packets will be sent. 412 | ## The [param id] can be one of: [constant MultiplayerPeer.TARGET_PEER_BROADCAST] 413 | ## to send to all connected peers, [constant MultiplayerPeer.TARGET_PEER_SERVER] 414 | ## to send to the peer acting as server, a valid peer ID to send to that 415 | ## specific peer, a negative peer ID to send to all peers except that one. By 416 | ## default, the target peer is [constant MultiplayerPeer.TARGET_PEER_BROADCAST]. 417 | func set_target_peer(id : int) -> void: 418 | _target_peer = id 419 | 420 | 421 | ## Sends a raw packet. See [method set_target_peer] 422 | func put_packet(buffer : PackedByteArray) -> Error: 423 | if not _tcp.is_listening(): 424 | return ERR_UNCONFIGURED 425 | if _target_peer == MultiplayerPeer.TARGET_PEER_SERVER: 426 | return OK 427 | elif _target_peer < 0: 428 | _target_peer *= -1 429 | for client_id in _clients: 430 | if client_id != _target_peer: 431 | var client : _Client = _clients[client_id] 432 | if client.status == _Client.STATUS.CONNECTED and \ 433 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 434 | client.socket.put_packet(buffer) 435 | else: 436 | for client_id in _clients: 437 | if _target_peer == MultiplayerPeer.TARGET_PEER_BROADCAST or client_id == _target_peer: 438 | var client : _Client = _clients[client_id] 439 | if client.status == _Client.STATUS.CONNECTED and \ 440 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 441 | client.socket.put_packet(buffer) 442 | return OK 443 | 444 | 445 | ## Sends a binary packet. See [method set_target_peer] 446 | func send(message : PackedByteArray) -> Error: 447 | if not _tcp.is_listening(): 448 | return ERR_UNCONFIGURED 449 | if _target_peer == MultiplayerPeer.TARGET_PEER_SERVER: 450 | return OK 451 | elif _target_peer < 0: 452 | _target_peer *= -1 453 | for client_id in _clients: 454 | if client_id != _target_peer: 455 | var client : _Client = _clients[client_id] 456 | if client.status == _Client.STATUS.CONNECTED and \ 457 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 458 | client.socket.send(message) 459 | else: 460 | for client_id in _clients: 461 | if _target_peer == MultiplayerPeer.TARGET_PEER_BROADCAST or client_id == _target_peer: 462 | var client : _Client = _clients[client_id] 463 | if client.status == _Client.STATUS.CONNECTED and \ 464 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 465 | client.socket.send(message) 466 | return OK 467 | 468 | 469 | ## Sends a string message. See [method set_target_peer] 470 | func send_text(message : String) -> Error: 471 | if not _tcp.is_listening(): 472 | return ERR_UNCONFIGURED 473 | if _target_peer == MultiplayerPeer.TARGET_PEER_SERVER: 474 | return OK 475 | elif _target_peer < 0: 476 | _target_peer *= -1 477 | for client_id in _clients: 478 | if client_id != _target_peer: 479 | var client : _Client = _clients[client_id] 480 | if client.status == _Client.STATUS.CONNECTED and \ 481 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 482 | client.socket.send_text(message) 483 | else: 484 | for client_id in _clients: 485 | if _target_peer == MultiplayerPeer.TARGET_PEER_BROADCAST or client_id == _target_peer: 486 | var client : _Client = _clients[client_id] 487 | if client.status == _Client.STATUS.CONNECTED and \ 488 | client.socket.get_ready_state() == WebSocketPeer.STATE_OPEN: 489 | client.socket.send_text(message) 490 | return OK 491 | 492 | --------------------------------------------------------------------------------