#include #include #include #include #include #include #include #include #include #include #include #include #define WOTA_IMPLEMENTATION #include "wota.h" #include "qjs_wota.h" #include "physfs.h" #include "stb_ds.h" #include "jsffi.h" #include "cell.h" #ifdef TRACY_ENABLE #include #if defined(__APPLE__) #include #define MALLOC_OVERHEAD 0 #elif defined(_WIN32) #include #define MALLOC_OVERHEAD 8 #elif defined(__linux__) || defined(__GLIBC__) #define _GNU_SOURCE #include #define MALLOC_OVERHEAD 8 #endif #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #endif int tracy_profiling_enabled = 0; #define ENGINE "engine.cm" static cell_rt **ready_queue = NULL; static cell_rt **main_ready_queue = NULL; static SDL_Mutex *queue_mutex = NULL; static SDL_Condition *queue_cond = NULL; static SDL_Mutex *actors_mutex = NULL; static struct { char *key; cell_rt *value; } *actors = NULL; static unsigned char *zip_buffer_global = NULL; static char *prosperon = NULL; cell_rt *root_cell = NULL; /* Event watch subscribers */ char **event_watchers = NULL; SDL_Mutex *event_watchers_mutex = NULL; static Uint32 queue_event; static SDL_AtomicInt engine_shutdown; static SDL_Thread **runners = NULL; static void exit_handler(void) { SDL_SetAtomicInt(&engine_shutdown, 1); /* Push a terminating event to the SDL event queue */ SDL_Event terminating_event; terminating_event.type = SDL_EVENT_TERMINATING; SDL_PushEvent(&terminating_event); int status; SDL_BroadcastCondition(queue_cond); for (int i = 0; i < arrlen(runners); i++) SDL_WaitThread(runners[i], &status); SDL_Quit(); exit(0); } void actor_free(cell_rt *actor) { // Delete it out of actors first so it can no longer get messages SDL_LockMutex(actors_mutex); shdel(actors, actor->id); int remaining = shlen(actors); SDL_UnlockMutex(actors_mutex); // If in a queue, remove it SDL_LockMutex(queue_mutex); for (int i = 0; i < arrlen(ready_queue); i++) { if (ready_queue[i] == actor) { arrdel(ready_queue, i); break; } } SDL_UnlockMutex(queue_mutex); // Do not go forward with actor destruction until the actor is completely free SDL_LockMutex(actor->mutex); SDL_LockMutex(actor->msg_mutex); SDL_LockMutex(actor->turn); JSContext *js = actor->context; JS_FreeValue(js, actor->idx_buffer); JS_FreeValue(js, actor->message_handle); JS_FreeValue(js, actor->on_exception); JS_FreeValue(js, actor->unneeded); JS_FreeAtom(js, actor->doc_sym); JS_FreeAtom(js, actor->actor_sym); SDL_RemoveTimer(actor->ar); for (int i = 0; i < arrlen(actor->js_swapchains); i++) JS_FreeValue(js, actor->js_swapchains[i]); for (int i = 0; i < hmlen(actor->timers); i++) { SDL_RemoveTimer(actor->timers[i].key); JS_FreeValue(js, actor->timers[i].value); } hmfree(actor->timers); arrfree(actor->js_swapchains); arrfree(actor->module_registry); for (int i = 0; i < arrlen(actor->messages); i++) free(actor->messages[i]); arrfree(actor->messages); /* If still present, free each JSValue. */ for (int i = 0; i < arrlen(actor->events); i++) JS_FreeValue(js, actor->events[i]); arrfree(actor->events); JSRuntime *rt = JS_GetRuntime(js); JS_SetInterruptHandler(rt, NULL, NULL); JS_FreeContext(js); JS_FreeRuntime(rt); free(actor->id); SDL_UnlockMutex(actor->mutex); SDL_DestroyMutex(actor->mutex); SDL_UnlockMutex(actor->msg_mutex); SDL_DestroyMutex(actor->msg_mutex); SDL_UnlockMutex(actor->turn); SDL_DestroyMutex(actor->turn); free(actor); if (actor == root_cell) exit(0); // exit_handler(); } static Uint32 actor_remove_cb(cell_rt *actor, Uint32 id, Uint32 interval) { if (actor->need_stop) { actor_free(actor); return 0; } if (JS_IsUndefined(actor->unneeded)) actor_free(actor); else { SDL_LockMutex(actor->mutex); JSValue ret = JS_Call(actor->context, actor->unneeded, JS_UNDEFINED, 0, NULL); uncaught_exception(actor->context, ret); SDL_UnlockMutex(actor->mutex); actor_free(actor); } return 0; } void js_dofree(JSRuntime *rt, void *opaque, void *ptr) { js_free_rt(rt, ptr); } SDL_TLSID prosperon_id; #ifdef TRACY_ENABLE static size_t js_tracy_malloc_usable_size(const void *ptr) { #if defined(__APPLE__) return malloc_size(ptr); #elif defined(_WIN32) return _msize((void *)ptr); #elif defined(EMSCRIPTEN) return 0; #elif defined(__linux__) || defined(__GLIBC__) return malloc_usable_size((void *)ptr); #else return malloc_usable_size((void *)ptr); #endif } static void *js_tracy_malloc(JSMallocState *s, size_t size) { void *ptr; assert(size != 0); if (unlikely(s->malloc_size + size > s->malloc_limit)) return NULL; ptr = malloc(size); if (!ptr) return NULL; s->malloc_count++; s->malloc_size += js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD; TracyCAllocN(ptr, js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs"); return ptr; } static void js_tracy_free(JSMallocState *s, void *ptr) { if (!ptr) return; s->malloc_count--; s->malloc_size -= js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD; TracyCFreeN(ptr, "quickjs"); free(ptr); } static void *js_tracy_realloc(JSMallocState *s, void *ptr, size_t size) { size_t old_size; if (!ptr) return size ? js_tracy_malloc(s, size) : NULL; old_size = js_tracy_malloc_usable_size(ptr); if (!size) { s->malloc_count--; s->malloc_size -= old_size + MALLOC_OVERHEAD; TracyCFreeN(ptr, "quickjs"); free(ptr); return NULL; } if (s->malloc_size + size - old_size > s->malloc_limit) return NULL; TracyCFreeN(ptr, "quickjs"); ptr = realloc(ptr, size); if (!ptr) return NULL; s->malloc_size += js_tracy_malloc_usable_size(ptr) - old_size; TracyCAllocN(ptr, js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs"); return ptr; } static const JSMallocFunctions tracy_malloc_funcs = { js_tracy_malloc, js_tracy_free, js_tracy_realloc, js_tracy_malloc_usable_size }; #endif static void free_zip(void) { free(zip_buffer_global); zip_buffer_global = NULL; } int prosperon_mount_core(void) { size_t size; char exe_path[PATH_MAX]; // Get the full path of the executable const char *base_dir = PHYSFS_getBaseDir(); if (base_dir) { snprintf(exe_path, sizeof(exe_path), "%s%s", base_dir, PHYSFS_getDirSeparator()); // Extract just the executable name from argv[0] const char *exe_name = strrchr(prosperon, '/'); if (!exe_name) exe_name = strrchr(prosperon, '\\'); if (exe_name) exe_name++; else exe_name = prosperon; strncat(exe_path, exe_name, sizeof(exe_path) - strlen(exe_path) - 1); } else { strncpy(exe_path, prosperon, sizeof(exe_path) - 1); exe_path[sizeof(exe_path) - 1] = '\0'; } FILE *f = fopen(exe_path, "rb"); if (!f) return perror("fopen"), 0; if (fseek(f, 0, SEEK_END) != 0) return perror("fseek"), fclose(f), 0; size = ftell(f); if (size < 0) return perror("ftell"), fclose(f), 0; zip_buffer_global = malloc(size); if (!zip_buffer_global) return perror("malloc"), fclose(f), 0; rewind(f); if (fread(zip_buffer_global, 1, size, f) != size) { perror("fread"); free(zip_buffer_global); fclose(f); return 0; } fclose(f); long max_comment_len = 0xFFFF; long eocd_search_start = (size > max_comment_len + 22) ? size - (max_comment_len + 22) : 0; long eocd_pos = -1; for (long i = size - 22; i >= eocd_search_start; i--) if (zip_buffer_global[i] == 'P' && zip_buffer_global[i + 1] == 'K' && zip_buffer_global[i + 2] == 0x05 && zip_buffer_global[i + 3] == 0x06) { eocd_pos = i; break; } if (eocd_pos < 0) { free(zip_buffer_global); return 0; } uint16_t comment_length = zip_buffer_global[eocd_pos + 20] | (zip_buffer_global[eocd_pos + 21] << 8); int eocd_size = 22 + comment_length; uint32_t cd_size = zip_buffer_global[eocd_pos + 12] | (zip_buffer_global[eocd_pos + 13] << 8) | (zip_buffer_global[eocd_pos + 14] << 16) | (zip_buffer_global[eocd_pos + 15] << 24); uint32_t cd_offset_rel = zip_buffer_global[eocd_pos + 16] | (zip_buffer_global[eocd_pos + 17] << 8) | (zip_buffer_global[eocd_pos + 18] << 16) | (zip_buffer_global[eocd_pos + 19] << 24); uint32_t appended_zip_size = cd_offset_rel + cd_size + eocd_size; long zip_offset = size - appended_zip_size; if (zip_offset < 0 || zip_offset >= size) return fprintf(stderr, "Invalid zip offset: %ld\n", zip_offset), free(zip_buffer_global), 0; int ret = PHYSFS_mountMemory(zip_buffer_global + zip_offset, appended_zip_size, free_zip, "core.zip", NULL, 0); if (!ret) { printf("COULD NOT MOUNT! Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); } return ret; } cell_rt *create_actor(void *wota) { cell_rt *actor = calloc(sizeof(*actor), 1); actor->init_wota = wota; actor->idx_buffer = JS_UNDEFINED; actor->message_handle = JS_UNDEFINED; actor->unneeded = JS_UNDEFINED; actor->on_exception = JS_UNDEFINED; arrsetcap(actor->messages, 5); arrsetcap(actor->events, 5); actor->mutex = SDL_CreateMutex(); /* Protects JSContext + state */ actor->msg_mutex = SDL_CreateMutex(); /* Mailbox queue lock */ actor->turn = SDL_CreateMutex(); /* Lock actor->mutex while initializing JS runtime. */ SDL_LockMutex(actor->mutex); script_startup(actor); SDL_UnlockMutex(actor->mutex); set_actor_state(actor); return actor; } cell_rt *get_actor(char *id) { SDL_LockMutex(actors_mutex); int idx = shgeti(actors, id); if (idx == -1) { SDL_UnlockMutex(actors_mutex); return NULL; } cell_rt *actor = actors[idx].value; SDL_UnlockMutex(actors_mutex); return actor; } const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar) { SDL_LockMutex(actors_mutex); if (shgeti(actors, id) != -1) { SDL_UnlockMutex(actors_mutex); return "Actor with given ID already exists."; } actor->main_thread_only = mainthread; actor->id = strdup(id); actor->ar_secs = ar; shput(actors, id, actor); SDL_UnlockMutex(actors_mutex); return NULL; } const char *send_message(const char *id, void *msg) { cell_rt *target = get_actor(id); if (!target) { free(msg); return "Could not get actor from id."; } SDL_LockMutex(target->msg_mutex); arrput(target->messages, msg); if (target->ar) { SDL_RemoveTimer(target->ar); target->ar = 0; } SDL_UnlockMutex(target->msg_mutex); set_actor_state(target); return NULL; } /* set_actor_state should check if either messages or events are pending. */ void set_actor_state(cell_rt *actor) { SDL_LockMutex(actor->msg_mutex); if (actor->need_stop) { if (!actor->ar) actor->ar = SDL_AddTimerNS(0, actor_remove_cb, actor); SDL_UnlockMutex(actor->msg_mutex); return; } int has_messages = arrlen(actor->messages); int has_events = arrlen(actor->events); int has_upcoming = hmlen(actor->timers); if (actor->state == ACTOR_RUNNING) actor->state = ACTOR_IDLE; switch(actor->state) { case ACTOR_IDLE: if (has_messages || has_events) { actor->state = ACTOR_READY; SDL_LockMutex(queue_mutex); if (actor->main_thread_only) { arrput(main_ready_queue, actor); SDL_Event event; event.type = queue_event; SDL_PushEvent(&event); } else { SDL_SignalCondition(queue_cond); arrput(ready_queue, actor); } SDL_UnlockMutex(queue_mutex); goto END; } break; case ACTOR_READY: if (!has_messages && !has_events) { actor->state = ACTOR_IDLE; goto END; } break; } END: if (actor->state == ACTOR_IDLE && !actor->ar && !has_upcoming) actor->ar = SDL_AddTimerNS(SDL_SECONDS_TO_NS(actor->ar_secs), actor_remove_cb, actor); SDL_UnlockMutex(actor->msg_mutex); } void actor_turn(cell_rt *actor, int greedy) { SDL_LockMutex(actor->turn); #ifdef TRACY_ENABLE if (tracy_profiling_enabled) TracyCFiberEnter(actor->name); #endif SDL_LockMutex(actor->msg_mutex); actor->state = ACTOR_RUNNING; int msgs = 0; int events = 0; int need_stop = 0; JSValue result; msgs = arrlen(actor->messages); events = arrlen(actor->events); need_stop = actor->need_stop; SDL_UnlockMutex(actor->msg_mutex); if (need_stop) goto KILL; if (!msgs && !events) goto END; if (!msgs) goto EVENT; MESSAGE: SDL_LockMutex(actor->msg_mutex); void *msg = actor->messages[0]; arrdel(actor->messages,0); SDL_UnlockMutex(actor->msg_mutex); SDL_LockMutex(actor->mutex); JSValue arg = wota2value(actor->context, msg); free(msg); result = JS_Call(actor->context, actor->message_handle, JS_UNDEFINED, 1, &arg); uncaught_exception(actor->context, result); JS_FreeValue(actor->context,arg); SDL_UnlockMutex(actor->mutex); if (!greedy) goto END; SDL_LockMutex(actor->msg_mutex); need_stop = actor->need_stop; msgs = arrlen(actor->messages); events = arrlen(actor->events); SDL_UnlockMutex(actor->msg_mutex); if (need_stop) goto KILL; if (!msgs && !events) goto END; SDL_LockMutex(queue_mutex); int queuen = arrlen(ready_queue); SDL_UnlockMutex(queue_mutex); if (queuen != 0) goto END; if (msgs) goto MESSAGE; EVENT: SDL_LockMutex(actor->msg_mutex); JSValue event = actor->events[0]; arrdel(actor->events, 0); SDL_UnlockMutex(actor->msg_mutex); SDL_LockMutex(actor->mutex); result = JS_Call(actor->context, event, JS_UNDEFINED, 0, NULL); uncaught_exception(actor->context, result); JS_FreeValue(actor->context, event); SDL_UnlockMutex(actor->mutex); if (!greedy) goto END; SDL_LockMutex(actor->msg_mutex); events = arrlen(actor->events); need_stop = actor->need_stop; SDL_UnlockMutex(actor->msg_mutex); if (need_stop) goto KILL; if (!events) goto END; SDL_LockMutex(queue_mutex); int n = arrlen(ready_queue); SDL_UnlockMutex(queue_mutex); if (n != 0) goto END; goto EVENT; END: #ifdef TRACY_ENABLE if (tracy_profiling_enabled) TracyCFiberLeave(actor->name); #endif SDL_UnlockMutex(actor->turn); set_actor_state(actor); return; KILL: #ifdef TRACY_ENABLE if (tracy_profiling_enabled) TracyCFiberLeave(actor->name); #endif SDL_UnlockMutex(actor->turn); actor_free(actor); } /* Timer callback adds an event to the queue under evt_mutex. */ Uint32 actor_timer_cb(cell_rt *actor, SDL_TimerID id, Uint32 interval) { SDL_LockMutex(actor->msg_mutex); int idx = hmgeti(actor->timers, id); if (idx == -1) goto END; JSValue cb = actor->timers[idx].value; hmdel(actor->timers, id); arrput(actor->events, cb); set_actor_state(actor); END: SDL_UnlockMutex(actor->msg_mutex); return 0; } /* JS function that schedules a timer. */ JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv) { if (!JS_IsFunction(js, argv[0])) return JS_ThrowReferenceError(js, "Argument must be a function."); cell_rt *actor = JS_GetContextOpaque(js); double seconds; JS_ToFloat64(js, &seconds, argv[1]); if (seconds <= 0) { SDL_LockMutex(actor->msg_mutex); JSValue cb = JS_DupValue(js, argv[0]); arrput(actor->events, cb); SDL_UnlockMutex(actor->msg_mutex); return JS_NewInt32(js, -1); } Uint64 ns = seconds * SDL_NS_PER_SECOND; Uint32 id = SDL_AddTimerNS(ns, actor_timer_cb, actor); SDL_LockMutex(actor->msg_mutex); JSValue cb = JS_DupValue(js, argv[0]); hmput(actor->timers, id, cb); if (actor->ar) { SDL_RemoveTimer(actor->ar); actor->ar = 0; } SDL_UnlockMutex(actor->msg_mutex); return JS_NewUint32(js, id); } JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv) { cell_rt *actor = JS_GetContextOpaque(js); Uint32 timer_id; JS_ToUint32(js, &timer_id, argv[0]); if (timer_id == -1) return JS_UNDEFINED; SDL_RemoveTimer(timer_id); JSValue cb = JS_UNDEFINED; SDL_LockMutex(actor->msg_mutex); int id = hmgeti(actor->timers, timer_id); if (id != -1) { cb = actor->timers[id].value; hmdel(actor->timers, timer_id); } SDL_UnlockMutex(actor->msg_mutex); JS_FreeValue(js,cb); return JS_UNDEFINED; } // Wrapper struct to keep the array pointer stable typedef struct { TracyCZoneCtx *arr; // stb_ds dynamic array } tracy_stack_t; // Global TLS ID for the Tracy stack static SDL_TLSID tracy_stack_id = {0}; // Cleanup function for the wrapper struct static void tracy_cleanup_stack(void *value) { tracy_stack_t *stack = value; if (stack) { arrfree(stack->arr); free(stack); } } // Get or initialize the thread-local Tracy stack static tracy_stack_t *get_tracy_stack(void) { tracy_stack_t *stack = SDL_GetTLS(&tracy_stack_id); if (!stack) { stack = malloc(sizeof(tracy_stack_t)); stack->arr = NULL; // stb_ds starts with NULL arrsetcap(stack->arr, 5); // Initial capacity SDL_SetTLS(&tracy_stack_id, stack, tracy_cleanup_stack); } return stack; } void tracy_call_hook(JSContext *js, JSValue fn) { if (!tracy_profiling_enabled) return; tracy_stack_t *stack = get_tracy_stack(); js_debug debug; js_debug_info(js, fn, &debug); uint64_t srcloc = ___tracy_alloc_srcloc(debug.line, debug.filename, strlen(debug.filename), debug.name, strlen(debug.name), debug.unique); arrput(stack->arr, ___tracy_emit_zone_begin_alloc(srcloc, 1)); free_js_debug_info(js, &debug); } void tracy_end_hook(JSContext *js, JSValue fn) { if (!tracy_profiling_enabled) return; tracy_stack_t *stack = get_tracy_stack(); if (arrlen(stack->arr) > 0) ___tracy_emit_zone_end(arrpop(stack->arr)); } void actor_disrupt(cell_rt *crt) { crt->disrupt = 1; crt->need_stop = 1; } static int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt) { if (crt->disrupt) return 1; return 0; } void script_startup(cell_rt *prt) { JSRuntime *rt; #ifdef TRACY_ENABLE if (tracy_profiling_enabled) rt = JS_NewRuntime2(&tracy_malloc_funcs, NULL); else rt = JS_NewRuntime(); #else rt = JS_NewRuntime(); #endif JSContext *js = JS_NewContextRaw(rt); JS_SetInterruptHandler(rt, actor_interrupt_cb, prt); #ifdef TRACY_ENABLE if (tracy_profiling_enabled) { js_debug_sethook(js, tracy_call_hook, JS_HOOK_CALL); js_debug_sethook(js, tracy_end_hook, JS_HOOK_RET); } #endif JS_AddIntrinsicBaseObjects(js); JS_AddIntrinsicEval(js); JS_AddIntrinsicRegExp(js); JS_AddIntrinsicJSON(js); JS_AddIntrinsicMapSet(js); JS_AddIntrinsicProxy(js); JS_SetContextOpaque(js, prt); prt->context = js; ffi_load(js); PHYSFS_File *eng = PHYSFS_openRead(ENGINE); if (!eng) { printf("Could not open file! %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); return; } PHYSFS_Stat stat; PHYSFS_stat(ENGINE, &stat); char *data = malloc(stat.filesize+1); PHYSFS_readBytes(eng, data, stat.filesize); PHYSFS_close(eng); data[stat.filesize] = 0; /* Called with actor->mutex locked by create_actor(). */ JSValue v = JS_Eval(js, data, (size_t)stat.filesize, ENGINE, JS_EVAL_FLAG_STRICT); uncaught_exception(js, v); } int uncaught_exception(JSContext *js, JSValue v) { cell_rt *rt = JS_GetContextOpaque(js); SDL_LockMutex(rt->mutex); JS_FreeValue(js,v); if (!JS_HasException(js)) { JS_FreeValue(js,v); SDL_UnlockMutex(rt->mutex); return 1; } JSValue exp = JS_GetException(js); JSValue ret = JS_Call(js, rt->on_exception, JS_UNDEFINED, 1, &exp); JS_FreeValue(js,ret); JS_FreeValue(js, exp); SDL_UnlockMutex(rt->mutex); return 0; } void script_evalf(JSContext *js, const char *format, ...) { cell_rt *rt = JS_GetContextOpaque(js); SDL_LockMutex(rt->mutex); va_list args; va_start(args, format); int len = vsnprintf(NULL, 0, format, args); va_end(args); char *eval = malloc(len + 1); va_start(args, format); vsnprintf(eval, len + 1, format, args); va_end(args); JSValue obj = JS_Eval(js, eval, len, "C eval", JS_EVAL_FLAG_STRICT); free(eval); uncaught_exception(js, obj); SDL_UnlockMutex(rt->mutex); } static int crank_actor(void *data) { while (!SDL_GetAtomicInt(&engine_shutdown)) { SDL_LockMutex(queue_mutex); cell_rt *actor = NULL; if (arrlen(ready_queue) > 0) { actor = ready_queue[0]; arrdel(ready_queue, 0); } else { SDL_WaitCondition(queue_cond, queue_mutex); SDL_UnlockMutex(queue_mutex); continue; } SDL_UnlockMutex(queue_mutex); if (actor) actor_turn(actor, 1); } return 0; } static void signal_handler(int sig) { const char *str = NULL; switch (sig) { case SIGABRT: str = "SIGABRT"; break; case SIGFPE: str = "SIGFPE"; break; case SIGILL: str = "SIGILL"; break; case SIGINT: str = "SIGINT"; break; case SIGSEGV: str = "SIGSEGV"; break; case SIGTERM: str = "SIGTERM"; break; } if (!str) return; exit_handler(); } // Assume these helper functions exist or need to be implemented const char* event_type_to_string(Uint32 event_type) { switch (event_type) { // Application events case SDL_EVENT_QUIT: return "quit"; case SDL_EVENT_TERMINATING: return "terminating"; case SDL_EVENT_LOW_MEMORY: return "low_memory"; case SDL_EVENT_WILL_ENTER_BACKGROUND: return "will_enter_background"; case SDL_EVENT_DID_ENTER_BACKGROUND: return "did_enter_background"; case SDL_EVENT_WILL_ENTER_FOREGROUND: return "will_enter_foreground"; case SDL_EVENT_DID_ENTER_FOREGROUND: return "did_enter_foreground"; case SDL_EVENT_LOCALE_CHANGED: return "locale_changed"; case SDL_EVENT_SYSTEM_THEME_CHANGED: return "system_theme_changed"; // Display events case SDL_EVENT_DISPLAY_ORIENTATION: return "display_orientation"; case SDL_EVENT_DISPLAY_ADDED: return "display_added"; case SDL_EVENT_DISPLAY_REMOVED: return "display_removed"; case SDL_EVENT_DISPLAY_MOVED: return "display_moved"; case SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED: return "display_desktop_mode_changed"; case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED: return "display_current_mode_changed"; case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED: return "display_content_scale_changed"; // Window events case SDL_EVENT_WINDOW_SHOWN: return "window_shown"; case SDL_EVENT_WINDOW_HIDDEN: return "window_hidden"; case SDL_EVENT_WINDOW_EXPOSED: return "window_exposed"; case SDL_EVENT_WINDOW_MOVED: return "window_moved"; case SDL_EVENT_WINDOW_RESIZED: return "window_resized"; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: return "window_pixel_size_changed"; case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: return "window_metal_view_resized"; case SDL_EVENT_WINDOW_MINIMIZED: return "window_minimized"; case SDL_EVENT_WINDOW_MAXIMIZED: return "window_maximized"; case SDL_EVENT_WINDOW_RESTORED: return "window_restored"; case SDL_EVENT_WINDOW_MOUSE_ENTER: return "window_mouse_enter"; case SDL_EVENT_WINDOW_MOUSE_LEAVE: return "window_mouse_leave"; case SDL_EVENT_WINDOW_FOCUS_GAINED: return "window_focus_gained"; case SDL_EVENT_WINDOW_FOCUS_LOST: return "window_focus_lost"; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: return "window_close_requested"; case SDL_EVENT_WINDOW_HIT_TEST: return "window_hit_test"; case SDL_EVENT_WINDOW_ICCPROF_CHANGED: return "window_iccprof_changed"; case SDL_EVENT_WINDOW_DISPLAY_CHANGED: return "window_display_changed"; case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: return "window_display_scale_changed"; case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: return "window_safe_area_changed"; case SDL_EVENT_WINDOW_OCCLUDED: return "window_occluded"; case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: return "window_enter_fullscreen"; case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: return "window_leave_fullscreen"; case SDL_EVENT_WINDOW_DESTROYED: return "window_destroyed"; case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: return "window_hdr_state_changed"; // Keyboard events case SDL_EVENT_KEY_DOWN: return "key_down"; case SDL_EVENT_KEY_UP: return "key_up"; case SDL_EVENT_TEXT_EDITING: return "text_editing"; case SDL_EVENT_TEXT_INPUT: return "text_input"; case SDL_EVENT_KEYMAP_CHANGED: return "keymap_changed"; case SDL_EVENT_KEYBOARD_ADDED: return "keyboard_added"; case SDL_EVENT_KEYBOARD_REMOVED: return "keyboard_removed"; case SDL_EVENT_TEXT_EDITING_CANDIDATES: return "text_editing_candidates"; // Mouse events case SDL_EVENT_MOUSE_MOTION: return "mouse_motion"; case SDL_EVENT_MOUSE_BUTTON_DOWN: return "mouse_button_down"; case SDL_EVENT_MOUSE_BUTTON_UP: return "mouse_button_up"; case SDL_EVENT_MOUSE_WHEEL: return "mouse_wheel"; case SDL_EVENT_MOUSE_ADDED: return "mouse_added"; case SDL_EVENT_MOUSE_REMOVED: return "mouse_removed"; // Joystick events case SDL_EVENT_JOYSTICK_AXIS_MOTION: return "joystick_axis_motion"; case SDL_EVENT_JOYSTICK_BALL_MOTION: return "joystick_ball_motion"; case SDL_EVENT_JOYSTICK_HAT_MOTION: return "joystick_hat_motion"; case SDL_EVENT_JOYSTICK_BUTTON_DOWN: return "joystick_button_down"; case SDL_EVENT_JOYSTICK_BUTTON_UP: return "joystick_button_up"; case SDL_EVENT_JOYSTICK_ADDED: return "joystick_added"; case SDL_EVENT_JOYSTICK_REMOVED: return "joystick_removed"; case SDL_EVENT_JOYSTICK_BATTERY_UPDATED: return "joystick_battery_updated"; case SDL_EVENT_JOYSTICK_UPDATE_COMPLETE: return "joystick_update_complete"; // Gamepad events case SDL_EVENT_GAMEPAD_AXIS_MOTION: return "gamepad_axis_motion"; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: return "gamepad_button_down"; case SDL_EVENT_GAMEPAD_BUTTON_UP: return "gamepad_button_up"; case SDL_EVENT_GAMEPAD_ADDED: return "gamepad_added"; case SDL_EVENT_GAMEPAD_REMOVED: return "gamepad_removed"; case SDL_EVENT_GAMEPAD_REMAPPED: return "gamepad_remapped"; case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: return "gamepad_touchpad_down"; case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: return "gamepad_touchpad_motion"; case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: return "gamepad_touchpad_up"; case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: return "gamepad_sensor_update"; case SDL_EVENT_GAMEPAD_UPDATE_COMPLETE: return "gamepad_update_complete"; case SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED: return "gamepad_steam_handle_updated"; // Touch events case SDL_EVENT_FINGER_DOWN: return "finger_down"; case SDL_EVENT_FINGER_UP: return "finger_up"; case SDL_EVENT_FINGER_MOTION: return "finger_motion"; // Clipboard events case SDL_EVENT_CLIPBOARD_UPDATE: return "clipboard_update"; // Drag and drop events case SDL_EVENT_DROP_FILE: return "drop_file"; case SDL_EVENT_DROP_TEXT: return "drop_text"; case SDL_EVENT_DROP_BEGIN: return "drop_begin"; case SDL_EVENT_DROP_COMPLETE: return "drop_complete"; case SDL_EVENT_DROP_POSITION: return "drop_position"; // Audio device events case SDL_EVENT_AUDIO_DEVICE_ADDED: return "audio_device_added"; case SDL_EVENT_AUDIO_DEVICE_REMOVED: return "audio_device_removed"; case SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED: return "audio_device_format_changed"; // Sensor events case SDL_EVENT_SENSOR_UPDATE: return "sensor_update"; // Pen events case SDL_EVENT_PEN_PROXIMITY_IN: return "pen_proximity_in"; case SDL_EVENT_PEN_PROXIMITY_OUT: return "pen_proximity_out"; case SDL_EVENT_PEN_DOWN: return "pen_down"; case SDL_EVENT_PEN_UP: return "pen_up"; case SDL_EVENT_PEN_BUTTON_DOWN: return "pen_button_down"; case SDL_EVENT_PEN_BUTTON_UP: return "pen_button_up"; case SDL_EVENT_PEN_MOTION: return "pen_motion"; case SDL_EVENT_PEN_AXIS: return "pen_axis"; // Camera events case SDL_EVENT_CAMERA_DEVICE_ADDED: return "camera_device_added"; case SDL_EVENT_CAMERA_DEVICE_REMOVED: return "camera_device_removed"; case SDL_EVENT_CAMERA_DEVICE_APPROVED: return "camera_device_approved"; case SDL_EVENT_CAMERA_DEVICE_DENIED: return "camera_device_denied"; // Render events case SDL_EVENT_RENDER_TARGETS_RESET: return "render_targets_reset"; case SDL_EVENT_RENDER_DEVICE_RESET: return "render_device_reset"; case SDL_EVENT_RENDER_DEVICE_LOST: return "render_device_lost"; // User event (assuming it should be included) case SDL_EVENT_USER: return "user"; default: return "unknown"; } } const char* mouse_button_to_string(int mouse) { switch (mouse) { case SDL_BUTTON_LEFT: return "left"; case SDL_BUTTON_MIDDLE: return "middle"; case SDL_BUTTON_RIGHT: return "right"; case SDL_BUTTON_X1: return "x1"; case SDL_BUTTON_X2: return "x2"; default: return "left"; } } static void wota_write_vec2(WotaBuffer *wb, double x, double y) { // We'll store as WOTA_ARR of length 2, then two numbers wota_write_array(wb, 2); wota_write_number(wb, x); wota_write_number(wb, y); } static int event2wota_count_props(const SDL_Event *event) { // We always store at least "type" and "timestamp". int count = 2; switch (event->type) { case SDL_EVENT_AUDIO_DEVICE_ADDED: case SDL_EVENT_AUDIO_DEVICE_REMOVED: count += 2; // which, recording break; case SDL_EVENT_DISPLAY_ORIENTATION: case SDL_EVENT_DISPLAY_ADDED: case SDL_EVENT_DISPLAY_REMOVED: case SDL_EVENT_DISPLAY_MOVED: case SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED: case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED: case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED: count += 3; // which, data1, data2 break; case SDL_EVENT_MOUSE_MOTION: count += 5; break; case SDL_EVENT_MOUSE_WHEEL: // window, which, scroll, mouse => 4 extra count += 4; break; case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_DOWN: // window, which, down, button, clicks, mouse => 6 extra count += 6; break; case SDL_EVENT_SENSOR_UPDATE: // which, sensor_timestamp => 2 extra count += 2; break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: // window, which, down, repeat, key, scancode, mod => 7 extra count += 7; break; case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_UP: // touch, finger, pos, d_pos, pressure, window => 6 extra count += 6; break; case SDL_EVENT_DROP_BEGIN: case SDL_EVENT_DROP_FILE: case SDL_EVENT_DROP_TEXT: case SDL_EVENT_DROP_COMPLETE: case SDL_EVENT_DROP_POSITION: // window, pos, data, source => 4 extra count += 4; break; case SDL_EVENT_TEXT_INPUT: // window, text, mod => 3 extra count += 3; break; case SDL_EVENT_CAMERA_DEVICE_APPROVED: case SDL_EVENT_CAMERA_DEVICE_REMOVED: case SDL_EVENT_CAMERA_DEVICE_ADDED: case SDL_EVENT_CAMERA_DEVICE_DENIED: // which => 1 extra count += 1; break; case SDL_EVENT_CLIPBOARD_UPDATE: // owner => 1 extra count += 1; break; /* Window events (just group them all together) */ case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_EXPOSED: case SDL_EVENT_WINDOW_MOVED: case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: case SDL_EVENT_WINDOW_MINIMIZED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: case SDL_EVENT_WINDOW_MOUSE_ENTER: case SDL_EVENT_WINDOW_MOUSE_LEAVE: case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_HIT_TEST: case SDL_EVENT_WINDOW_ICCPROF_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: case SDL_EVENT_WINDOW_OCCLUDED: case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: case SDL_EVENT_WINDOW_DESTROYED: case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: // which, data1, data2 => 3 extra count += 3; break; case SDL_EVENT_JOYSTICK_ADDED: case SDL_EVENT_JOYSTICK_REMOVED: case SDL_EVENT_JOYSTICK_UPDATE_COMPLETE: // which => 1 extra count += 1; break; case SDL_EVENT_JOYSTICK_AXIS_MOTION: // which, axis, value => 3 extra count += 3; break; case SDL_EVENT_JOYSTICK_BALL_MOTION: // which, ball, rel => 3 extra count += 3; break; case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: // which, button, down => 3 extra count += 3; break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: case SDL_EVENT_GAMEPAD_REMAPPED: case SDL_EVENT_GAMEPAD_UPDATE_COMPLETE: case SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED: // which => 1 extra count += 1; break; case SDL_EVENT_GAMEPAD_AXIS_MOTION: // which, axis, value => 3 extra count += 3; break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: // which, button, down => 3 extra count += 3; break; case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: // which, touchpad, finger, pos, pressure => 5 extra count += 5; break; case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: // which, sensor, sensor_timestamp => 3 extra count += 3; break; case SDL_EVENT_USER: // cb => 1 extra count += 1; break; } return count; } static void event2wota_write(WotaBuffer *wb, const SDL_Event *e, int c) { wota_write_record(wb, (unsigned long long)c); wota_write_text(wb, "type"); wota_write_text(wb, event_type_to_string(e->type)); wota_write_text(wb, "timestamp"); wota_write_number(wb, (double)e->common.timestamp); switch(e->type) { case SDL_EVENT_AUDIO_DEVICE_ADDED: case SDL_EVENT_AUDIO_DEVICE_REMOVED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->adevice.which); wota_write_text(wb, "recording"); wota_write_sym(wb, e->adevice.recording ? WOTA_TRUE : WOTA_FALSE); break; case SDL_EVENT_DISPLAY_ORIENTATION: case SDL_EVENT_DISPLAY_ADDED: case SDL_EVENT_DISPLAY_REMOVED: case SDL_EVENT_DISPLAY_MOVED: case SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED: case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED: case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->display.displayID); wota_write_text(wb, "data1"); wota_write_number(wb, (double)e->display.data1); wota_write_text(wb, "data2"); wota_write_number(wb, (double)e->display.data2); break; case SDL_EVENT_MOUSE_MOTION: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->motion.windowID); wota_write_text(wb, "which"); wota_write_number(wb, (double)e->motion.which); wota_write_text(wb, "state"); wota_write_number(wb, (double)e->motion.state); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->motion.x, (double)e->motion.y); wota_write_text(wb, "d_pos"); wota_write_vec2(wb, (double)e->motion.xrel, (double)e->motion.yrel); break; case SDL_EVENT_MOUSE_WHEEL: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->wheel.windowID); wota_write_text(wb, "which"); wota_write_number(wb, (double)e->wheel.which); wota_write_text(wb, "scroll"); wota_write_vec2(wb, (double)e->wheel.x, (double)e->wheel.y); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->wheel.mouse_x, (double)e->wheel.mouse_y); break; case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_DOWN: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->button.windowID); wota_write_text(wb, "which"); wota_write_number(wb, (double)e->button.which); wota_write_text(wb, "down"); wota_write_sym(wb, e->button.down ? WOTA_TRUE : WOTA_FALSE); wota_write_text(wb, "button"); wota_write_text(wb, mouse_button_to_string(e->button.button)); wota_write_text(wb, "clicks"); wota_write_number(wb, (double)e->button.clicks); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->button.x, (double)e->button.y); break; case SDL_EVENT_SENSOR_UPDATE: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->sensor.which); wota_write_text(wb, "sensor_timestamp"); wota_write_number(wb, (double)e->sensor.sensor_timestamp); break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->key.windowID); wota_write_text(wb, "which"); wota_write_number(wb, (double)e->key.which); wota_write_text(wb, "down"); wota_write_sym(wb, e->key.down ? WOTA_TRUE : WOTA_FALSE); wota_write_text(wb, "repeat"); wota_write_sym(wb, e->key.repeat ? WOTA_TRUE : WOTA_FALSE); wota_write_text(wb, "key"); wota_write_number(wb, (double)e->key.key); wota_write_text(wb, "scancode"); wota_write_number(wb, (double)e->key.scancode); wota_write_text(wb, "mod"); wota_write_number(wb, 0); break; case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_UP: wota_write_text(wb, "touch"); wota_write_number(wb, (double)e->tfinger.touchID); wota_write_text(wb, "finger"); wota_write_number(wb, (double)e->tfinger.fingerID); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->tfinger.x, (double)e->tfinger.y); wota_write_text(wb, "d_pos"); wota_write_vec2(wb, (double)e->tfinger.dx, (double)e->tfinger.dy); wota_write_text(wb, "pressure"); wota_write_number(wb, (double)e->tfinger.pressure); wota_write_text(wb, "window"); wota_write_number(wb, (double)e->key.windowID); break; case SDL_EVENT_DROP_BEGIN: case SDL_EVENT_DROP_FILE: case SDL_EVENT_DROP_TEXT: case SDL_EVENT_DROP_COMPLETE: case SDL_EVENT_DROP_POSITION: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->drop.windowID); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->drop.x, (double)e->drop.y); wota_write_text(wb, "data"); wota_write_text(wb, e->drop.data ? e->drop.data : ""); wota_write_text(wb, "source"); wota_write_text(wb, e->drop.source ? e->drop.source : ""); break; case SDL_EVENT_TEXT_INPUT: wota_write_text(wb, "window"); wota_write_number(wb, (double)e->text.windowID); wota_write_text(wb, "text"); wota_write_text(wb, e->text.text); wota_write_text(wb, "mod"); wota_write_number(wb, 0); break; case SDL_EVENT_CAMERA_DEVICE_APPROVED: case SDL_EVENT_CAMERA_DEVICE_REMOVED: case SDL_EVENT_CAMERA_DEVICE_ADDED: case SDL_EVENT_CAMERA_DEVICE_DENIED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->cdevice.which); break; case SDL_EVENT_CLIPBOARD_UPDATE: wota_write_text(wb, "owner"); wota_write_sym(wb, e->clipboard.owner ? WOTA_TRUE : WOTA_FALSE); break; case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_EXPOSED: case SDL_EVENT_WINDOW_MOVED: case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: case SDL_EVENT_WINDOW_MINIMIZED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: case SDL_EVENT_WINDOW_MOUSE_ENTER: case SDL_EVENT_WINDOW_MOUSE_LEAVE: case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_HIT_TEST: case SDL_EVENT_WINDOW_ICCPROF_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: case SDL_EVENT_WINDOW_OCCLUDED: case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: case SDL_EVENT_WINDOW_DESTROYED: case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->window.windowID); wota_write_text(wb, "data1"); wota_write_number(wb, (double)e->window.data1); wota_write_text(wb, "data2"); wota_write_number(wb, (double)e->window.data2); break; case SDL_EVENT_JOYSTICK_ADDED: case SDL_EVENT_JOYSTICK_REMOVED: case SDL_EVENT_JOYSTICK_UPDATE_COMPLETE: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->jdevice.which); break; case SDL_EVENT_JOYSTICK_AXIS_MOTION: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->jaxis.which); wota_write_text(wb, "axis"); wota_write_number(wb, (double)e->jaxis.axis); wota_write_text(wb, "value"); wota_write_number(wb, (double)e->jaxis.value); break; case SDL_EVENT_JOYSTICK_BALL_MOTION: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->jball.which); wota_write_text(wb, "ball"); wota_write_number(wb, (double)e->jball.ball); wota_write_text(wb, "rel"); wota_write_vec2(wb, (double)e->jball.xrel, (double)e->jball.yrel); break; case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->jbutton.which); wota_write_text(wb, "button"); wota_write_number(wb, (double)e->jbutton.button); wota_write_text(wb, "down"); wota_write_sym(wb, e->jbutton.down ? WOTA_TRUE : WOTA_FALSE); break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: case SDL_EVENT_GAMEPAD_REMAPPED: case SDL_EVENT_GAMEPAD_UPDATE_COMPLETE: case SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->gdevice.which); break; case SDL_EVENT_GAMEPAD_AXIS_MOTION: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->gaxis.which); wota_write_text(wb, "axis"); wota_write_number(wb, (double)e->gaxis.axis); wota_write_text(wb, "value"); wota_write_number(wb, (double)e->gaxis.value); break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->gbutton.which); wota_write_text(wb, "button"); wota_write_number(wb, (double)e->gbutton.button); wota_write_text(wb, "down"); wota_write_sym(wb, e->gbutton.down ? WOTA_TRUE : WOTA_FALSE); break; case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->gtouchpad.which); wota_write_text(wb, "touchpad"); wota_write_number(wb, (double)e->gtouchpad.touchpad); wota_write_text(wb, "finger"); wota_write_number(wb, (double)e->gtouchpad.finger); wota_write_text(wb, "pos"); wota_write_vec2(wb, (double)e->gtouchpad.x, (double)e->gtouchpad.y); wota_write_text(wb, "pressure"); wota_write_number(wb, (double)e->gtouchpad.pressure); break; case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->gsensor.which); wota_write_text(wb, "sensor"); wota_write_number(wb, (double)e->gsensor.sensor); wota_write_text(wb, "sensor_timestamp"); wota_write_number(wb, (double)e->gsensor.sensor_timestamp); break; case SDL_EVENT_USER: wota_write_text(wb, "cb"); wota_write_number(wb, (double)(uintptr_t)e->user.data1); break; } } static WotaBuffer event2wota(const SDL_Event *event) { WotaBuffer wb; wota_buffer_init(&wb, 8); int n = event2wota_count_props(event); event2wota_write(&wb, event, n); return wb; } bool event_watch(void *data, SDL_Event *e) { if (e->type == queue_event) return true; SDL_LockMutex(event_watchers_mutex); int n_watchers = arrlen(event_watchers); if (n_watchers == 0) { SDL_UnlockMutex(event_watchers_mutex); return true; } /* Create a copy of watcher IDs while holding the lock */ char **watchers_copy = NULL; arrsetcap(watchers_copy, n_watchers); for (int i = 0; i < n_watchers; i++) { arrput(watchers_copy, strdup(event_watchers[i])); } SDL_UnlockMutex(event_watchers_mutex); WotaBuffer wb = event2wota(e); for (int i = 0; i < arrlen(watchers_copy); i++) { if (actor_exists(watchers_copy[i])) { const char *err = send_message(watchers_copy[i], wota_dup_data(&wb)); if (err) { /* Remove dead actor from watchers */ SDL_LockMutex(event_watchers_mutex); for (int j = 0; j < arrlen(event_watchers); j++) { if (strcmp(event_watchers[j], watchers_copy[i]) == 0) { free(event_watchers[j]); arrdel(event_watchers, j); break; } } SDL_UnlockMutex(event_watchers_mutex); } } else { /* Remove dead actor from watchers */ SDL_LockMutex(event_watchers_mutex); for (int j = 0; j < arrlen(event_watchers); j++) { if (strcmp(event_watchers[j], watchers_copy[i]) == 0) { free(event_watchers[j]); arrdel(event_watchers, j); break; } } SDL_UnlockMutex(event_watchers_mutex); } free(watchers_copy[i]); } arrfree(watchers_copy); wota_buffer_free(&wb); return true; } int main(int argc, char **argv) { int profile_enabled = 0; int script_start = 1; /* Check for --profile flag first */ if (argc > 1 && strcmp(argv[1], "--profile") == 0) { profile_enabled = 1; script_start = 2; } if (!SDL_Init(SDL_INIT_EVENTS)) { printf("CRITICAL ERROR: %s\n", SDL_GetError()); exit(1); } queue_event = SDL_RegisterEvents(1); #ifdef TRACY_ENABLE tracy_profiling_enabled = profile_enabled; #endif int cores = SDL_GetNumLogicalCPUCores(); prosperon = argv[0]; PHYSFS_init(argv[0]); /* Search for .cell directory up the tree */ char *search_dir = SDL_GetCurrentDirectory(); char *cell_parent_dir = NULL; struct stat st; while (search_dir && strlen(search_dir) > 1) { char test_path[PATH_MAX]; snprintf(test_path, sizeof(test_path), "%s/.cell", search_dir); if (stat(test_path, &st) == 0 && S_ISDIR(st.st_mode)) { cell_parent_dir = strdup(search_dir); break; } char *last_sep = strrchr(search_dir, '/'); if (!last_sep || last_sep == search_dir) { if (stat("/.cell", &st) == 0 && S_ISDIR(st.st_mode)) cell_parent_dir = strdup("/"); break; } *last_sep = '\0'; } if (cell_parent_dir) { /* 1) Strip any trailing slash from cell_parent_dir (except if it's just "/") */ size_t proj_len = strlen(cell_parent_dir); if (proj_len > 1 && cell_parent_dir[proj_len - 1] == '/') cell_parent_dir[proj_len - 1] = '\0'; char scriptpath[PATH_MAX]; snprintf(scriptpath, sizeof(scriptpath), "%s/scripts", cell_parent_dir); char cellpath[PATH_MAX]; snprintf(cellpath, sizeof(cellpath), "%s/.cell/modules", cell_parent_dir); PHYSFS_mount(scriptpath, NULL, 1); PHYSFS_mount(cellpath, NULL, 0); PHYSFS_mount(cell_parent_dir, NULL, 0); PHYSFS_setWriteDir(cell_parent_dir); free(cell_parent_dir); } else { // Not in a project - use CELLPATH after confirming .. // TODO: implement printf("Could not find project! Exiting!\n"); exit(1); } SDL_free(search_dir); /* Initialize synchronization primitives */ queue_mutex = SDL_CreateMutex(); queue_cond = SDL_CreateCondition(); actors_mutex = SDL_CreateMutex(); event_watchers_mutex = SDL_CreateMutex(); /* Create the initial actor from the command line */ int actor_argc = argc - script_start; char **actor_argv = argv + script_start; WotaBuffer startwota; wota_buffer_init(&startwota, 5); wota_write_record(&startwota, 2); wota_write_text(&startwota, "program"); wota_write_text(&startwota, actor_argv[0]); wota_write_text(&startwota, "arg"); wota_write_array(&startwota, actor_argc - 1); for (int i = 1; i < actor_argc; i++) wota_write_text(&startwota, actor_argv[i]); root_cell = create_actor(startwota.data); /* Launch runner threads */ for (int i = 0; i < cores; i++) { char threadname[128]; snprintf(threadname, sizeof(threadname), "actor runner %d", i); SDL_Thread *thread = SDL_CreateThread(crank_actor, threadname, NULL); arrput(runners, thread); } /* Set up signal and exit handlers */ signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); signal(SIGSEGV, signal_handler); signal(SIGABRT, signal_handler); atexit(exit_handler); SDL_AddEventWatch(event_watch, NULL); /* Main loop: pump ready actors */ SDL_Event event; while (SDL_WaitEvent(&event)) { if (event.type != queue_event) continue; QUEUE: SDL_LockMutex(queue_mutex); if (arrlen(main_ready_queue) == 0) { SDL_UnlockMutex(queue_mutex); continue; } cell_rt *actor = main_ready_queue[0]; arrdel(main_ready_queue, 0); SDL_UnlockMutex(queue_mutex); actor_turn(actor, 0); } return 0; } int actor_exists(const char *id) { SDL_LockMutex(actors_mutex); int idx = shgeti(actors,id); SDL_UnlockMutex(actors_mutex); if (idx == -1) return 0; else return 1; } int JS_ArrayLength(JSContext *js, JSValue a) { JSValue length = JS_GetPropertyStr(js, a, "length"); int len; JS_ToInt32(js,&len,length); JS_FreeValue(js,length); return len; }