diff --git a/assets/init.lua b/assets/init.lua index 7d27e3a..6402dec 100644 --- a/assets/init.lua +++ b/assets/init.lua @@ -2,6 +2,7 @@ module('platform') module('input') module('scene') module('locale') +module('event') -- Default Input bindings. if PLATFORM == "psp" then @@ -40,4 +41,10 @@ end -- sceneSet('map') -- mapLoad('map/testmap/testmap.dmf') -localeSet(DUSK_LOCALE_EN_US) \ No newline at end of file +localeSet(DUSK_LOCALE_EN_US) + +function eventTest() + print("Event system is working!") +end + +eventSubscribe(EVENT_TEST, eventTest) \ No newline at end of file diff --git a/src/engine/engine.c b/src/engine/engine.c index f7f5732..483fe53 100644 --- a/src/engine/engine.c +++ b/src/engine/engine.c @@ -27,7 +27,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { ENGINE.argc = argc; ENGINE.argv = argv; - // Init systems. Order is important. timeInit(); inputInit(); diff --git a/src/event/event.c b/src/event/event.c index 6d79d4a..1bce770 100644 --- a/src/event/event.c +++ b/src/event/event.c @@ -24,17 +24,34 @@ void eventInit( event->maxListeners = maxListeners; } -eventsub_t eventSubscribe( +eventsub_t eventSubscribeUser( event_t *event, - const eventcallback_t callback, - const void *user + const eventtype_t type, + const eventuserdata_t user ) { assertNotNull(event, "Event cannot be NULL"); - assertNotNull(callback, "Callback cannot be NULL"); assertTrue( event->listenerCount < event->maxListeners, "Maximum number of listeners reached" ); + + if(type == EVENT_TYPE_C) { + assertNotNull( + user.c.callback, + "C event listener callback cannot be NULL" + ); + } else if(type == EVENT_TYPE_SCRIPT) { + assertNotNull( + user.script.context, + "Script event listener context cannot be NULL" + ); + assertTrue( + user.script.luaFunctionRef != LUA_NOREF, + "Script event listener function reference is invalid" + ); + } else { + assertUnreachable("Unknown event listener type"); + } // Gen a new ID eventsub_t id = event->nextId++; @@ -44,13 +61,86 @@ eventsub_t eventSubscribe( // Append listener eventlistener_t *listener = &event->listenerArray[event->listenerCount++]; memoryZero(listener, sizeof(eventlistener_t)); - listener->callback = callback; - listener->user = (void*)user; + listener->user = user; listener->id = id; + listener->type = type; return id; } +eventsub_t eventSubscribe( + event_t *event, + const eventcallback_t callback, + const void *user +) { + eventSubscribeUser( + event, + EVENT_TYPE_C, + (eventuserdata_t){ .c = { .callback = callback, .user = (void *)user } } + ); +} + +eventsub_t eventSubscribeScriptContext( + event_t *event, + scriptcontext_t *context, + const int functionIndex +) { + assertNotNull(context, "Script context cannot be NULL"); + assertTrue( + lua_isfunction(context->luaState, functionIndex), + "Expected function at given index" + ); + + // Create a reference to the function + lua_pushvalue(context->luaState, functionIndex); + int funcRef = luaL_ref(context->luaState, LUA_REGISTRYINDEX); + + eventscript_t scriptUser = { + .context = context, + .luaFunctionRef = funcRef + }; + + // Note to the context that it is now a part of this event + bool_t alreadySubbed = false; + uint8_t i; + i = 0; + do { + if(context->subscribedEvents[i] != event) { + i++; + continue; + } + + if(context->subscribedEvents[i] == NULL) break; + + alreadySubbed = true; + break; + } while(i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS); + + if(!alreadySubbed) { + i = 0; + do { + if(context->subscribedEvents[i] != NULL) { + i++; + continue; + } + + context->subscribedEvents[i] = event; + break; + } while(i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS); + + assertTrue( + i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS, + "Script context has reached maximum event subscriptions" + ); + } + + return eventSubscribeUser( + event, + EVENT_TYPE_SCRIPT, + (eventuserdata_t){ .script = scriptUser } + ); +} + void eventUnsubscribe(event_t *event, const eventsub_t id) { assertNotNull(event, "Event cannot be NULL"); assertFalse(event->isInvoking, "Cannot unsubscribe while invoking event"); @@ -83,6 +173,29 @@ void eventUnsubscribe(event_t *event, const eventsub_t id) { event->listenerCount--; } +void eventUnsubscribeScriptContext(event_t *event, const scriptcontext_t *ctx) { + assertNotNull(event, "Event cannot be NULL"); + assertNotNull(ctx, "Script context cannot be NULL"); + + if(event->listenerCount == 0) return; + + uint16_t i = 0; + do { + eventlistener_t *listener = &event->listenerArray[i]; + if( + listener->type != EVENT_TYPE_SCRIPT || + listener->user.script.context != ctx + ) { + i++; + continue; + } + + // This listener belongs to the context and will need to go away. This will + // in turn decrement the listener count so we don't increment i here. + eventUnsubscribe(event, listener->id); + } while(i < event->listenerCount); +} + void eventInvoke(event_t *event, const void *eventParams) { assertNotNull(event, "Event cannot be NULL"); @@ -97,8 +210,30 @@ void eventInvoke(event_t *event, const void *eventParams) { }; do { - data.user = event->listenerArray[i].user; - event->listenerArray[i].callback(&data); + eventlistener_t *listener = &event->listenerArray[i]; + + if(listener->type == EVENT_TYPE_C) { + listener->user.c.callback(&data, listener->user.c); + } else if(listener->type == EVENT_TYPE_SCRIPT) { + // Call Lua function + lua_State *L = listener->user.script.context->luaState; + assertNotNull(L, "Lua state in event listener cannot be NULL"); + + // Push function + lua_rawgeti(L, LUA_REGISTRYINDEX, listener->user.script.luaFunctionRef); + // Push eventdata as lightuserdata + lua_pushlightuserdata(L, &data); + + // Call function with 1 arg, 0 return values + if(lua_pcall(L, 1, 0, 0) != LUA_OK) { + const char_t *strErr = lua_tostring(L, -1); + lua_pop(L, 1); + // Log error but continue + printf("Error invoking Lua event listener: %s\n", strErr); + } + } else { + assertUnreachable("Unknown event listener type"); + } i++; } while(i < event->listenerCount); diff --git a/src/event/event.h b/src/event/event.h index 9c065ce..c89f4fe 100644 --- a/src/event/event.h +++ b/src/event/event.h @@ -6,21 +6,18 @@ */ #pragma once -#include "dusk.h" +#include "eventuser.h" typedef struct event_s event_t; -typedef struct { - void *user; +typedef struct eventdata_s { const void *eventParams; const event_t *event; } eventdata_t; -typedef void (*eventcallback_t)(eventdata_t *data); - -typedef struct { - eventcallback_t callback; - void *user; +typedef struct eventlistener_s { + eventuserdata_t user; + eventtype_t type; uint16_t id; } eventlistener_t; @@ -52,16 +49,44 @@ void eventInit( * Subscribe to an event. * * @param event The event to subscribe to. - * @param callback The callback function to invoke. + * @param type The type of the event (C or Lua). * @param user User data to pass to the callback. * @return The subscription ID, used to unsubscribe later. */ +eventsub_t eventSubscribeUser( + event_t *event, + const eventtype_t type, + const eventuserdata_t user +); + +/** + * Subscribe to an event with a C function callback. + * + * @param event The event to subscribe to. + * @param callback The C function callback. + * @param user User data pointer to pass to the callback. + * @return The subscription ID, used to unsubscribe later. + */ eventsub_t eventSubscribe( event_t *event, const eventcallback_t callback, const void *user ); +/** + * Subscribe to an event with a script function callback. + * + * @param event The event to subscribe to. + * @param context The script context. + * @param functionIndex The index of the Lua function on the stack. + * @return The subscription ID, used to unsubscribe later. + */ +eventsub_t eventSubscribeScriptContext( + event_t *event, + scriptcontext_t *context, + const int functionIndex +); + /** * Unsubscribe from an event. * @@ -70,6 +95,14 @@ eventsub_t eventSubscribe( */ void eventUnsubscribe(event_t *event, const eventsub_t subscription); +/** + * Unsubscribe all event listeners associated with a script context. + * + * @param event The event to unsubscribe from. + * @param context The script context whose listeners should be removed. + */ +void eventUnsubscribeScriptContext(event_t *event, const scriptcontext_t *ctx); + /** * Invoke an event, calling all subscribed listeners. * diff --git a/src/event/eventcallback.h b/src/event/eventcallback.h new file mode 100644 index 0000000..a6a6a37 --- /dev/null +++ b/src/event/eventcallback.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct eventdata_s eventdata_t; +typedef struct eventc_s eventc_t; + +typedef void (*eventcallback_t)(eventdata_t *data, eventc_t user); \ No newline at end of file diff --git a/src/event/eventuser.h b/src/event/eventuser.h new file mode 100644 index 0000000..1e6f3c5 --- /dev/null +++ b/src/event/eventuser.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "eventcallback.h" +#include "script/scriptcontext.h" + +typedef enum { + EVENT_TYPE_C = 0, + EVENT_TYPE_SCRIPT = 1 +} eventtype_t; + +typedef struct { + scriptcontext_t *context; + int luaFunctionRef; +} eventscript_t; + +typedef struct eventc_s { + void *user; + eventcallback_t callback; +} eventc_t; + +typedef union eventuserdata_u { + eventscript_t script; + eventc_t c; +} eventuserdata_t; \ No newline at end of file diff --git a/src/input/input.c b/src/input/input.c index 0073e13..6e3d54f 100644 --- a/src/input/input.c +++ b/src/input/input.c @@ -24,6 +24,13 @@ void inputInit(void) { } INPUT.deadzone = 0.2f; + + eventInit( + &INPUT.eventPressed, INPUT.pressedListeners, INPUT_LISTENER_PRESSED_MAX + ); + eventInit( + &INPUT.eventReleased, INPUT.releasedListeners, INPUT_LISTENER_RELEASED_MAX + ); } void inputUpdate(void) { @@ -89,6 +96,29 @@ void inputUpdate(void) { cur++; } while(cur->name); + + // Do we need to fire off events? + if(INPUT.eventPressed.listenerCount > 0) { + action = &INPUT.actions[0]; + do { + if(inputPressed(action->action)) { + inputevent_t inputEvent = { .action = action->action }; + eventInvoke(&INPUT.eventPressed, &inputEvent); + } + action++; + } while(action < &INPUT.actions[INPUT_ACTION_COUNT]); + } + + if(INPUT.eventReleased.listenerCount > 0) { + action = &INPUT.actions[0]; + do { + if(inputReleased(action->action)) { + inputevent_t inputEvent = { .action = action->action }; + eventInvoke(&INPUT.eventReleased, &inputEvent); + } + action++; + } while(action < &INPUT.actions[INPUT_ACTION_COUNT]); + } } float_t inputGetCurrentValue(const inputaction_t action) { diff --git a/src/input/input.h b/src/input/input.h index f4fd861..45f820e 100644 --- a/src/input/input.h +++ b/src/input/input.h @@ -8,10 +8,23 @@ #pragma once #include "inputbutton.h" #include "inputaction.h" +#include "event/event.h" + +#define INPUT_LISTENER_PRESSED_MAX 16 +#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX + +typedef struct { + const inputaction_t action; +} inputevent_t; typedef struct { inputactiondata_t actions[INPUT_ACTION_COUNT]; + eventlistener_t pressedListeners[INPUT_LISTENER_PRESSED_MAX]; + event_t eventPressed; + eventlistener_t releasedListeners[INPUT_LISTENER_RELEASED_MAX]; + event_t eventReleased; + #if INPUT_GAMEPAD == 1 float_t deadzone; #endif diff --git a/src/script/module/moduleevent.h b/src/script/module/moduleevent.h new file mode 100644 index 0000000..2565524 --- /dev/null +++ b/src/script/module/moduleevent.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptcontext.h" +#include "event/event.h" +#include "engine/engine.h" + +int moduleEventSubscribe(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL"); + + // State has user pointer to owning scriptcontext_t + scriptcontext_t *context = *(scriptcontext_t **)lua_getextraspace(L); + assertNotNull(context, "Script context cannot be NULL"); + + // Expecting event pointer + if(!lua_islightuserdata(L, 1)) { + luaL_error(L, "eventSubscribe: Expected event pointer as first argument"); + return 0; + } + + // Expecting callback function (Lua function) + if(!lua_isfunction(L, 2)) { + luaL_error(L, "eventSubscribe: Expected function as second argument"); + return 0; + } + + event_t *event = (event_t *)lua_touserdata(L, 1); + assertNotNull(event, "Event cannot be NULL"); + + eventsub_t id = eventSubscribeScriptContext(event, context, 2); + + // Pass back to lua. + lua_pushinteger(L, id); + return 1; +} + +void moduleEvent(scriptcontext_t *context) { + scriptContextRegFunc(context, "eventSubscribe", moduleEventSubscribe); + + // register test event constant + scriptContextRegPointer( + context, + "EVENT_TEST", + (void *)&EVENT_TEST + ); +} \ No newline at end of file diff --git a/src/script/scriptcontext.c b/src/script/scriptcontext.c index 7527548..8f26064 100644 --- a/src/script/scriptcontext.c +++ b/src/script/scriptcontext.c @@ -11,6 +11,7 @@ #include "util/memory.h" #include "debug/debug.h" #include "script/scriptmodule.h" +#include "event/event.h" errorret_t scriptContextInit(scriptcontext_t *context) { assertNotNull(context, "Script context cannot be NULL"); @@ -195,6 +196,12 @@ errorret_t scriptContextExecFile(scriptcontext_t *ctx, const char_t *fname) { void scriptContextDispose(scriptcontext_t *context) { assertNotNull(context, "Script context cannot be NULL"); assertNotNull(context->luaState, "Lua state is not initialized"); + + for(uint8_t i = 0; i < SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS; i++) { + event_t *event = context->subscribedEvents[i]; + if(event == NULL) continue; + eventUnsubscribeScriptContext(event, context); + } if(context->luaState != NULL) { lua_close(context->luaState); diff --git a/src/script/scriptcontext.h b/src/script/scriptcontext.h index 3c63b95..bfbfbfe 100644 --- a/src/script/scriptcontext.h +++ b/src/script/scriptcontext.h @@ -12,8 +12,13 @@ #include #include +typedef struct event_s event_t; + +#define SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS 64 + typedef struct scriptcontext_s { lua_State *luaState; + event_t* subscribedEvents[SCRIPT_CONTEXT_MAX_EVENT_SUBSCRIPTIONS]; } scriptcontext_t; /** diff --git a/src/script/scriptmodule.c b/src/script/scriptmodule.c index de89970..b42c914 100644 --- a/src/script/scriptmodule.c +++ b/src/script/scriptmodule.c @@ -13,6 +13,7 @@ #include "script/module/moduleitem.h" #include "script/module/modulelocale.h" #include "script/module/moduletime.h" +#include "script/module/moduleevent.h" const scriptmodule_t SCRIPT_MODULE_LIST[] = { { .name = "system", .callback = moduleSystem }, @@ -22,6 +23,7 @@ const scriptmodule_t SCRIPT_MODULE_LIST[] = { { .name = "item", .callback = moduleItem }, { .name = "locale", .callback = moduleLocale }, { .name = "time", .callback = moduleTime }, + { .name = "event", .callback = moduleEvent }, }; #define SCRIPT_MODULE_COUNT ( \