Last active
April 2, 2024 11:51
-
-
Save krshock/046c7ddb15e8100f8bbf78bf3d8e8a45 to your computer and use it in GitHub Desktop.
NetSingleton.gd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Node | |
## Networking TCP client/server architecture for godot 4 (WIP) | |
## | |
## Includes TYPE_VARIANT serialization using encode_var/decode_var for network serialization | |
@onready var sock_server : TCPServer = TCPServer.new() | |
@onready var sock_client : Socket = Socket.new() | |
var host : String = "localhost" | |
var serve_port: int = 7777 | |
var is_server : bool = false | |
var is_open : bool = false | |
var is_local : bool = true | |
var pf = randi_range(100,999) | |
var _id = 0 | |
signal new_client(cli:Socket) | |
#region Node functions | |
func _ready(): | |
is_local = true | |
sock_client.stream = StreamPeerTCP.new() | |
sock_client.packet = PacketPeerStream.new() | |
sock_client.packet.stream_peer=sock_client.stream | |
sock_client.type = 1 | |
clients = [] | |
clients.resize(MAX_CLIENTS) | |
start_server() | |
if !is_open: | |
start_client() | |
Session.initializing.connect(func(): | |
Session.entity_register(self) | |
) | |
func _process(delta): | |
if is_open: | |
if is_server: | |
_server_tcp_poll() | |
else: | |
_client_poll() | |
#endregion | |
#region Utils | |
func build_packet(entity_id:int, f:int, data:PackedByteArray): | |
var p = PackedByteArray() | |
p.append_array([0,0,0,0,0,0]) | |
p.encode_u16(0,1) | |
p.encode_u16(2,entity_id) | |
p.encode_u16(4,f) | |
p.append_array(data) | |
return p | |
func encode_var(v): | |
var p = PackedByteArray() | |
p.resize(1000) | |
var size = p.encode_var(0,v) | |
assert(size<=p.size(), "packet overflow") | |
return p.slice(0,size) | |
class Socket: | |
var id:int | |
var stream:StreamPeerTCP | |
var packet:PacketPeerStream | |
var _state : SockState | |
var _player : int | |
var _spawn_id : int = 0 #spawn_id of the selected character | |
var type = 0 #0 server, 1 client | |
func poll(): | |
if stream: | |
self.stream.poll() | |
enum SockState { | |
NONE, | |
STEP1, | |
STEP2, | |
READY, | |
} | |
#endregion | |
#region Server Section | |
var clients : Array[Socket]=[] | |
const MAX_CLIENTS = 16 | |
const MIN_CLIENT_ID = 1 | |
func start_server(): | |
is_local = true | |
stop_server() | |
stop_client() | |
if sock_server.listen(7777)==OK: | |
is_open=true | |
is_server=true | |
print(pf, " Server Listen OK") | |
func stop_server(): | |
sock_server.stop() | |
for idx in range(MAX_CLIENTS): | |
if clients[idx]==null: continue | |
clients[idx].stream.disconnect_from_host() | |
clients[idx] = null | |
is_server=false | |
is_open=false | |
func _server_send_cmd(client:Socket, eid:int, cmdid:int, value) -> bool: | |
if client==null or !is_open or !is_server: return false | |
var msg = build_packet(eid, cmdid, encode_var(value)) | |
if client.stream.get_status()==client.stream.STATUS_CONNECTED: | |
client.packet.put_packet(msg) | |
return true | |
else: | |
pass | |
return false | |
func server_broadcast_text(text:String): | |
if !is_open or !is_server: return | |
assert(text!=null and text.length()>0) | |
var msg = text.to_utf8_buffer() | |
for c in clients: | |
if c==null: continue | |
if c.stream.get_status()==c.stream.STATUS_CONNECTED: | |
c.packet.put_packet(msg) | |
else: | |
pass | |
func server_broadcast(entity_id:int, fid:int, v): | |
if !is_open or !is_server: return | |
assert(v!=null) | |
var msg = build_packet(entity_id, fid, encode_var(v)) | |
for c in clients: | |
if c==null: continue | |
if c.stream.get_status()==c.stream.STATUS_CONNECTED: | |
c.packet.put_packet(msg) | |
else: | |
_server_close_client(c) | |
print(pf, " server: broadcasting to invalid connection (id=%d, status=%d)" % [c.id , c.stream.get_status()]) | |
func _server_get_client_id(): | |
for idx in range(1,MAX_CLIENTS): | |
if clients[idx]==null: | |
return idx | |
assert(false, " server: no client slots found") | |
return -1 | |
func _server_tcp_poll(): | |
while sock_server.is_connection_available(): | |
var conn : Socket = Socket.new() | |
var s = sock_server.take_connection() | |
conn.stream = s | |
conn.packet = PacketPeerStream.new() | |
conn.packet.stream_peer = s | |
clients.append(conn) | |
conn.packet.put_packet("0MOB32".to_utf8_buffer()) | |
for conn in clients: | |
if conn==null: continue | |
conn.poll() | |
if conn.stream.get_status()==StreamPeerTCP.STATUS_CONNECTED: | |
while conn.packet.get_available_packet_count()>0: | |
conn.poll() | |
var packet = conn.packet.get_packet() | |
if packet.size()>1 and packet[0]==48: | |
_server_cmd_decode(conn,packet) | |
if packet.decode_u16(0)==1: | |
var idx = packet.decode_u16(2) | |
if idx>=Session.ENTITY_ARRAY_SIZE: | |
conn.poll() | |
continue | |
var ent = Session.entities[idx] | |
if idx>=Session.ENTITY_ARRAY_SIZE or ent==null or !is_instance_valid(ent): | |
conn.poll() | |
continue | |
if !ent.has_method("_decode_cmd"): | |
conn.poll() | |
continue | |
ent._decode_cmd(packet.decode_u16(4), packet.slice(6)) | |
conn.poll() | |
elif [StreamPeerTCP.STATUS_NONE,StreamPeerTCP.STATUS_ERROR].has(conn.stream.get_status()): | |
_server_close_client(conn) | |
func _server_cmd_decode(sock:Socket, msg:PackedByteArray) -> bool: | |
if !is_open or !is_server: | |
return false | |
var text=msg.get_string_from_utf8() | |
print(pf, " msg: ", text) | |
if text.begins_with("0SET_READY") and sock._state==SockState.STEP1: | |
sock._state = SockState.READY | |
Session._sync_session(sock) | |
new_client.emit(sock) | |
return true | |
if text.begins_with("0STEP1") and sock._state==SockState.NONE: | |
sock._state=SockState.STEP1 | |
print(pf, "server received json: ",text.substr("0STEP1".length())) | |
var json = JSON.parse_string(text.substr("0STEP1".length())) | |
var pl : T.Player = T.Player.new() | |
pl.display_name = json["name"] | |
pl.local = false | |
if Session.add_player(pl): | |
var _htmsg="0STEP2"+JSON.stringify({"id":pl.id,"name":pl.display_name}) | |
sock._player=pl.id | |
sock.packet.put_packet(_htmsg.to_utf8_buffer()) | |
else: | |
print(pf, " Net.step1: clossing client") | |
sock.packet.stream_peer = null | |
sock.stream.disconnect_from_host() | |
clients[sock.id] = null | |
return true | |
return false | |
#endregion | |
#region Client Code | |
func start_client(): | |
is_local = false | |
stop_client() | |
stop_server() | |
is_server=false | |
is_open=false | |
if sock_client.stream.connect_to_host(host,serve_port)==OK: | |
print(pf, " Server Client OK") | |
is_open=true | |
func stop_client(): | |
is_server=false | |
is_open=false | |
sock_client.stream.disconnect_from_host() | |
func _server_close_client(sock:Socket): | |
print("==",clients[sock.id]) | |
print("== closing client: %s (id=%d)" %[sock.stream.get_connected_host(),sock.id]) | |
sock.stream.disconnect_from_host() | |
Session.remove_player_by_id(sock._player) | |
clients[sock.id]= null | |
sock.id = -1 | |
print("==",clients[sock.id]) | |
func _client_poll(): | |
sock_client.poll() | |
if sock_client.stream.get_status()==StreamPeerTCP.STATUS_CONNECTED: | |
while sock_client.packet.get_available_packet_count()>0: | |
sock_client.poll() | |
var packet = sock_client.packet.get_packet() | |
if packet.size()>1 and packet[0]==48: | |
_client_cmd_decode(packet) | |
if packet.decode_u16(0)==1: | |
var idx = packet.decode_u16(2) | |
if idx>=Session.ENTITY_ARRAY_SIZE: | |
sock_client.poll() | |
continue | |
var ent = Session.entities[idx] | |
if idx>=Session.ENTITY_ARRAY_SIZE or ent==null or !is_instance_valid(ent): | |
sock_client.poll() | |
continue | |
if !ent.has_method("_decode_cmd"): | |
sock_client.poll() | |
continue | |
ent._decode_cmd(packet.decode_u16(4), packet.slice(6)) | |
sock_client.poll() | |
elif sock_client.stream.get_status()==StreamPeerTCP.STATUS_NONE: | |
print(pf, " client disconnected") | |
is_open=false | |
elif sock_client.stream.get_status()==StreamPeerTCP.STATUS_ERROR: | |
print(pf, " client error") | |
is_open=false | |
func client_send(entid:int, cmdid:int, v): | |
if !is_open or is_server: | |
return | |
if sock_client.stream.get_status()!=sock_client.stream.STATUS_CONNECTED: | |
return | |
sock_client.packet.put_packet(build_packet(entid,cmdid,encode_var(v))) | |
func client_send_text(text:String): | |
assert(text!=null) | |
if !is_open or is_server: | |
return | |
sock_client.packet.put_packet(text.to_utf8_buffer()) | |
func _client_cmd_decode(packet:PackedByteArray): | |
var st = packet.get_string_from_utf8() | |
print(pf, " msg: ", st) | |
if sock_client._state==SockState.STEP1: | |
if st.begins_with("0STEP2"): | |
var json=JSON.parse_string(st.substr("0STEP2".length())) | |
sock_client._state=SockState.READY | |
sock_client._player = int(json["id"]) | |
client_send_text("0SET_READY") | |
Session._restart_session() | |
elif sock_client._state==SockState.NONE: | |
if st=="0MOB32": | |
client_send_text("0STEP1"+JSON.stringify({"name":Session.current_player.display_name, "time": Time.get_unix_time_from_system()})) | |
sock_client._state=SockState.STEP1 | |
#var pl = Session.add_player(json["name"],json["id"]) | |
#endregion | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment