Skip to content

Instantly share code, notes, and snippets.

@krshock
Last active April 2, 2024 11:51
Show Gist options
  • Save krshock/046c7ddb15e8100f8bbf78bf3d8e8a45 to your computer and use it in GitHub Desktop.
Save krshock/046c7ddb15e8100f8bbf78bf3d8e8a45 to your computer and use it in GitHub Desktop.
NetSingleton.gd
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