From b3446573de054f695a4d4e9b6a4505152c623908 Mon Sep 17 00:00:00 2001 From: Tim Kurvers Date: Fri, 12 Apr 2024 23:25:40 +0200 Subject: [PATCH] feat: add event machinery and global script arguments handling (#57) --- src/ui/components/abstract/ScriptObject.ts | 2 +- src/ui/components/simple/Frame.script.ts | 15 ++- src/ui/scripting/EventEmitter.ts | 39 ++++++ src/ui/scripting/EventType.ts | 6 + src/ui/scripting/FrameScriptObject.ts | 30 ++++- src/ui/scripting/Script.ts | 4 + src/ui/scripting/ScriptingContext.ts | 144 ++++++++++++++++++++- 7 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 src/ui/scripting/EventEmitter.ts create mode 100644 src/ui/scripting/EventType.ts diff --git a/src/ui/components/abstract/ScriptObject.ts b/src/ui/components/abstract/ScriptObject.ts index 3432534..1caddc7 100644 --- a/src/ui/components/abstract/ScriptObject.ts +++ b/src/ui/components/abstract/ScriptObject.ts @@ -23,7 +23,7 @@ class ScriptObject extends FrameScriptObject { set name(name) { if (this._name) { - this.deregister(); + this.unregister(); this._name = null; } diff --git a/src/ui/components/simple/Frame.script.ts b/src/ui/components/simple/Frame.script.ts index 9c025af..662eacb 100644 --- a/src/ui/components/simple/Frame.script.ts +++ b/src/ui/components/simple/Frame.script.ts @@ -1,7 +1,12 @@ +import EventType from '../../scripting/EventType'; import { lua_State, + lua_isstring, lua_pushnil, lua_pushnumber, + lua_tolstring, + luaL_error, + to_jsstring, } from '../../scripting/lua'; import Frame from './Frame'; @@ -76,7 +81,15 @@ export const HookScript = () => { return 0; }; -export const RegisterEvent = () => { +export const RegisterEvent = (L: lua_State): number => { + const frame = Frame.getObjectFromStack(L); + + if (!lua_isstring(L, 2)) { + return luaL_error(L, 'Usage: %s:RegisterEvent("event")', frame.displayName); + } + + const event = to_jsstring(lua_tolstring(L, 2, 0)); + frame.registerScriptEvent(event as EventType); return 0; }; diff --git a/src/ui/scripting/EventEmitter.ts b/src/ui/scripting/EventEmitter.ts new file mode 100644 index 0000000..ab9d919 --- /dev/null +++ b/src/ui/scripting/EventEmitter.ts @@ -0,0 +1,39 @@ +import { LinkedList, LinkedListNode } from '../../utils'; + +import EventType from './EventType'; +import FrameScriptObject from './FrameScriptObject'; + +class EventListenerNode extends LinkedListNode { + listener: FrameScriptObject; + + constructor(listener: FrameScriptObject) { + super(); + + this.listener = listener; + } +} + +class EventEmitter { + type: EventType; + listeners: LinkedList; + unregisterListeners: LinkedList; + registerListeners: LinkedList; + signalCount: number; + pendingSignalCount: number; + + constructor(type: EventType) { + this.type = type; + this.listeners = LinkedList.using('link'); + this.unregisterListeners = LinkedList.using('link'); + this.registerListeners = LinkedList.using('link'); + this.signalCount = 0; + this.pendingSignalCount = 0; + } + + get name() { + return this.type; + } +} + +export default EventEmitter; +export { EventListenerNode }; diff --git a/src/ui/scripting/EventType.ts b/src/ui/scripting/EventType.ts new file mode 100644 index 0000000..67ed21f --- /dev/null +++ b/src/ui/scripting/EventType.ts @@ -0,0 +1,6 @@ +enum EventType { + FRAMES_LOADED = 'FRAMES_LOADED', + SET_GLUE_SCREEN = 'SET_GLUE_SCREEN' +} + +export default EventType; diff --git a/src/ui/scripting/FrameScriptObject.ts b/src/ui/scripting/FrameScriptObject.ts index 2fae107..a8a224b 100644 --- a/src/ui/scripting/FrameScriptObject.ts +++ b/src/ui/scripting/FrameScriptObject.ts @@ -1,3 +1,4 @@ +import EventType from './EventType'; import ScriptingContext, { ScriptFunction } from './ScriptingContext'; import Script from './Script'; import ScriptRegistry from './ScriptRegistry'; @@ -84,8 +85,31 @@ class FrameScriptObject { } } - deregister() { - // TODO: Unregister + unregister(_name: string | null = null) { + // TODO: Implementation + } + + registerScriptEvent(type: EventType) { + const scripting = ScriptingContext.instance; + + const event = scripting.events[type]; + if (!event) { + return false; + } + + if (event.pendingSignalCount) { + const node = event.unregisterListeners.find((node) => node.listener === this); + if (node) { + event.unregisterListeners.unlink(node); + } + } + + const node = event.listeners.find((node) => node.listener === this); + if (!node) { + scripting.registerScriptEvent(this, event); + } + + return true; } runScript(name: string, argsCount = 0) { @@ -93,7 +117,7 @@ class FrameScriptObject { const script = this.scripts.get(name); if (script && script.luaRef) { // TODO: Pass in remaining arguments - ScriptingContext.instance.executeFunction(script.luaRef, this, argsCount, undefined, undefined); + ScriptingContext.instance.executeFunction(script.luaRef, this, argsCount); } } diff --git a/src/ui/scripting/Script.ts b/src/ui/scripting/Script.ts index 9775c6c..eb5db68 100644 --- a/src/ui/scripting/Script.ts +++ b/src/ui/scripting/Script.ts @@ -23,6 +23,10 @@ class Script { this.source = null; } + get isLuaRegistered() { + return this.luaRef !== null; + } + get wrapper() { return `return function(${this.args.join(', ')})\n$body\nend`; } diff --git a/src/ui/scripting/ScriptingContext.ts b/src/ui/scripting/ScriptingContext.ts index b956aed..c01d5d8 100644 --- a/src/ui/scripting/ScriptingContext.ts +++ b/src/ui/scripting/ScriptingContext.ts @@ -6,18 +6,23 @@ import { lua_State, lua_atnativeerror, lua_call, + lua_checkstack, lua_createtable, lua_gc, lua_getglobal, lua_getinfo, lua_getlocal, lua_getstack, + lua_gettop, lua_insert, lua_isstring, lua_isuserdata, lua_pcall, + lua_pushboolean, lua_pushcclosure, + lua_pushnumber, lua_pushstring, + lua_pushvalue, lua_rawgeti, lua_replace, lua_setglobal, @@ -36,6 +41,8 @@ import { import bitLua from './vendor/bit.lua?raw'; // eslint-disable-line import/no-unresolved import compatLua from './vendor/compat.lua?raw'; // eslint-disable-line import/no-unresolved +import EventType from './EventType'; +import EventEmitter, { EventListenerNode } from './EventEmitter'; import FrameScriptObject from './FrameScriptObject'; import * as extraScriptFunctions from './globals/extra'; @@ -54,6 +61,7 @@ class ScriptingContext { state: lua_State; errorHandlerRef: lua_Ref; recursiveTableHash: lua_Ref; + events: Record; constructor() { ScriptingContext.instance = this; @@ -73,6 +81,10 @@ class ScriptingContext { this.recursiveTableHash = luaL_ref(L, LUA_REGISTRYINDEX); lua_gc(L, 6, 110); + this.events = Object.assign({}, ...Object.values(EventType).map((type) => ({ + [type]: new EventEmitter(type) + }))); + // TODO: Is this OK, rather than lua_openbase + friends? luaL_openlibs(L); this.execute(bitLua, 'bit.lua'); @@ -140,12 +152,44 @@ class ScriptingContext { return true; } - executeFunction(functionRef: lua_Ref, thisArg: FrameScriptObject, givenArgsCount: number, _unk: unknown, _event: unknown) { + executeFunction(functionRef: lua_Ref, thisArg: FrameScriptObject, givenArgsCount: number, _unk?: unknown, event?: EventEmitter) { const L = this.state; + const stackBase = 1 - givenArgsCount + lua_gettop(L); let argsCount = givenArgsCount; - // TODO: Global 'this', 'event' and 'argX' + lua_checkstack(L, givenArgsCount + 2); + + if (thisArg) { + lua_getglobal(L, 'this'); + + if (!thisArg.isLuaRegistered) { + thisArg.register(); + } + + lua_rawgeti(L, LUA_REGISTRYINDEX, thisArg.luaRef!); + lua_setglobal(L, 'this'); + } + + if (event) { + lua_getglobal(L, 'event'); + lua_pushvalue(L, stackBase); + lua_setglobal(L, 'event'); + } + + const firstArg = event ? 1 : 0; + let globalArgId = 0; + if (firstArg < givenArgsCount) { + for (let i = firstArg; i < givenArgsCount; ++i) { + globalArgId++; + const argName = `arg${globalArgId}`; + lua_getglobal(L, argName); + lua_pushvalue(L, stackBase + firstArg); + lua_setglobal(L, argName); + } + } + + lua_checkstack(L, givenArgsCount + 3); lua_rawgeti(L, LUA_REGISTRYINDEX, this.errorHandlerRef); lua_rawgeti(L, LUA_REGISTRYINDEX, functionRef); @@ -159,7 +203,9 @@ class ScriptingContext { argsCount++; } - // TODO: Arguments + for (let i = 0; i < givenArgsCount; ++i) { + lua_pushvalue(L, stackBase + i); + } if (lua_pcall(L, argsCount, 0, -2 - argsCount)) { lua_settop(L, -2); @@ -167,7 +213,18 @@ class ScriptingContext { lua_settop(L, -2); - // TODO: Clean-up + for (let i = globalArgId; i > 0; --i) { + const argName = `arg${globalArgId}`; + lua_setglobal(L, argName); + } + + if (event) { + lua_setglobal(L, 'event'); + } + + if (thisArg) { + lua_setglobal(L, 'this'); + } lua_settop(L, -1 - givenArgsCount); } @@ -276,6 +333,85 @@ class ScriptingContext { lua_pushcclosure(L, func, 0); lua_setglobal(L, name); } + + registerScriptEvent(object: FrameScriptObject, event: EventEmitter) { + if (event.pendingSignalCount) { + let node = event.registerListeners.find((node) => node.listener === object); + if (node) { + return; + } + + node = new EventListenerNode(object); + event.registerListeners.add(node); + } else { + const node = new EventListenerNode(object); + event.listeners.add(node); + } + } + + signalEvent(type: EventType, format?: string, ...args: Array) { + const L = this.state; + + const event = this.events[type]; + if (!event) { + return; + } + + let argsCount = 1; + lua_pushstring(L, event.type); + + if (format) { + for (const char of format) { + switch (char) { + case 'b': + lua_pushboolean(L, args[argsCount++ - 1] as boolean); + break; + + case 'd': + lua_pushnumber(L, args[argsCount++ - 1] as number); + break; + + case 'f': + lua_pushnumber(L, args[argsCount++ - 1] as number); + break; + + case 's': + lua_pushstring(L, args[argsCount++ - 1] as string); + break; + + case 'u': + lua_pushnumber(L, args[argsCount++ - 1] as number); + break; + } + } + } + + event.signalCount++; + event.pendingSignalCount++; + + lua_checkstack(L, argsCount); + + for (const node of event.listeners) { + const unregisterNode = event.unregisterListeners.find((inner) => inner.listener === node.listener); + if (unregisterNode) { + break; + } + + const script = node.listener.scripts.get('OnEvent'); + if (script?.isLuaRegistered) { + for (let i = 0; i < argsCount; ++i) { + lua_pushvalue(L, -argsCount); + } + + this.executeFunction(script.luaRef!, node.listener, argsCount, null, event); + } + } + + event.pendingSignalCount--; + + // TODO: Unregister listeners + // TODO: Register listeners + } } export default ScriptingContext;