Browse Source

[Net] Update & refactor WebSocket Chat demo.

Uses new unified StreamPeer, dropped the multiplayer part (in favor of
the dedicated WebSocket demo), add reference WebSocketClient and
WebSocketServer signal-based implementations that can be used as drop-in
nodes in any project. Might be worth maintaning it as a separate addon.
Fabio Alessandrelli 2 years ago
parent
commit
dd2ba9a5ba

+ 85 - 0
networking/websocket_chat/chat.tscn

@@ -0,0 +1,85 @@
+[gd_scene format=3 uid="uid://cyvrywci15kev"]
+
+[node name="Chat" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Panel" type="Panel" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Listen" type="HBoxContainer" parent="Panel/VBoxContainer"]
+offset_right = 1152.0
+
+[node name="Connect" type="HBoxContainer" parent="Panel/VBoxContainer"]
+offset_top = 4.0
+offset_right = 1152.0
+offset_bottom = 35.0
+
+[node name="Host" type="LineEdit" parent="Panel/VBoxContainer/Connect"]
+offset_right = 930.0
+offset_bottom = 31.0
+size_flags_horizontal = 3
+text = "ws://localhost:8000/test/"
+placeholder_text = "ws://my.server/path/"
+
+[node name="Connect" type="Button" parent="Panel/VBoxContainer/Connect"]
+offset_left = 934.0
+offset_right = 1006.0
+offset_bottom = 31.0
+toggle_mode = true
+text = "Connect"
+
+[node name="Port" type="SpinBox" parent="Panel/VBoxContainer/Connect"]
+offset_left = 1010.0
+offset_right = 1093.0
+offset_bottom = 31.0
+min_value = 1.0
+max_value = 65535.0
+value = 8000.0
+
+[node name="Listen" type="Button" parent="Panel/VBoxContainer/Connect"]
+offset_left = 1097.0
+offset_right = 1152.0
+offset_bottom = 31.0
+toggle_mode = true
+text = "Listen"
+
+[node name="Send" type="HBoxContainer" parent="Panel/VBoxContainer"]
+offset_top = 39.0
+offset_right = 1152.0
+offset_bottom = 70.0
+
+[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/Send"]
+offset_right = 1101.0
+offset_bottom = 31.0
+size_flags_horizontal = 3
+placeholder_text = "Enter some text to send..."
+
+[node name="Send" type="Button" parent="Panel/VBoxContainer/Send"]
+offset_left = 1105.0
+offset_right = 1152.0
+offset_bottom = 31.0
+text = "Send"
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"]
+offset_top = 74.0
+offset_right = 1152.0
+offset_bottom = 648.0
+size_flags_vertical = 3

+ 47 - 0
networking/websocket_chat/client.gd

@@ -0,0 +1,47 @@
+extends Control
+
+@onready var _client : WebSocketClient = $WebSocketClient
+@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
+@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit
+@onready var _host = $Panel/VBoxContainer/Connect/Host
+
+func info(msg):
+	print(msg)
+	_log_dest.add_text(str(msg) + "\n")
+
+
+# Client signals
+func _on_web_socket_client_connection_closed():
+	var ws = _client.get_socket()
+	info("Client just disconnected with code: %s, reson: %s" % [ws.get_close_code(), ws.get_close_reason()])
+
+
+func _on_web_socket_client_connected_to_server():
+	info("Client just connected with protocol: %s" % _client.get_socket().get_selected_protocol())
+
+
+func _on_web_socket_client_message_received(message):
+	info("%s" % message)
+
+
+# UI signals.
+func _on_send_pressed():
+	if _line_edit.text == "":
+		return
+
+	info("Sending message: %s" % [_line_edit.text])
+	_client.send(_line_edit.text)
+	_line_edit.text = ""
+
+
+func _on_connect_toggled(pressed):
+	if not pressed:
+		_client.close()
+		return
+	if _host.text == "":
+		return
+	info("Connecting to host: %s." % [_host.text])
+	var err = _client.connect_to_url(_host.text)
+	if err != OK:
+		info("Error connecting to host: %s" % [_host.text])
+		return

+ 59 - 0
networking/websocket_chat/client.tscn

@@ -0,0 +1,59 @@
+[gd_scene load_steps=4 format=3 uid="uid://ph5ghsflqegf"]
+
+[ext_resource type="PackedScene" uid="uid://cyvrywci15kev" path="res://chat.tscn" id="1_cfcun"]
+[ext_resource type="Script" path="res://websocket/WebSocketClient.gd" id="2_m4g4y"]
+[ext_resource type="Script" path="res://client.gd" id="2_opbid"]
+
+[node name="Client" instance=ExtResource("1_cfcun")]
+script = ExtResource("2_opbid")
+
+[node name="WebSocketClient" type="Node" parent="." index="0"]
+script = ExtResource("2_m4g4y")
+supported_protocols = PackedStringArray("demo-chat")
+
+[node name="Panel" parent="." index="1"]
+layout_mode = 1
+
+[node name="VBoxContainer" parent="Panel" index="0"]
+layout_mode = 1
+
+[node name="Listen" parent="Panel/VBoxContainer" index="0"]
+layout_mode = 2
+
+[node name="Connect" parent="Panel/VBoxContainer" index="1"]
+layout_mode = 2
+
+[node name="Host" parent="Panel/VBoxContainer/Connect" index="0"]
+layout_mode = 2
+offset_right = 1076.0
+
+[node name="Connect" parent="Panel/VBoxContainer/Connect" index="1"]
+layout_mode = 2
+offset_left = 1080.0
+offset_right = 1152.0
+
+[node name="Port" parent="Panel/VBoxContainer/Connect" index="2"]
+visible = false
+layout_mode = 2
+
+[node name="Listen" parent="Panel/VBoxContainer/Connect" index="3"]
+visible = false
+layout_mode = 2
+
+[node name="Send" parent="Panel/VBoxContainer" index="2"]
+layout_mode = 2
+
+[node name="LineEdit" parent="Panel/VBoxContainer/Send" index="0"]
+layout_mode = 2
+
+[node name="Send" parent="Panel/VBoxContainer/Send" index="1"]
+layout_mode = 2
+
+[node name="RichTextLabel" parent="Panel/VBoxContainer" index="3"]
+layout_mode = 2
+
+[connection signal="connected_to_server" from="WebSocketClient" to="." method="_on_web_socket_client_connected_to_server"]
+[connection signal="connection_closed" from="WebSocketClient" to="." method="_on_web_socket_client_connection_closed"]
+[connection signal="message_received" from="WebSocketClient" to="." method="_on_web_socket_client_message_received"]
+[connection signal="toggled" from="Panel/VBoxContainer/Connect/Connect" to="." method="_on_connect_toggled"]
+[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_send_pressed"]

+ 0 - 84
networking/websocket_chat/client/client.gd

@@ -1,84 +0,0 @@
-extends Node
-
-@onready var _log_dest = get_parent().get_node(^"Panel/VBoxContainer/RichTextLabel")
-
-var _client = WebSocketClient.new()
-var _write_mode = WebSocketPeer.WRITE_MODE_BINARY
-var _use_multiplayer = true
-var last_connected_client = 0
-
-func _init():
-	_client.connect(&"connection_established", self._client_connected)
-	_client.connect(&"connection_error", self._client_disconnected)
-	_client.connect(&"connection_closed", self._client_disconnected)
-	_client.connect(&"server_close_request", self._client_close_request)
-	_client.connect(&"data_received", self._client_received)
-
-	_client.connect(&"peer_packet", self._client_received)
-	_client.connect(&"peer_connected", self._peer_connected)
-	_client.connect(&"connection_succeeded", self._client_connected, ["multiplayer_protocol"])
-	_client.connect(&"connection_failed", self._client_disconnected)
-
-
-func _client_close_request(code, reason):
-	Utils._log(_log_dest, "Close code: %d, reason: %s" % [code, reason])
-
-
-func _peer_connected(id):
-	Utils._log(_log_dest, "%s: Client just connected" % id)
-	last_connected_client = id
-
-
-func _exit_tree():
-	_client.disconnect_from_host(1001, "Bye bye!")
-
-
-func _process(_delta):
-	if _client.get_connection_status() == WebSocketClient.CONNECTION_DISCONNECTED:
-		return
-
-	_client.poll()
-
-
-func _client_connected(protocol):
-	Utils._log(_log_dest, "Client just connected with protocol: %s" % protocol)
-	_client.get_peer(1).set_write_mode(_write_mode)
-
-
-func _client_disconnected(clean=true):
-	Utils._log(_log_dest, "Client just disconnected. Was clean: %s" % clean)
-
-
-func _client_received(_p_id = 1):
-	if _use_multiplayer:
-		var peer_id = _client.get_packet_peer()
-		var packet = _client.get_packet()
-		Utils._log(_log_dest, "MPAPI: From %s Data: %s" % [str(peer_id), Utils.decode_data(packet, false)])
-	else:
-		var packet = _client.get_peer(1).get_packet()
-		var is_string = _client.get_peer(1).was_string_packet()
-		Utils._log(_log_dest, "Received data. BINARY: %s: %s" % [not is_string, Utils.decode_data(packet, is_string)])
-
-
-func connect_to_url(host, protocols, multiplayer):
-	_use_multiplayer = multiplayer
-	if _use_multiplayer:
-		_write_mode = WebSocketPeer.WRITE_MODE_BINARY
-	return _client.connect_to_url(host, protocols, multiplayer)
-
-
-func disconnect_from_host():
-	_client.disconnect_from_host(1000, "Bye bye!")
-
-
-func send_data(data, dest):
-	_client.get_peer(1).set_write_mode(_write_mode)
-	if _use_multiplayer:
-		_client.set_target_peer(dest)
-		_client.put_packet(Utils.encode_data(data, _write_mode))
-	else:
-		_client.get_peer(1).put_packet(Utils.encode_data(data, _write_mode))
-
-
-func set_write_mode(mode):
-	_write_mode = mode

+ 0 - 89
networking/websocket_chat/client/client.tscn

@@ -1,89 +0,0 @@
-[gd_scene load_steps=3 format=2]
-
-[ext_resource path="res://client/client_ui.gd" type="Script" id=1]
-[ext_resource path="res://client/client.gd" type="Script" id=2]
-
-[node name="Client" type="Control"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-script = ExtResource( 1 )
-__meta__ = {
-"_edit_use_anchors_": false
-}
-
-[node name="Panel" type="Panel" parent="."]
-anchor_right = 1.0
-anchor_bottom = 1.0
-
-[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-
-[node name="Connect" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_right = 1024.0
-offset_bottom = 24.0
-
-[node name="Host" type="LineEdit" parent="Panel/VBoxContainer/Connect"]
-offset_right = 956.0
-offset_bottom = 24.0
-size_flags_horizontal = 3
-text = "ws://localhost:8000/test/"
-placeholder_text = "ws://my.server/path/"
-
-[node name="Connect" type="Button" parent="Panel/VBoxContainer/Connect"]
-offset_left = 960.0
-offset_right = 1024.0
-offset_bottom = 24.0
-toggle_mode = true
-text = "Connect"
-
-[node name="Settings" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_top = 28.0
-offset_right = 1024.0
-offset_bottom = 52.0
-
-[node name="Mode" type="OptionButton" parent="Panel/VBoxContainer/Settings"]
-offset_right = 29.0
-offset_bottom = 24.0
-
-[node name="Multiplayer" type="CheckBox" parent="Panel/VBoxContainer/Settings"]
-offset_left = 33.0
-offset_right = 159.0
-offset_bottom = 24.0
-pressed = true
-text = "Multiplayer API"
-
-[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/Settings"]
-offset_left = 163.0
-offset_right = 192.0
-offset_bottom = 24.0
-
-[node name="Send" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_top = 56.0
-offset_right = 1024.0
-offset_bottom = 80.0
-
-[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/Send"]
-offset_right = 977.0
-offset_bottom = 24.0
-size_flags_horizontal = 3
-placeholder_text = "Enter some text to send..."
-
-[node name="Send" type="Button" parent="Panel/VBoxContainer/Send"]
-offset_left = 981.0
-offset_right = 1024.0
-offset_bottom = 24.0
-text = "Send"
-
-[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"]
-offset_top = 84.0
-offset_right = 1024.0
-offset_bottom = 600.0
-size_flags_vertical = 3
-
-[node name="Client" type="Node" parent="."]
-script = ExtResource( 2 )
-
-[connection signal="toggled" from="Panel/VBoxContainer/Connect/Connect" to="." method="_on_Connect_toggled"]
-[connection signal="item_selected" from="Panel/VBoxContainer/Settings/Mode" to="." method="_on_Mode_item_selected"]
-[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_Send_pressed"]

+ 0 - 62
networking/websocket_chat/client/client_ui.gd

@@ -1,62 +0,0 @@
-extends Control
-
-@onready var _client = $Client
-@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
-@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit
-@onready var _host = $Panel/VBoxContainer/Connect/Host
-@onready var _multiplayer = $Panel/VBoxContainer/Settings/Multiplayer
-@onready var _write_mode = $Panel/VBoxContainer/Settings/Mode
-@onready var _destination = $Panel/VBoxContainer/Settings/Destination
-
-func _ready():
-	_write_mode.clear()
-	_write_mode.add_item("BINARY")
-	_write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY)
-	_write_mode.add_item("TEXT")
-	_write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT)
-
-	_destination.add_item("Broadcast")
-	_destination.set_item_metadata(0, 0)
-	_destination.add_item("Last connected")
-	_destination.set_item_metadata(1, 1)
-	_destination.add_item("All But last connected")
-	_destination.set_item_metadata(2, -1)
-	_destination.select(0)
-
-
-func _on_Mode_item_selected(_id):
-	_client.set_write_mode(_write_mode.get_selected_metadata())
-
-
-func _on_Send_pressed():
-	if _line_edit.text == "":
-		return
-
-	var dest = _destination.get_selected_metadata()
-	if dest > 0:
-		dest = _client.last_connected_client
-	elif dest < 0:
-		dest = -_client.last_connected_client
-
-	Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest])
-	_client.send_data(_line_edit.text, dest)
-	_line_edit.text = ""
-
-
-func _on_Connect_toggled( pressed ):
-	if pressed:
-		var multiplayer = _multiplayer.pressed
-		if multiplayer:
-			_write_mode.disabled = true
-		else:
-			_destination.disabled = true
-		_multiplayer.disabled = true
-		if _host.text != "":
-			Utils._log(_log_dest, "Connecting to host: %s" % [_host.text])
-			var supported_protocols = PackedStringArray(["my-protocol2", "my-protocol", "binary"])
-			_client.connect_to_url(_host.text, supported_protocols, multiplayer)
-	else:
-		_destination.disabled = false
-		_write_mode.disabled = false
-		_multiplayer.disabled = false
-		_client.disconnect_from_host()

+ 56 - 0
networking/websocket_chat/combo.tscn

@@ -0,0 +1,56 @@
+[gd_scene load_steps=3 format=3 uid="uid://dye16x7udqrxg"]
+
+[ext_resource type="PackedScene" uid="uid://qvg4q16blgx5" path="res://server.tscn" id="1_0srxc"]
+[ext_resource type="PackedScene" uid="uid://ph5ghsflqegf" path="res://client.tscn" id="2_percb"]
+
+[node name="Combo" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 1
+
+[node name="Box" type="HBoxContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Server" parent="Box" instance=ExtResource("1_0srxc")]
+anchors_preset = 0
+anchor_right = 0.0
+anchor_bottom = 0.0
+offset_right = 574.0
+offset_bottom = 648.0
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Box"]
+offset_left = 578.0
+offset_right = 1152.0
+offset_bottom = 648.0
+size_flags_horizontal = 3
+
+[node name="Client" parent="Box/VBoxContainer" instance=ExtResource("2_percb")]
+anchors_preset = 0
+anchor_right = 0.0
+anchor_bottom = 0.0
+offset_right = 574.0
+offset_bottom = 213.0
+
+[node name="Client2" parent="Box/VBoxContainer" instance=ExtResource("2_percb")]
+anchors_preset = 0
+anchor_right = 0.0
+anchor_bottom = 0.0
+offset_top = 217.0
+offset_right = 574.0
+offset_bottom = 430.0
+
+[node name="Client3" parent="Box/VBoxContainer" instance=ExtResource("2_percb")]
+anchors_preset = 0
+anchor_right = 0.0
+anchor_bottom = 0.0
+offset_top = 434.0
+offset_right = 574.0
+offset_bottom = 648.0

+ 0 - 53
networking/websocket_chat/combo/combo.tscn

@@ -1,53 +0,0 @@
-[gd_scene load_steps=3 format=2]
-
-[ext_resource path="res://server/server.tscn" type="PackedScene" id=1]
-[ext_resource path="res://client/client.tscn" type="PackedScene" id=2]
-
-[node name="Combo" type="Control"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-mouse_filter = 1
-
-[node name="Box" type="HBoxContainer" parent="."]
-anchor_right = 1.0
-anchor_bottom = 1.0
-custom_constants/separation = 20
-
-[node name="ServerControl" parent="Box" instance=ExtResource( 1 )]
-anchor_right = 0.0
-anchor_bottom = 0.0
-offset_right = 502.0
-offset_bottom = 600.0
-size_flags_horizontal = 3
-
-[node name="VBoxContainer" type="VBoxContainer" parent="Box"]
-offset_left = 522.0
-offset_right = 1024.0
-offset_bottom = 600.0
-size_flags_horizontal = 3
-
-[node name="Client" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
-anchor_right = 0.0
-anchor_bottom = 0.0
-offset_right = 502.0
-offset_bottom = 197.0
-size_flags_horizontal = 3
-size_flags_vertical = 3
-
-[node name="Client2" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
-anchor_right = 0.0
-anchor_bottom = 0.0
-offset_top = 201.0
-offset_right = 502.0
-offset_bottom = 398.0
-size_flags_horizontal = 3
-size_flags_vertical = 3
-
-[node name="Client3" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
-anchor_right = 0.0
-anchor_bottom = 0.0
-offset_top = 402.0
-offset_right = 502.0
-offset_bottom = 600.0
-size_flags_horizontal = 3
-size_flags_vertical = 3

+ 14 - 15
networking/websocket_chat/icon.png.import

@@ -1,8 +1,9 @@
 [remap]
 
 importer="texture"
-type="StreamTexture2D"
-path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+type="CompressedTexture2D"
+uid="uid://db0a1leye11ap"
+path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
 metadata={
 "vram_texture": false
 }
@@ -10,26 +11,24 @@ metadata={
 [deps]
 
 source_file="res://icon.png"
-dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"]
+dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
 
 [params]
 
 compress/mode=0
 compress/lossy_quality=0.7
-compress/hdr_mode=0
+compress/hdr_compression=1
 compress/bptc_ldr=0
 compress/normal_map=0
-flags/repeat=0
-flags/filter=true
-flags/mipmaps=false
-flags/anisotropic=false
-flags/srgb=2
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
 process/fix_alpha_border=true
 process/premult_alpha=false
-process/HDR_as_SRGB=false
-process/invert_color=false
 process/normal_map_invert_y=false
-stream=false
-size_limit=0
-detect_3d=true
-svg/scale=1.0
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1

+ 19 - 6
networking/websocket_chat/project.godot

@@ -6,19 +6,32 @@
 ;   [section] ; section goes between []
 ;   param=value ; assign values to parameters
 
-config_version=4
+config_version=5
+
+_global_script_classes=[{
+"base": "Node",
+"class": &"WebSocketClient",
+"language": &"GDScript",
+"path": "res://websocket/WebSocketClient.gd"
+}, {
+"base": "Node",
+"class": &"WebSocketServer",
+"language": &"GDScript",
+"path": "res://websocket/WebSocketServer.gd"
+}]
+_global_script_class_icons={
+"WebSocketClient": "",
+"WebSocketServer": ""
+}
 
 [application]
 
 config/name="WebSocket Chat Demo"
 config/description="This is a demo of a simple chat implemented using WebSockets, showing both how to host a websocket server from Godot and how to connect to it."
-run/main_scene="res://combo/combo.tscn"
+run/main_scene="res://combo.tscn"
+config/features=PackedStringArray("4.0")
 config/icon="res://icon.png"
 
-[autoload]
-
-Utils="*res://utils.gd"
-
 [gdnative]
 
 singletons=[]

+ 51 - 0
networking/websocket_chat/server.gd

@@ -0,0 +1,51 @@
+extends Control
+
+@onready var _server : WebSocketServer = $WebSocketServer
+@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
+@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit
+@onready var _listen_port = $Panel/VBoxContainer/Connect/Port
+
+func info(msg):
+	print(msg)
+	_log_dest.add_text(str(msg) + "\n")
+
+
+# Server signals
+func _on_web_socket_server_client_connected(peer_id):
+	var peer : WebSocketPeer = _server.peers[peer_id]
+	info("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()])
+	_server.send(-peer_id, "[%d] connected" % peer_id)
+
+
+func _on_web_socket_server_client_disconnected(peer_id):
+	var peer : WebSocketPeer = _server.peers[peer_id]
+	info("Remote client disconnected: %d. Code: %d, Reason: %s" % [peer_id, peer.get_close_code(), peer.get_close_reason()])
+	_server.send(-peer_id, "[%d] disconnected" % peer_id)
+
+
+func _on_web_socket_server_message_received(peer_id, message):
+	info("Server received data from peer %d: %s" % [peer_id, message])
+	_server.send(-peer_id, "[%d] Says: %s" % [peer_id, message])
+
+
+# UI signals.
+func _on_send_pressed():
+	if _line_edit.text == "":
+		return
+
+	info("Sending message: %s" % [_line_edit.text])
+	_server.send(0, "Server says: %s" % _line_edit.text)
+	_line_edit.text = ""
+
+
+func _on_listen_toggled(pressed):
+	if not pressed:
+		_server.stop()
+		info("Server stopped")
+		return
+	var port = int(_listen_port.value)
+	var err = _server.listen(port)
+	if err != OK:
+		info("Error listing on port %s" % port)
+		return
+	info("Listing on port %s, supported protocols: %s" % [port, _server.supported_protocols])

+ 61 - 0
networking/websocket_chat/server.tscn

@@ -0,0 +1,61 @@
+[gd_scene load_steps=4 format=3 uid="uid://qvg4q16blgx5"]
+
+[ext_resource type="PackedScene" uid="uid://cyvrywci15kev" path="res://chat.tscn" id="1_i673i"]
+[ext_resource type="Script" path="res://server.gd" id="1_urpfw"]
+[ext_resource type="Script" path="res://websocket/WebSocketServer.gd" id="3_0eqsy"]
+
+[node name="Server" instance=ExtResource("1_i673i")]
+script = ExtResource("1_urpfw")
+
+[node name="WebSocketServer" type="Node" parent="." index="0"]
+script = ExtResource("3_0eqsy")
+supported_protocols = PackedStringArray("demo-chat")
+
+[node name="Panel" parent="." index="1"]
+layout_mode = 3
+
+[node name="VBoxContainer" parent="Panel" index="0"]
+layout_mode = 3
+
+[node name="Listen" parent="Panel/VBoxContainer" index="0"]
+layout_mode = 3
+
+[node name="Connect" parent="Panel/VBoxContainer" index="1"]
+layout_mode = 3
+
+[node name="Host" parent="Panel/VBoxContainer/Connect" index="0"]
+visible = false
+layout_mode = 3
+offset_right = 1006.0
+
+[node name="Connect" parent="Panel/VBoxContainer/Connect" index="1"]
+visible = false
+layout_mode = 3
+
+[node name="Port" parent="Panel/VBoxContainer/Connect" index="2"]
+layout_mode = 3
+offset_left = 0.0
+offset_right = 83.0
+
+[node name="Listen" parent="Panel/VBoxContainer/Connect" index="3"]
+layout_mode = 3
+offset_left = 87.0
+offset_right = 142.0
+
+[node name="Send" parent="Panel/VBoxContainer" index="2"]
+layout_mode = 3
+
+[node name="LineEdit" parent="Panel/VBoxContainer/Send" index="0"]
+layout_mode = 3
+
+[node name="Send" parent="Panel/VBoxContainer/Send" index="1"]
+layout_mode = 3
+
+[node name="RichTextLabel" parent="Panel/VBoxContainer" index="3"]
+layout_mode = 3
+
+[connection signal="client_connected" from="WebSocketServer" to="." method="_on_web_socket_server_client_connected"]
+[connection signal="client_disconnected" from="WebSocketServer" to="." method="_on_web_socket_server_client_disconnected"]
+[connection signal="message_received" from="WebSocketServer" to="." method="_on_web_socket_server_message_received"]
+[connection signal="toggled" from="Panel/VBoxContainer/Connect/Listen" to="." method="_on_listen_toggled"]
+[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_send_pressed"]

+ 0 - 84
networking/websocket_chat/server/server.gd

@@ -1,84 +0,0 @@
-extends Node
-
-@onready var _log_dest = get_parent().get_node(^"Panel/VBoxContainer/RichTextLabel")
-
-var _server = WebSocketServer.new()
-var _clients = {}
-var _write_mode = WebSocketPeer.WRITE_MODE_BINARY
-var _use_multiplayer = true
-var last_connected_client = 0
-
-func _init():
-	_server.connect(&"client_connected", self._client_connected)
-	_server.connect(&"client_disconnected", self._client_disconnected)
-	_server.connect(&"client_close_request", self._client_close_request)
-	_server.connect(&"data_received", self._client_receive)
-
-	_server.connect(&"peer_packet", self._client_receive)
-	_server.connect(&"peer_connected", self._client_connected, ["multiplayer_protocol"])
-	_server.connect(&"peer_disconnected", self._client_disconnected)
-
-
-func _exit_tree():
-	_clients.clear()
-	_server.stop()
-
-
-func _process(_delta):
-	if _server.is_listening():
-		_server.poll()
-
-
-func _client_close_request(id, code, reason):
-	print(reason == "Bye bye!")
-	Utils._log(_log_dest, "Client %s close code: %d, reason: %s" % [id, code, reason])
-
-
-func _client_connected(id, protocol):
-	_clients[id] = _server.get_peer(id)
-	_clients[id].set_write_mode(_write_mode)
-	last_connected_client = id
-	Utils._log(_log_dest, "%s: Client connected with protocol %s" % [id, protocol])
-
-
-func _client_disconnected(id, clean = true):
-	Utils._log(_log_dest, "Client %s disconnected. Was clean: %s" % [id, clean])
-	if _clients.has(id):
-		_clients.erase(id)
-
-
-func _client_receive(id):
-	if _use_multiplayer:
-		var peer_id = _server.get_packet_peer()
-		var packet = _server.get_packet()
-		Utils._log(_log_dest, "MPAPI: From %s data: %s" % [peer_id, Utils.decode_data(packet, false)])
-	else:
-		var packet = _server.get_peer(id).get_packet()
-		var is_string = _server.get_peer(id).was_string_packet()
-		Utils._log(_log_dest, "Data from %s BINARY: %s: %s" % [id, not is_string, Utils.decode_data(packet, is_string)])
-
-
-func send_data(data, dest):
-	if _use_multiplayer:
-		_server.set_target_peer(dest)
-		_server.put_packet(Utils.encode_data(data, _write_mode))
-	else:
-		for id in _clients:
-			_server.get_peer(id).put_packet(Utils.encode_data(data, _write_mode))
-
-
-func listen(port, supported_protocols, multiplayer):
-	_use_multiplayer = multiplayer
-	if _use_multiplayer:
-		set_write_mode(WebSocketPeer.WRITE_MODE_BINARY)
-	return _server.listen(port, supported_protocols, multiplayer)
-
-
-func stop():
-	_server.stop()
-
-
-func set_write_mode(mode):
-	_write_mode = mode
-	for c in _clients:
-		_clients[c].set_write_mode(_write_mode)

+ 0 - 88
networking/websocket_chat/server/server.tscn

@@ -1,88 +0,0 @@
-[gd_scene load_steps=3 format=2]
-
-[ext_resource path="res://server/server_ui.gd" type="Script" id=1]
-[ext_resource path="res://server/server.gd" type="Script" id=2]
-
-[node name="ServerControl" type="Control"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-script = ExtResource( 1 )
-__meta__ = {
-"_edit_use_anchors_": false
-}
-
-[node name="Server" type="Node" parent="."]
-script = ExtResource( 2 )
-
-[node name="Panel" type="Panel" parent="."]
-anchor_right = 1.0
-anchor_bottom = 1.0
-
-[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-
-[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_right = 1024.0
-offset_bottom = 24.0
-
-[node name="Port" type="SpinBox" parent="Panel/VBoxContainer/HBoxContainer"]
-offset_right = 74.0
-offset_bottom = 24.0
-min_value = 1.0
-max_value = 65535.0
-value = 8000.0
-
-[node name="Listen" type="Button" parent="Panel/VBoxContainer/HBoxContainer"]
-offset_left = 78.0
-offset_right = 129.0
-offset_bottom = 24.0
-toggle_mode = true
-text = "Listen"
-
-[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_top = 28.0
-offset_right = 1024.0
-offset_bottom = 52.0
-
-[node name="WriteMode" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"]
-offset_right = 29.0
-offset_bottom = 24.0
-
-[node name="MPAPI" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer2"]
-offset_left = 33.0
-offset_right = 159.0
-offset_bottom = 24.0
-pressed = true
-text = "Multiplayer API"
-
-[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"]
-offset_left = 163.0
-offset_right = 192.0
-offset_bottom = 24.0
-
-[node name="HBoxContainer3" type="HBoxContainer" parent="Panel/VBoxContainer"]
-offset_top = 56.0
-offset_right = 1024.0
-offset_bottom = 80.0
-
-[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer3"]
-offset_right = 977.0
-offset_bottom = 24.0
-size_flags_horizontal = 3
-
-[node name="Send" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"]
-offset_left = 981.0
-offset_right = 1024.0
-offset_bottom = 24.0
-text = "Send"
-
-[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"]
-offset_top = 84.0
-offset_right = 1024.0
-offset_bottom = 600.0
-size_flags_vertical = 3
-
-[connection signal="toggled" from="Panel/VBoxContainer/HBoxContainer/Listen" to="." method="_on_Listen_toggled"]
-[connection signal="item_selected" from="Panel/VBoxContainer/HBoxContainer2/WriteMode" to="." method="_on_WriteMode_item_selected"]
-[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/Send" to="." method="_on_Send_pressed"]

+ 0 - 70
networking/websocket_chat/server/server_ui.gd

@@ -1,70 +0,0 @@
-extends Control
-
-@onready var _server = $Server
-@onready var _port = $Panel/VBoxContainer/HBoxContainer/Port
-@onready var _line_edit = $Panel/VBoxContainer/HBoxContainer3/LineEdit
-@onready var _write_mode = $Panel/VBoxContainer/HBoxContainer2/WriteMode
-@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
-@onready var _multiplayer = $Panel/VBoxContainer/HBoxContainer2/MPAPI
-@onready var _destination = $Panel/VBoxContainer/HBoxContainer2/Destination
-
-func _ready():
-	_write_mode.clear()
-	_write_mode.add_item("BINARY")
-	_write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY)
-	_write_mode.add_item("TEXT")
-	_write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT)
-	_write_mode.select(0)
-
-	_destination.add_item("Broadcast")
-	_destination.set_item_metadata(0, 0)
-	_destination.add_item("Last connected")
-	_destination.set_item_metadata(1, 1)
-	_destination.add_item("All But last connected")
-	_destination.set_item_metadata(2, -1)
-	_destination.select(0)
-
-
-func _on_Listen_toggled(pressed):
-	if pressed:
-		var use_multiplayer = _multiplayer.pressed
-		_multiplayer.disabled = true
-		var supported_protocols = PackedStringArray(["my-protocol", "binary"])
-		var port = int(_port.value)
-		if use_multiplayer:
-			_write_mode.disabled = true
-			_write_mode.select(0)
-		else:
-			_destination.disabled = true
-			_destination.select(0)
-		if _server.listen(port, supported_protocols, use_multiplayer) == OK:
-			Utils._log(_log_dest, "Listing on port %s" % port)
-			if not use_multiplayer:
-				Utils._log(_log_dest, "Supported protocols: %s" % supported_protocols)
-		else:
-			Utils._log(_log_dest, "Error listening on port %s" % port)
-	else:
-		_server.stop()
-		_multiplayer.disabled = false
-		_write_mode.disabled = false
-		_destination.disabled = false
-		Utils._log(_log_dest, "Server stopped")
-
-
-func _on_Send_pressed():
-	if _line_edit.text == "":
-		return
-
-	var dest = _destination.get_selected_metadata()
-	if dest > 0:
-		dest = _server.last_connected_client
-	elif dest < 0:
-		dest = -_server.last_connected_client
-
-	Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest])
-	_server.send_data(_line_edit.text, dest)
-	_line_edit.text = ""
-
-
-func _on_WriteMode_item_selected(_id):
-	_server.set_write_mode(_write_mode.get_selected_metadata())

+ 0 - 17
networking/websocket_chat/utils.gd

@@ -1,17 +0,0 @@
-extends Node
-
-func encode_data(data, mode):
-	if mode == WebSocketPeer.WRITE_MODE_TEXT:
-		return data.to_utf8()
-	return var2bytes(data)
-
-
-func decode_data(data, is_string):
-	if is_string:
-		return data.get_string_from_utf8()
-	return bytes2var(data)
-
-
-func _log(node, msg):
-	print(msg)
-	node.add_text(str(msg) + "\n")

+ 73 - 0
networking/websocket_chat/websocket/WebSocketClient.gd

@@ -0,0 +1,73 @@
+extends Node
+class_name WebSocketClient
+
+@export var handshake_headers : PackedStringArray
+@export var supported_protocols : PackedStringArray
+@export var tls_trusted_certificate : X509Certificate
+@export var tls_verify := true
+
+
+var socket = WebSocketPeer.new()
+var last_state = WebSocketPeer.STATE_CLOSED
+
+
+signal connected_to_server()
+signal connection_closed()
+signal message_received(message: Variant)
+
+
+func connect_to_url(url) -> int:
+	socket.supported_protocols = supported_protocols
+	socket.handshake_headers = handshake_headers
+	var err = socket.connect_to_url(url, tls_verify, tls_trusted_certificate)
+	if err != OK:
+		return err
+	last_state = socket.get_ready_state()
+	return OK
+
+
+func send(message) -> int:
+	if typeof(message) == TYPE_STRING:
+		return socket.send_text(message)
+	return socket.send(var_to_bytes(message))
+
+
+func get_message() -> Variant:
+	if socket.get_available_packet_count() < 1:
+		return null
+	var pkt = socket.get_packet()
+	if socket.was_string_packet():
+		return pkt.get_string_from_utf8()
+	return bytes_to_var(pkt)
+
+
+func close(code := 1000, reason := "") -> void:
+	socket.close(code, reason)
+	last_state = socket.get_ready_state()
+
+
+func clear() -> void:
+	socket = WebSocketPeer.new()
+	last_state = socket.get_ready_state()
+
+
+func get_socket() -> WebSocketPeer:
+	return socket
+
+
+func poll() -> void:
+	if socket.get_ready_state() != socket.STATE_CLOSED:
+		socket.poll()
+	var state = socket.get_ready_state()
+	if last_state != state:
+		last_state = state
+		if state == socket.STATE_OPEN:
+			connected_to_server.emit()
+		elif state == socket.STATE_CLOSED:
+			connection_closed.emit()
+	while socket.get_ready_state() == socket.STATE_OPEN and socket.get_available_packet_count():
+		message_received.emit(get_message())
+
+
+func _process(delta):
+	poll()

+ 162 - 0
networking/websocket_chat/websocket/WebSocketServer.gd

@@ -0,0 +1,162 @@
+extends Node
+class_name WebSocketServer
+
+signal message_received(peer_id : int, message)
+signal client_connected(peer_id : int)
+signal client_disconnected(peer_id : int)
+
+@export var handshake_headers := PackedStringArray()
+@export var supported_protocols : PackedStringArray
+@export var handshake_timout := 3000
+@export var use_tls := false
+@export var tls_cert : X509Certificate
+@export var tls_key : CryptoKey
+@export var refuse_new_connections := false :
+	set(refuse):
+		if refuse:
+			pending_peers.clear()
+
+
+class PendingPeer:
+	var connect_time : int
+	var tcp : StreamPeerTCP
+	var connection : StreamPeer
+	var ws : WebSocketPeer
+
+	func _init(p_tcp: StreamPeerTCP):
+		tcp = p_tcp
+		connection = p_tcp
+		connect_time = Time.get_ticks_msec()
+
+
+var tcp_server := TCPServer.new()
+var pending_peers : Array[PendingPeer] = []
+var peers : Dictionary
+
+
+func listen(port : int) -> int:
+	assert(not tcp_server.is_listening())
+	return tcp_server.listen(port)
+
+
+func stop():
+	tcp_server.stop()
+	pending_peers.clear()
+	peers.clear()
+
+
+func send(peer_id, message) -> int:
+	var type = typeof(message)
+	if peer_id <= 0:
+		# Send to multiple peers, (zero = brodcast, negative = exclude one)
+		for id in peers:
+			if id == -peer_id:
+				continue
+			if type == TYPE_STRING:
+				peers[id].send_text(message)
+			else:
+				peers[id].put_packet(message)
+		return OK
+
+	assert(peers.has(peer_id))
+	var socket = peers[peer_id]
+	if type == TYPE_STRING:
+		return socket.send_text(message)
+	return socket.send(var_to_bytes(message))
+
+
+func get_message(peer_id) -> Variant:
+	assert(peers.has(peer_id))
+	var socket = peers[peer_id]
+	if socket.get_available_packet_count() < 1:
+		return null
+	var pkt = socket.get_packet()
+	if socket.was_string_packet():
+		return pkt.get_string_from_utf8()
+	return bytes_to_var(pkt)
+
+
+func has_message(peer_id) -> bool:
+	assert(peers.has(peer_id))
+	return peers[peer_id].get_available_packet_count() > 0
+
+
+func _create_peer() -> WebSocketPeer:
+	var ws = WebSocketPeer.new()
+	ws.supported_protocols = supported_protocols
+	ws.handshake_headers = handshake_headers
+	return ws
+
+
+func poll() -> void:
+	if not tcp_server.is_listening():
+		return
+	while not refuse_new_connections and tcp_server.is_connection_available():
+		var conn = tcp_server.take_connection()
+		assert(conn != null)
+		pending_peers.append(PendingPeer.new(conn))
+	var to_remove := []
+	for p in pending_peers:
+		if not _connect_pending(p):
+			if p.connect_time + handshake_timout < Time.get_ticks_msec():
+				# Timeout
+				to_remove.append(p)
+			continue # Still pending
+		to_remove.append(p)
+	for r in to_remove:
+		pending_peers.erase(r)
+	to_remove.clear()
+	for id in peers:
+		var p : WebSocketPeer = peers[id]
+		var packets = p.get_available_packet_count()
+		p.poll()
+		if p.get_ready_state() != WebSocketPeer.STATE_OPEN:
+			client_disconnected.emit(id)
+			to_remove.append(id)
+			continue
+		while p.get_available_packet_count():
+			message_received.emit(id, get_message(id))
+	for r in to_remove:
+		peers.erase(r)
+	to_remove.clear()
+
+
+func _connect_pending(p: PendingPeer) -> bool:
+	if p.ws != null:
+		# Poll websocket client if doing handshake
+		p.ws.poll()
+		var state = p.ws.get_ready_state()
+		if state == WebSocketPeer.STATE_OPEN:
+			var id = randi_range(2, 1 << 30)
+			peers[id] = p.ws
+			client_connected.emit(id)
+			return true # Success.
+		elif state != WebSocketPeer.STATE_CONNECTING:
+			return true # Failure.
+		return false # Still connecting.
+	elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED:
+		return true # TCP disconnected.
+	elif not use_tls:
+		# TCP is ready, create WS peer
+		p.ws = _create_peer()
+		p.ws.accept_stream(p.tcp)
+		return false # WebSocketPeer connection is pending.
+	else:
+		if p.connection == p.tcp:
+			assert(tls_key != null and tls_cert != null)
+			var tls = StreamPeerTLS.new()
+			tls.accept_stream(p.tcp, tls_key, tls_cert)
+			p.connection = tls
+		p.connection.poll()
+		var status = p.connection.get_status()
+		if status == StreamPeerTLS.STATUS_CONNECTED:
+			p.ws = _create_peer()
+			p.ws.accept_stream(p.connection)
+			return false # WebSocketPeer connection is pending.
+		if status != StreamPeerTLS.STATUS_HANDSHAKING:
+			return true # Failure.
+		return false
+
+
+func _process(delta):
+	poll()