purpose: making a online user count realtime display using the stream method in godot. The reason I don’t use normal nk.stream_user_leave method is it cannot catch player force shut down that the stream signal will not be sent and I cannot update the number upon they leaving
according to document (Streams - Heroic Labs Documentation)
where " Receiving stream presence events
When a new presence joins a stream or an existing presence leaves the server will broadcast presence events to all users currently on the stream."
The socket.received_stream_presence signal should be sent to every use every time a user leave or join the server.
However from the docker log:
{“level”:“debug”,“ts”:“2025-05-19T13:46:26.816Z”,“caller”:“server/tracker.go:912”,“msg”:“Processing presence event”,“joins”:0,“leaves”:1}
the handle of presence event is presence.
however in my godot, the socket.received_stream_presence signal was never sent.
here is my lua code:
local nk = require("nakama")
local stream_id = { mode = 2, label = "Global Chat Room" }
local function join(context, _)
local hidden = false
local persistence = false
-- Debugging: Log session ID
nk.logger_info("User ID: " .. context.user_id)
nk.logger_info("Session ID: " .. tostring(context.session_id))
-- Ensure session_id is valid before joining stream
nk.stream_user_join(context.user_id, context.session_id, stream_id)
user_count_update()
end
nk.register_rpc(join, "join")
local function leave(context, _)
nk.logger_info("User Leaving: " .. context.user_id)
-- Remove the user from the stream
nk.stream_user_leave(context.user_id, context.session_id, stream_id)
user_count_update()
end
nk.register_rpc(leave, "leave")
function user_count_update()
local payload = nk.json_encode({ event_type=1,context=nk.stream_count(stream_id)})
nk.stream_send(stream_id, payload)
end
nk.register_rpc(user_count_update,"count")
my gdscript
extends Control
var server_key: String = "defaultkey"
var host: String = "127.0.0.1"
var port: int = 7350
var scheme: String = "http"
var timeout : int = 10
var client : NakamaClient
var socket : NakamaSocket
var device_id = OS.get_unique_id()
var session:NakamaSession
var user_id
signal session_changed (nakama_session)
var onLogInPage:bool = 1
var onSignInPage:bool = 0
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
client = Nakama.create_client(server_key,host,port,scheme,timeout)
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
func _on_login_button_button_down() -> void:
$Panel/LoginPanel.show()
$Panel/SigninPanel.hide()
onLogInPage = 1
onSignInPage = 0
pass # Replace with function body.
func _on_sign_in_button_button_down() -> void:
$Panel/LoginPanel.hide()
$Panel/SigninPanel.show()
onLogInPage = 0
onSignInPage = 1
pass # Replace with function body.
#login menu
func _on_enter_button_button_down() -> void:
#when field present and in login page
if onLogInPage and $Panel/LoginPanel/EmailInput.text !="" and $Panel/LoginPanel/PasswordInput.text!="":
session = await client.authenticate_email_async($Panel/LoginPanel/EmailInput.text,$Panel/LoginPanel/PasswordInput.text)
socket = Nakama.create_socket_from(client)
socket.connected.connect(onSocketConnected)
socket.connection_error.connect(onSocketConnectionError)
await socket.connect_async(session)
if session.created:
$Panel/Debug.text = "Please sign up before login"
_on_sign_in_button_button_down()
client.delete_account_async(session)
client.session_logout_async(session)
return
elif onSignInPage and $Panel/SigninPanel/EmailInput.text != "" and $Panel/SigninPanel/PasswordInput.text != "" and $Panel/SigninPanel/DisplayNameInput.text != "" and $Panel/SigninPanel/NameInput.text != "":
session = await client.authenticate_email_async($Panel/SigninPanel/EmailInput.text,$Panel/SigninPanel/PasswordInput.text)
socket = Nakama.create_socket_from(client)
socket.connected.connect(onSocketConnected)
await socket.connect_async(session)
await client.update_account_async(session,$Panel/SigninPanel/NameInput.text,$Panel/SigninPanel/DisplayNameInput.text)
else:
$Panel/Debug.text = "At least one field is not filled in"
return
socket.closed.connect(onSocketClosed)
socket.received_error.connect(onSocketReceivedError)
socket.received_stream_state.connect(onSocketReceivedStreamState)
socket.received_channel_message.connect(onReceivedChannelMessage)
socket.received_match_presence.connect(onMatchPresence)
socket.received_match_state.connect(onMatchState)
socket.received_stream_presence.connect(onSocketReceivedStreamPresence)
socket.received_status_presence.connect(onSocketReceivedStreamState)
pass # Replace with function body.
#region socket signals
func onMatchPresence(presence:NakamaRTAPI.MatchPresenceEvent):
print(presence)
func onMatchState(state:NakamaRTAPI.MatchData):
print(state.data)
func onSocketConnected():
print("Socket Connected")
if onLogInPage:
$Panel/Debug.text = "Successfully login"
else:
$Panel/Debug.text = "Successfully sign up"
var response = await socket.rpc_async("join")
socket.received_stream_presence.connect(self._on_stream_presence)
func onSocketClosed():
print("Socket Closed")
func onSocketReceivedError(err):
print("Socket Error:"+str(err))
func onSocketConnectionError(err):
print("Socket Error:"+str(err))
$Panel/Debug.text = "Password incorrect"
func onSocketReceivedStreamState(state: NakamaRTAPI.StreamData):
print("Socket stream state:"+str(JSON.parse_string(state.state)))
if JSON.parse_string(state.state)["event_type"]==1:
$Panel2/PlayerCount.text = str(int(JSON.parse_string(state.state)["context"])) + " Online"
func onReceivedChannelMessage(message):
print("Received message:"+str(message))
func onSocketReceivedStreamPresence(state):
print("someone joined or someone leaved"+state)
func _on_stream_presence(p_presence : NakamaRTAPI.StreamPresenceEvent):
print("Received presences on stream: %s" % [p_presence.stream])
for p in p_presence.joins:
print("User ID: %s, Username: %s, Status: %s" % [p.user_id, p.username, p.status])
for p in p_presence.leaves:
print("User ID: %s, Username: %s, Status: %s" % [p.user_id, p.username, p.status])
#endregion
func _on_disconnect_button_button_down() -> void:
var response = await socket.rpc_async("leave")
pass # Replace with function body.
and here is the godot output when I login 2 account then disconnect one using disconnect button calling rpc of leave in lua
=== Nakama : DEBUG === Sending request [ID: 1, Method: POST, Uri: http://127.0.0.1:7350/v2/account/authenticate/email?create=true&, Headers: { "Authorization": "Basic ZGVmYXVsdGtleTo=" }, Body: {"email":"test@gmail.com","password":"password"}, Timeout: 10, Retries: 3, Backoff base: 10 ms]
=== Nakama : DEBUG === Freeing request 1
=== Nakama : DEBUG === Connecting to host: ws://127.0.0.1:7350/ws?lang=en&status=false&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWQiOiI3OWQ2MDhlMS05ODBiLTQ1MTMtOGFlMi1lZTRhY2IyMWI2MmEiLCJ1aWQiOiI0ZTM1MTBhMy0yNDAwLTRjZGMtOWMyYi1jNGQzNjNjODFmZmMiLCJ1c24iOiJ0ZXN0IiwiZXhwIjoxNzQ3NjY5NTgxfQ.-hefFztRCZrhm8o0CFHPwowCZpimZBPkNaWzOnUeP2s
Socket Connected
=== Nakama : DEBUG === Sending async request: http_key: <null>, id: join, payload: <null>,
=== Nakama : INFO === Connected!
Socket stream state:{ "context": 1.0, "event_type": 1.0 }
=== Nakama : DEBUG === Resuming request: 1: { "cid": "1", "rpc": { "id": "join" } }
=== Nakama : DEBUG === Sending request [ID: 1, Method: POST, Uri: http://127.0.0.1:7350/v2/account/authenticate/email?create=true&, Headers: { "Authorization": "Basic ZGVmYXVsdGtleTo=" }, Body: {"email":"test2@gmail.com","password":"password"}, Timeout: 10, Retries: 3, Backoff base: 10 ms]
=== Nakama : DEBUG === Freeing request 1
=== Nakama : DEBUG === Connecting to host: ws://127.0.0.1:7350/ws?lang=en&status=false&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWQiOiI0NjViOTdiNi1lYWE5LTQyODctYmJiNC02YjA5NjgxNzY3YzciLCJ1aWQiOiJmNzY3ODczOC01ODdhLTQ2ZDktYTYwNC0xN2MxMzhiNGEzMzQiLCJ1c24iOiJ0ZXN0MiIsImV4cCI6MTc0NzY2OTU4NX0.1uynf1fj69kLFSNtb3tuSbvAs9FbmZ0R5j3Yts4N-j0
Socket Connected
=== Nakama : DEBUG === Sending async request: http_key: <null>, id: join, payload: <null>,
=== Nakama : INFO === Connected!
Socket stream state:{ "context": 2.0, "event_type": 1.0 }
Socket stream state:{ "context": 2.0, "event_type": 1.0 }
=== Nakama : DEBUG === Resuming request: 1: { "cid": "1", "rpc": { "id": "join" } }
=== Nakama : DEBUG === Sending async request: http_key: <null>, id: leave, payload: <null>,
=== Nakama : DEBUG === Resuming request: 2: { "cid": "2", "rpc": { "id": "leave" } }
Socket stream state:{ "context": 1.0, "event_type": 1.0 }
Sorry if the code and log too long and messy for read