diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index c1fd4f4..e2aeb31 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -1569,6 +1569,219 @@ var HTML_CONTENTa = """ """.to_utf8_buffer() var HTML_CONTENT = """ + WebSocket API Demo + + + + + + + + + + +

๐Ÿ”Œ WebSocket API Demo

+ +
+
+

WebSocket API Usage:

+

local socket = WebSocket:new(url)

+

socket:on('open', function() ... end)

+

socket:on('message', function(event) print(event.data) end)

+

socket:send('Hello Server!')

+

socket:close()

+
+ +

Connection

+
+
+
+ + +
+
Status: Disconnected
+
+ + +
+
+
+ +

Send Messages

+
+
+
+ + +
+
+ + Or press Enter to send +
+
+
+ +

Event Log

+
+ +
+
+
Initializing...
+
+ +
+

WebSocket Events:

+
    +
  • open: Fired when connection is established
  • +
  • message: Fired when a message is received (event.data contains the message)
  • +
  • close: Fired when connection is closed
  • +
  • error: Fired when an error occurs (event.message contains error info)
  • +
+

WebSocket Methods:

+
    +
  • WebSocket:new(url): Creates and connects to WebSocket server
  • +
  • socket:send(message): Sends a text message to the server
  • +
  • socket:close(): Closes the connection
  • +
  • socket:on(event, callback): Registers event handlers
  • +
+
+
+ +""".to_utf8_buffer() + +var HTML_CONTENTy = """ Network & JSON API Demo diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index ae379e6..67dc489 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -122,10 +122,7 @@ func _lua_thread_worker(): lua_vm.lua_setfield(-2, "sleep") lua_vm.lua_setglobal("Time") - # Setup GURT API with thread-safe versions _setup_threaded_gurt_api() - - # Setup additional API functions that are needed in callbacks _setup_additional_lua_apis() while not should_exit: @@ -320,17 +317,11 @@ func _setup_additional_lua_apis(): lua_vm.lua_setfield(-2, "tostring") lua_vm.lua_pop(1) # Pop table from stack - # Setup Signal API for threaded execution LuaSignalUtils.setup_signal_api(lua_vm) - - # Setup Clipboard API for threaded execution LuaClipboardUtils.setup_clipboard_api(lua_vm) - - # Setup Network API for threaded execution LuaNetworkUtils.setup_network_api(lua_vm) - - # Setup JSON API for threaded execution LuaJSONUtils.setup_json_api(lua_vm) + LuaWebSocketUtils.setup_websocket_api(lua_vm) func _threaded_table_tostring_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) diff --git a/flumi/Scripts/Utils/Lua/WebSocket.gd b/flumi/Scripts/Utils/Lua/WebSocket.gd new file mode 100644 index 0000000..cb178c9 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/WebSocket.gd @@ -0,0 +1,260 @@ +class_name LuaWebSocketUtils +extends RefCounted + +static var websocket_instances: Dictionary = {} +static var instance_counter: int = 0 + +class WebSocketWrapper: + extends RefCounted + + var instance_id: String + var vm: LuauVM + var url: String + var websocket: WebSocketPeer + var is_connected: bool = false + var event_handlers: Dictionary = {} + var timer: Timer + var last_state: int = -1 + + func _init(): + websocket = WebSocketPeer.new() + + func connect_to_url(): + if is_connected: + return + + var error = websocket.connect_to_url(url) + + if error == OK: + # Start polling timer + if timer: + timer.queue_free() + + timer = Timer.new() + timer.wait_time = 0.016 # ~60 FPS polling + timer.timeout.connect(_poll_websocket) + + # Add to scene tree using call_deferred for thread safety + var main_loop = Engine.get_main_loop() + + if main_loop and main_loop.current_scene: + main_loop.current_scene.call_deferred("add_child", timer) + timer.call_deferred("start") + else: + trigger_event("error", {"message": "No scene available for WebSocket timer"}) + else: + trigger_event("error", {"message": "Failed to connect to " + url + " (error: " + str(error) + ")"}) + + func _poll_websocket(): + if not websocket: + return + + websocket.poll() + var state = websocket.get_ready_state() + + match state: + WebSocketPeer.STATE_OPEN: + if not is_connected: + is_connected = true + trigger_event("open", {}) + + # Check for messages + while websocket.get_available_packet_count() > 0: + var packet = websocket.get_packet() + var message = packet.get_string_from_utf8() + trigger_event("message", {"data": message}) + + WebSocketPeer.STATE_CLOSED: + if is_connected: + is_connected = false + trigger_event("close", {}) + + # Clean up timer + if timer: + timer.queue_free() + timer = null + + WebSocketPeer.STATE_CONNECTING: + # Still connecting, keep polling + pass + + WebSocketPeer.STATE_CLOSING: + # Connection is closing + if is_connected: + is_connected = false + + _: + # Unknown state or connection failed + if is_connected: + is_connected = false + trigger_event("close", {}) + elif not is_connected: + # This might be a connection failure + trigger_event("error", {"message": "Connection failed or was rejected by server"}) + + func send_message(message: String): + if is_connected and websocket: + websocket.send_text(message) + + func close_connection(): + if websocket: + websocket.close() + is_connected = false + + if timer: + timer.queue_free() + timer = null + + func trigger_event(event_name: String, data: Dictionary): + if not vm or not event_handlers.has(event_name): + return + + var func_ref = event_handlers[event_name] + + # Get the function from the reference + vm.lua_getref(func_ref) + + if vm.lua_isfunction(-1): + # Create event data table + vm.lua_newtable() + for key in data: + vm.lua_pushvariant(data[key]) + vm.lua_setfield(-2, key) + + # Call the function + var result = vm.lua_pcall(1, 0, 0) + if result != vm.LUA_OK: + vm.lua_pop(1) + else: + vm.lua_pop(1) # Pop the non-function value + +static func setup_websocket_api(vm: LuauVM): + vm.lua_newtable() + vm.lua_pushcallable(_websocket_new, "WebSocket.new") + vm.lua_setfield(-2, "new") + vm.lua_setglobal("WebSocket") + +static func _websocket_new(vm: LuauVM) -> int: + # handle both WebSocket:new(url) and WebSocket.new(url) syntax + var url: String + if vm.lua_gettop() == 2 and vm.lua_istable(1): + url = vm.luaL_checkstring(2) + else: + url = vm.luaL_checkstring(1) + + # Generate unique instance ID + instance_counter += 1 + var instance_id = "ws_" + str(instance_counter) + + # Create WebSocket wrapper object + var wrapper = WebSocketWrapper.new() + wrapper.instance_id = instance_id + wrapper.vm = vm + wrapper.url = url + + # Store in global instances to keep it alive + websocket_instances[instance_id] = wrapper + + # Create Lua table for the WebSocket + vm.lua_newtable() + + # Store instance ID + vm.lua_pushstring(instance_id) + vm.lua_setfield(-2, "_instance_id") + + # Store URL + vm.lua_pushstring(url) + vm.lua_setfield(-2, "_url") + + # Store connection state + vm.lua_pushboolean(false) + vm.lua_setfield(-2, "_connected") + + # Initialize event handlers table + vm.lua_newtable() + vm.lua_setfield(-2, "_event_handlers") + + # Add methods + vm.lua_pushcallable(_websocket_on, "websocket.on") + vm.lua_setfield(-2, "on") + + vm.lua_pushcallable(_websocket_send, "websocket.send") + vm.lua_setfield(-2, "send") + + vm.lua_pushcallable(_websocket_close, "websocket.close") + vm.lua_setfield(-2, "close") + + vm.lua_pushcallable(_websocket_connect, "websocket.connect") + vm.lua_setfield(-2, "connect") + + # Auto-connect + wrapper.connect_to_url() + + return 1 + +static func _websocket_on(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var event_name: String = vm.luaL_checkstring(2) + vm.luaL_checktype(3, vm.LUA_TFUNCTION) + + # Get instance ID + vm.lua_getfield(1, "_instance_id") + var instance_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get wrapper instance + var wrapper: WebSocketWrapper = websocket_instances.get(instance_id) + if wrapper: + # Store the function reference in wrapper + var func_ref = vm.lua_ref(3) + wrapper.event_handlers[event_name] = func_ref + + return 0 + +static func _websocket_send(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var message: String = vm.luaL_checkstring(2) + + # Get instance ID + vm.lua_getfield(1, "_instance_id") + var instance_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get wrapper instance + var wrapper: WebSocketWrapper = websocket_instances.get(instance_id) + if wrapper and wrapper.is_connected: + wrapper.send_message(message) + else: + vm.luaL_error("WebSocket is not connected") + + return 0 + +static func _websocket_close(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + # Get instance ID + vm.lua_getfield(1, "_instance_id") + var instance_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get wrapper instance + var wrapper: WebSocketWrapper = websocket_instances.get(instance_id) + if wrapper: + wrapper.close_connection() + + return 0 + +static func _websocket_connect(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + # Get instance ID + vm.lua_getfield(1, "_instance_id") + var instance_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get wrapper instance + var wrapper: WebSocketWrapper = websocket_instances.get(instance_id) + if wrapper: + wrapper.connect_to_url() + + return 0 \ No newline at end of file diff --git a/flumi/Scripts/Utils/Lua/WebSocket.gd.uid b/flumi/Scripts/Utils/Lua/WebSocket.gd.uid new file mode 100644 index 0000000..4fac59b --- /dev/null +++ b/flumi/Scripts/Utils/Lua/WebSocket.gd.uid @@ -0,0 +1 @@ +uid://boar4c3uf4x1e