└── 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 |
102 |
--------------------------------------------------------------------------------
/addons/nodewebsockets/nws_server.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
156 |
--------------------------------------------------------------------------------
/addons/nodewebsockets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------