diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index 67f6d3e..ebecbf7 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -798,6 +798,197 @@ var HTML_CONTENT_ADD_REMOVE = """ """.to_utf8_buffer() var HTML_CONTENT = """ + Signal API Demo + + + + + + + + + + +

๐Ÿ”” Signal API Demo

+ +
+
+

Signal API Usage Example:

+

local mySignal = Signal.new()

+

mySignal:connect(function(arg1, arg2) print("Event fired with: ", arg1, arg2) end)

+

mySignal:fire("Hello", 123)

+

connection:disconnect()

+
+ +

Controls

+
+ + + + + +
+ +

Status

+
+
Loading status...
+
+ +

Event Log

+
+
Initializing...
+
+ +
+

Signal API Features:

+
    +
  • Signal.new(): Creates a new signal object
  • +
  • signal:connect(callback): Connects a callback function and returns a connection object
  • +
  • signal:fire(...): Fires the signal with optional arguments
  • +
  • connection:disconnect(): Disconnects a specific connection
  • +
  • signal:disconnect(): Disconnects all connections from the signal
  • +
  • Multiple Connections: One signal can have multiple connected callbacks
  • +
  • Argument Passing: Signals can pass multiple arguments to connected callbacks
  • +
+
+
+ +""".to_utf8_buffer() + +var HTML_CONTENTa = """ Button getAttribute/setAttribute Demo diff --git a/flumi/Scripts/Utils/Lua/Function.gd b/flumi/Scripts/Utils/Lua/Function.gd index c3bff96..0c24152 100644 --- a/flumi/Scripts/Utils/Lua/Function.gd +++ b/flumi/Scripts/Utils/Lua/Function.gd @@ -27,6 +27,12 @@ static func setup_gurt_api(vm: LuauVM, lua_api, dom_parser: HTMLParser) -> void: vm.lua_setfield(-2, "tostring") vm.lua_pop(1) # Pop table from stack + # Setup Signal API + LuaSignalUtils.setup_signal_api(vm) + + # Setup Time API + LuaTimeUtils.setup_time_api(vm) + vm.lua_newtable() vm.lua_pushcallable(LuaPrintUtils.lua_print, "gurt.log") diff --git a/flumi/Scripts/Utils/Lua/Signal.gd b/flumi/Scripts/Utils/Lua/Signal.gd new file mode 100644 index 0000000..26ba6c4 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Signal.gd @@ -0,0 +1,223 @@ +class_name LuaSignalUtils +extends RefCounted + +# Signal API for Lua - provides custom signal functionality +# Usage: local mySignal = Signal.new(), mySignal:connect(callback), mySignal:fire(args...) + +class LuaSignal: + var connections: Array[Dictionary] = [] + var next_connection_id: int = 1 + var signal_table_ref: int = -1 + + func connect_callback(callback_ref: int, vm: LuauVM) -> int: + var connection_id = next_connection_id + next_connection_id += 1 + + var connection = { + "id": connection_id, + "callback_ref": callback_ref, + "vm": vm + } + connections.append(connection) + return connection_id + + func disconnect_callback(connection_id: int, vm: LuauVM) -> void: + for i in range(connections.size() - 1, -1, -1): + var connection = connections[i] + if connection.id == connection_id: + # Clean up the Lua reference from custom storage + vm.lua_pushstring("SIGNAL_CALLBACKS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + vm.lua_pushinteger(connection.callback_ref) + vm.lua_pushnil() + vm.lua_rawset(-3) # Set callbacks[callback_ref] = nil + vm.lua_pop(1) # Pop callbacks table + connections.remove_at(i) + break + + func disconnect_all(vm: LuauVM) -> void: + # Clean up all Lua references from custom storage + vm.lua_pushstring("SIGNAL_CALLBACKS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + for connection in connections: + vm.lua_pushinteger(connection.callback_ref) + vm.lua_pushnil() + vm.lua_rawset(-3) # Set callbacks[callback_ref] = nil + vm.lua_pop(1) # Pop callbacks table + connections.clear() + + func fire_signal(args: Array, signal_table_ref: int = -1) -> void: + for connection in connections: + var vm = connection.vm as LuauVM + # Get the callback function from our custom storage + vm.lua_pushstring("SIGNAL_CALLBACKS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + vm.lua_pushinteger(connection.callback_ref) + vm.lua_rawget(-2) + if vm.lua_isfunction(-1): + # Push the arguments directly (don't pass self) + for arg in args: + vm.lua_pushvariant(arg) + + # Call the function + if vm.lua_pcall(args.size(), 0, 0) != vm.LUA_OK: + print("GURT ERROR in Signal callback: ", vm.lua_tostring(-1)) + vm.lua_pop(1) + # Pop the callbacks table + vm.lua_pop(1) + else: + vm.lua_pop(2) # Pop both the non-function and the callbacks table + +static var signals_registry: Dictionary = {} +static var next_signal_id: int = 1 +static var next_callback_ref: int = 1 + +# Signal.new() constructor +static func signal_new_handler(vm: LuauVM) -> int: + var signal_id = next_signal_id + next_signal_id += 1 + + var lua_signal = LuaSignal.new() + signals_registry[signal_id] = lua_signal + + # Create the signal table + vm.lua_newtable() + vm.lua_pushinteger(signal_id) + vm.lua_setfield(-2, "_signal_id") + + # Store a reference to this signal table for : syntax + vm.lua_pushvalue(-1) # Duplicate the signal table + var signal_table_ref = vm.luaL_ref(vm.LUA_REGISTRYINDEX) + lua_signal.signal_table_ref = signal_table_ref + + # Add methods + vm.lua_pushcallable(signal_connect_handler, "signal:connect") + vm.lua_setfield(-2, "connect") + + vm.lua_pushcallable(signal_fire_handler, "signal:fire") + vm.lua_setfield(-2, "fire") + + vm.lua_pushcallable(signal_disconnect_handler, "signal:disconnect") + vm.lua_setfield(-2, "disconnect") + + return 1 + +# signal:connect(callback) method +static func signal_connect_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.luaL_checktype(2, vm.LUA_TFUNCTION) + + # Get signal ID + vm.lua_getfield(1, "_signal_id") + var signal_id: int = vm.lua_tointeger(-1) + vm.lua_pop(1) + + var lua_signal = signals_registry.get(signal_id) as LuaSignal + if not lua_signal: + vm.lua_pushnil() + return 1 + + # Store callback in a custom registry table instead of using luaL_ref + # Get or create our custom callback storage table + vm.lua_pushstring("SIGNAL_CALLBACKS") + vm.lua_rawget(vm.LUA_REGISTRYINDEX) + if vm.lua_isnil(-1): + vm.lua_pop(1) # Pop nil + vm.lua_newtable() # Create new table + vm.lua_pushstring("SIGNAL_CALLBACKS") + vm.lua_pushvalue(-2) # Duplicate the table + vm.lua_rawset(vm.LUA_REGISTRYINDEX) # Set SIGNAL_CALLBACKS = table + + # Now store the callback function + var callback_ref = next_callback_ref + next_callback_ref += 1 + + vm.lua_pushinteger(callback_ref) # Key + vm.lua_pushvalue(2) # Value (the callback function) + vm.lua_rawset(-3) # Set callbacks[callback_ref] = function + + vm.lua_pop(1) # Pop the callbacks table + + # Connect the callback + var connection_id = lua_signal.connect_callback(callback_ref, vm) + + # Return connection object + vm.lua_newtable() + vm.lua_pushinteger(connection_id) + vm.lua_setfield(-2, "_connection_id") + vm.lua_pushinteger(signal_id) + vm.lua_setfield(-2, "_signal_id") + + vm.lua_pushcallable(connection_disconnect_handler, "connection:disconnect") + vm.lua_setfield(-2, "disconnect") + + return 1 + +# signal:fire(...) method +static func signal_fire_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + # Get signal ID + vm.lua_getfield(1, "_signal_id") + var signal_id: int = vm.lua_tointeger(-1) + vm.lua_pop(1) + + var lua_signal = signals_registry.get(signal_id) as LuaSignal + if not lua_signal: + return 0 + + # Collect arguments (everything after the signal table) + var args: Array = [] + var arg_count = vm.lua_gettop() - 1 # Subtract 1 for the signal table itself + for i in range(2, arg_count + 2): # Start from index 2 (after signal table) + args.append(vm.lua_tovariant(i)) + + # Fire the signal with the signal table reference + lua_signal.fire_signal(args, lua_signal.signal_table_ref) + + return 0 + +# signal:disconnect() method - disconnects all connections +static func signal_disconnect_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + # Get signal ID + vm.lua_getfield(1, "_signal_id") + var signal_id: int = vm.lua_tointeger(-1) + vm.lua_pop(1) + + var lua_signal = signals_registry.get(signal_id) as LuaSignal + if lua_signal: + lua_signal.disconnect_all(vm) + + return 0 + +# connection:disconnect() method - disconnects specific connection +static func connection_disconnect_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + # Get connection ID and signal ID + vm.lua_getfield(1, "_connection_id") + var connection_id: int = vm.lua_tointeger(-1) + vm.lua_pop(1) + + vm.lua_getfield(1, "_signal_id") + var signal_id: int = vm.lua_tointeger(-1) + vm.lua_pop(1) + + var lua_signal = signals_registry.get(signal_id) as LuaSignal + if lua_signal: + lua_signal.disconnect_callback(connection_id, vm) + + return 0 + +static func setup_signal_api(vm: LuauVM) -> void: + # Create Signal table + vm.lua_newtable() + + # Add Signal.new constructor + vm.lua_pushcallable(signal_new_handler, "Signal.new") + vm.lua_setfield(-2, "new") + + # Set as global Signal + vm.lua_setglobal("Signal") diff --git a/flumi/Scripts/Utils/Lua/Signal.gd.uid b/flumi/Scripts/Utils/Lua/Signal.gd.uid new file mode 100644 index 0000000..3fbc3c6 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Signal.gd.uid @@ -0,0 +1 @@ +uid://drb1qy7epbtwe diff --git a/flumi/Scripts/Utils/Lua/Time.gd b/flumi/Scripts/Utils/Lua/Time.gd new file mode 100644 index 0000000..9f3862f --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Time.gd @@ -0,0 +1,244 @@ +class_name LuaTimeUtils +extends RefCounted + +static func time_now_handler(vm: LuauVM) -> int: + var current_time = Time.get_unix_time_from_system() + vm.lua_pushnumber(current_time) + return 1 + +static func time_format_handler(vm: LuauVM) -> int: + var format_string: String = "%H:%M:%S" + var datetime: Dictionary + + if vm.lua_gettop() >= 1 and vm.lua_isnumber(1): + var timestamp: float = vm.lua_tonumber(1) + + var local_now_dict = Time.get_datetime_dict_from_system(false) + var utc_now_dict = Time.get_datetime_dict_from_system(true) + var offset_seconds = Time.get_unix_time_from_datetime_dict(local_now_dict) - Time.get_unix_time_from_datetime_dict(utc_now_dict) + + datetime = Time.get_datetime_dict_from_unix_time(int(timestamp) + offset_seconds) + else: + datetime = Time.get_datetime_dict_from_system(false) + + if vm.lua_gettop() >= 2 and vm.lua_isstring(2): + format_string = vm.lua_tostring(2) + + var formatted_time = format_datetime(datetime, format_string) + + vm.lua_pushstring(formatted_time) + return 1 + +static func time_date_handler(vm: LuauVM) -> int: + var datetime: Dictionary + + if vm.lua_gettop() >= 1 and vm.lua_isnumber(1): + var timestamp = vm.lua_tonumber(1) + + var local_now_dict = Time.get_datetime_dict_from_system(false) + var utc_now_dict = Time.get_datetime_dict_from_system(true) + var offset_seconds = Time.get_unix_time_from_datetime_dict(local_now_dict) - Time.get_unix_time_from_datetime_dict(utc_now_dict) + + datetime = Time.get_datetime_dict_from_unix_time(int(timestamp) + offset_seconds) + else: + datetime = Time.get_datetime_dict_from_system(false) + + vm.lua_newtable() + vm.lua_pushinteger(datetime.year) + vm.lua_setfield(-2, "year") + vm.lua_pushinteger(datetime.month) + vm.lua_setfield(-2, "month") + vm.lua_pushinteger(datetime.day) + vm.lua_setfield(-2, "day") + vm.lua_pushinteger(datetime.hour) + vm.lua_setfield(-2, "hour") + vm.lua_pushinteger(datetime.minute) + vm.lua_setfield(-2, "minute") + vm.lua_pushinteger(datetime.second) + vm.lua_setfield(-2, "second") + vm.lua_pushinteger(datetime.weekday) + vm.lua_setfield(-2, "weekday") + + return 1 + +static func time_sleep_handler(vm: LuauVM) -> int: + vm.luaL_checknumber(1) + var seconds = vm.lua_tonumber(1) + var milliseconds = int(seconds * 1000) + + # TODO: implement a proper sleep function + + vm.lua_pushnumber(seconds) + return 1 + +static func time_benchmark_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TFUNCTION) + + var start_time = Time.get_ticks_msec() + + vm.lua_pushvalue(1) + if vm.lua_pcall(0, 0, 0) != vm.LUA_OK: + var error_msg = vm.lua_tostring(-1) + vm.lua_pop(1) + + var end_time = Time.get_ticks_msec() + var elapsed_ms = end_time - start_time + + vm.lua_pushnumber(elapsed_ms / 1000.0) + vm.lua_pushstring("Error: " + error_msg) + return 2 + + var end_time = Time.get_ticks_msec() + var elapsed_ms = end_time - start_time + + vm.lua_pushnumber(elapsed_ms / 1000.0) + return 1 + +static func time_timer_handler(vm: LuauVM) -> int: + var start_time = Time.get_ticks_msec() + + vm.lua_newtable() + vm.lua_pushnumber(start_time) + vm.lua_setfield(-2, "_start_time") + + vm.lua_pushcallable(timer_elapsed_handler, "timer:elapsed") + vm.lua_setfield(-2, "elapsed") + + vm.lua_pushcallable(timer_reset_handler, "timer:reset") + vm.lua_setfield(-2, "reset") + + return 1 + +static func timer_elapsed_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_start_time") + var start_time = vm.lua_tonumber(-1) + vm.lua_pop(1) + + var current_time = Time.get_ticks_msec() + var elapsed = (current_time - start_time) / 1000.0 + + vm.lua_pushnumber(elapsed) + return 1 + +static func timer_reset_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + var current_time = Time.get_ticks_msec() + vm.lua_pushnumber(current_time) + vm.lua_setfield(1, "_start_time") + + return 0 + +static func time_delay_handler(vm: LuauVM) -> int: + vm.luaL_checknumber(1) + var seconds = vm.lua_tonumber(1) + var end_time = Time.get_ticks_msec() + (seconds * 1000) + + vm.lua_newtable() + vm.lua_pushnumber(end_time) + vm.lua_setfield(-2, "_end_time") + + vm.lua_pushcallable(delay_is_complete_handler, "delay:complete") + vm.lua_setfield(-2, "complete") + + vm.lua_pushcallable(delay_remaining_handler, "delay:remaining") + vm.lua_setfield(-2, "remaining") + + return 1 + +static func delay_is_complete_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_end_time") + var end_time = vm.lua_tonumber(-1) + vm.lua_pop(1) + + var current_time = Time.get_ticks_msec() + var is_complete = current_time >= end_time + + vm.lua_pushboolean(is_complete) + return 1 + +static func delay_remaining_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_end_time") + var end_time = vm.lua_tonumber(-1) + vm.lua_pop(1) + + var current_time = Time.get_ticks_msec() + var remaining = max(0.0, (end_time - current_time) / 1000.0) + + vm.lua_pushnumber(remaining) + return 1 + +static func format_datetime(datetime: Dictionary, format_string: String) -> String: + var result = format_string + + var local_datetime = datetime + if not local_datetime.has("weekday"): + var unix_time = Time.get_unix_time_from_datetime_dict(local_datetime) + local_datetime = Time.get_datetime_dict_from_unix_time(unix_time) + + var ampm = "" + var hour12 = local_datetime.hour + if local_datetime.hour >= 12: + ampm = "pm" + if local_datetime.hour > 12: + hour12 = local_datetime.hour - 12 + else: + ampm = "am" + if local_datetime.hour == 0: + hour12 = 12 + + result = result.replace("%Y", "%04d" % local_datetime.year) + result = result.replace("%y", "%02d" % (local_datetime.year % 100)) + result = result.replace("%m", "%02d" % local_datetime.month) + result = result.replace("%d", "%02d" % local_datetime.day) + result = result.replace("%H", "%02d" % local_datetime.hour) + result = result.replace("%I", "%02d" % hour12) + result = result.replace("%M", "%02d" % local_datetime.minute) + result = result.replace("%S", "%02d" % local_datetime.second) + result = result.replace("%p", ampm) + + var weekday_names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + var weekday_abbrev = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + if local_datetime.weekday >= 0 and local_datetime.weekday <= 6: + result = result.replace("%A", weekday_names[local_datetime.weekday]) + result = result.replace("%a", weekday_abbrev[local_datetime.weekday]) + + var month_names = ["January", "February", "March", "April", "May", "June","July", "August", "September", "October", "November", "December"] + var month_abbrev = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + if local_datetime.month >= 1 and local_datetime.month <= 12: + result = result.replace("%B", month_names[local_datetime.month - 1]) + result = result.replace("%b", month_abbrev[local_datetime.month - 1]) + + return result + +static func setup_time_api(vm: LuauVM) -> void: + vm.lua_newtable() + + vm.lua_pushcallable(time_now_handler, "Time.now") + vm.lua_setfield(-2, "now") + + vm.lua_pushcallable(time_format_handler, "Time.format") + vm.lua_setfield(-2, "format") + + vm.lua_pushcallable(time_date_handler, "Time.date") + vm.lua_setfield(-2, "date") + + vm.lua_pushcallable(time_sleep_handler, "Time.sleep") + vm.lua_setfield(-2, "sleep") + + vm.lua_pushcallable(time_benchmark_handler, "Time.benchmark") + vm.lua_setfield(-2, "benchmark") + + vm.lua_pushcallable(time_timer_handler, "Time.timer") + vm.lua_setfield(-2, "timer") + + vm.lua_pushcallable(time_delay_handler, "Time.delay") + vm.lua_setfield(-2, "delay") + + vm.lua_setglobal("Time") diff --git a/flumi/Scripts/Utils/Lua/Time.gd.uid b/flumi/Scripts/Utils/Lua/Time.gd.uid new file mode 100644 index 0000000..f32b74b --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Time.gd.uid @@ -0,0 +1 @@ +uid://ntey7mujnbo6