/* NetHack 3.7 winshim.c $NHDT-Date: 1596498345 2020/08/03 23:45:45 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.259 $ */ /* Copyright (c) Adam Powers, 2020 */ /* NetHack may be freely redistributed. See license for details. */ /* not an actual windowing port, but a fake win port for libnethack */ #include "hack.h" #include #ifdef SHIM_GRAPHICS #include /* for cross-compiling to WebAssembly (WASM) */ #ifdef __EMSCRIPTEN__ #include #endif #undef SHIM_DEBUG #ifdef SHIM_DEBUG #define debugf printf #else /* !SHIM_DEBUG */ #define debugf(...) #endif /* SHIM_DEBUG */ /* shim_graphics_callback is the primary interface to shim graphics, * call this function with your declared callback function * and you will receive all the windowing calls */ #ifdef __EMSCRIPTEN__ /************ * WASM interface ************/ EMSCRIPTEN_KEEPALIVE static char *shim_callback_name = NULL; void shim_graphics_set_callback(char *cbName) { if (shim_callback_name != NULL) free(shim_callback_name); if(cbName && strlen(cbName) > 0) { debugf("setting shim_callback_name: %s\n", cbName); shim_callback_name = strdup(cbName); } else { debugf("un-setting shim_callback_name\n"); shim_callback_name = NULL; } /* TODO: free(shim_callback_name) during shutdown? */ } void local_callback (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args); /* A2P = Argument to Pointer */ #define A2P & /* P2V = Pointer to Void */ #define P2V (void *) #define DECLCB(ret_type, name, fn_args, fmt, ...) \ ret_type name fn_args { \ void *args[] = { __VA_ARGS__ }; \ ret_type ret = (ret_type) 0; \ debugf("SHIM GRAPHICS: " #name "\n"); \ if (!shim_callback_name) return ret; \ local_callback(shim_callback_name, #name, (void *)&ret, fmt, args); \ debugf("SHIM GRAPHICS: " #name " done.\n"); \ return ret; \ } #define VDECLCB(name, fn_args, fmt, ...) \ void name fn_args { \ void *args[] = { __VA_ARGS__ }; \ debugf("SHIM GRAPHICS: " #name "\n"); \ if (!shim_callback_name) return; \ local_callback(shim_callback_name, #name, NULL, fmt, args); \ debugf("SHIM GRAPHICS: " #name " done.\n"); \ } #else /* !__EMSCRIPTEN__ */ /************ * libnethack.a interface ************/ typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...); static shim_callback_t shim_graphics_callback = NULL; void shim_graphics_set_callback(shim_callback_t cb) { shim_graphics_callback = cb; } #define A2P #define P2V #define DECLCB(ret_type, name, fn_args, fmt, ...) \ ret_type name fn_args { \ ret_type ret = (ret_type) 0; \ debugf("SHIM GRAPHICS: " #name "\n"); \ if (!shim_graphics_callback) return ret; \ shim_graphics_callback(#name, (void *)&ret, fmt, ## __VA_ARGS__); \ debugf("SHIM GRAPHICS: " #name " done.\n"); \ return ret; \ } #define VDECLCB(name, fn_args, fmt, ...) \ void name fn_args { \ debugf("SHIM GRAPHICS: " #name "\n"); \ if (!shim_graphics_callback) return; \ shim_graphics_callback(#name, NULL, fmt, ## __VA_ARGS__); \ debugf("SHIM GRAPHICS: " #name " done.\n"); \ } #endif /* __EMSCRIPTEN__ */ VDECLCB(shim_init_nhwindows,(int *argcp, char **argv), "vpp", P2V argcp, P2V argv) VDECLCB(shim_player_selection,(void), "v") VDECLCB(shim_askname,(void), "v") VDECLCB(shim_get_nh_event,(void), "v") VDECLCB(shim_exit_nhwindows,(const char *str), "vs", P2V str) VDECLCB(shim_suspend_nhwindows,(const char *str), "vs", P2V str) VDECLCB(shim_resume_nhwindows,(void), "v") DECLCB(winid, shim_create_nhwindow, (int type), "ii", A2P type) VDECLCB(shim_clear_nhwindow,(winid window), "vi", A2P window) VDECLCB(shim_display_nhwindow,(winid window, boolean blocking), "vii", A2P window, A2P blocking) VDECLCB(shim_destroy_nhwindow,(winid window), "vi", A2P window) VDECLCB(shim_curs,(winid a, int x, int y), "viii", A2P a, A2P x, A2P y) VDECLCB(shim_putstr,(winid w, int attr, const char *str), "viis", A2P w, A2P attr, P2V str) VDECLCB(shim_display_file,(const char *name, boolean complain), "vsi", P2V name, A2P complain) VDECLCB(shim_start_menu,(winid window, unsigned long mbehavior), "vii", A2P window, A2P mbehavior) VDECLCB(shim_add_menu, (winid window, const glyph_info *glyphinfo, const ANY_P *identifier, char ch, char gch, int attr, const char *str, unsigned int itemflags), "vippiiisi", A2P window, P2V glyphinfo, P2V identifier, A2P ch, A2P gch, A2P attr, P2V str, A2P itemflags) VDECLCB(shim_end_menu,(winid window, const char *prompt), "vis", A2P window, P2V prompt) /* XXX: shim_select_menu menu_list is an output */ DECLCB(int, shim_select_menu,(winid window, int how, MENU_ITEM_P **menu_list), "iiio", A2P window, A2P how, P2V menu_list) DECLCB(char, shim_message_menu,(char let, int how, const char *mesg), "ciis", A2P let, A2P how, P2V mesg) VDECLCB(shim_mark_synch,(void), "v") VDECLCB(shim_wait_synch,(void), "v") VDECLCB(shim_cliparound,(int x, int y), "vii", A2P x, A2P y) VDECLCB(shim_update_positionbar,(char *posbar), "vp", P2V posbar) VDECLCB(shim_print_glyph,(winid w, xchar x, xchar y, const glyph_info *glyphinfo, const glyph_info *bkglyphinfo), "viiipp", A2P w, A2P x, A2P y, P2V glyphinfo, P2V bkglyphinfo) VDECLCB(shim_raw_print,(const char *str), "vs", P2V str) VDECLCB(shim_raw_print_bold,(const char *str), "vs", P2V str) DECLCB(int, shim_nhgetch,(void), "i") DECLCB(int, shim_nh_poskey,(int *x, int *y, int *mod), "iooo", P2V x, P2V y, P2V mod) VDECLCB(shim_nhbell,(void), "v") DECLCB(int, shim_doprev_message,(void),"iv") DECLCB(char, shim_yn_function,(const char *query, const char *resp, char def), "cssi", P2V query, P2V resp, A2P def) VDECLCB(shim_getlin,(const char *query, char *bufp), "vso", P2V query, P2V bufp) DECLCB(int,shim_get_ext_cmd,(void),"iv") VDECLCB(shim_number_pad,(int state), "vi", A2P state) VDECLCB(shim_delay_output,(void), "v") VDECLCB(shim_change_color,(int color, long rgb, int reverse), "viii", A2P color, A2P rgb, A2P reverse) VDECLCB(shim_change_background,(int white_or_black), "vi", A2P white_or_black) DECLCB(short, set_shim_font_name,(winid window_type, char *font_name),"2is", A2P window_type, P2V font_name) DECLCB(char *,shim_get_color_string,(void),"sv") /* other defs that really should go away (they're tty specific) */ VDECLCB(shim_start_screen, (void), "v") VDECLCB(shim_end_screen, (void), "v") VDECLCB(shim_preference_update, (const char *pref), "vp", P2V pref) DECLCB(char *,shim_getmsghistory, (boolean init), "si", A2P init) VDECLCB(shim_putmsghistory, (const char *msg, boolean restoring_msghist), "vsi", P2V msg, A2P restoring_msghist) VDECLCB(shim_status_init, (void), "v") VDECLCB(shim_status_enablefield, (int fieldidx, const char *nm, const char *fmt, boolean enable), "vippi", A2P fieldidx, P2V nm, P2V fmt, A2P enable) /* XXX: the second argument to shim_status_update is sometimes an integer and sometimes a pointer */ VDECLCB(shim_status_update, (int fldidx, genericptr_t ptr, int chg, int percent, int color, unsigned long *colormasks), "vioiiip", A2P fldidx, P2V ptr, A2P chg, A2P percent, A2P color, P2V colormasks) #ifdef __EMSCRIPTEN__ /* XXX: calling display_inventory() from shim_update_inventory() causes reentrancy that breaks emscripten Asyncify */ /* this should be fine since according to windows.doc, the only purpose of shim_update_inventory() is to call display_inventory() */ void shim_update_inventory() { if(iflags.perm_invent) { display_inventory(NULL, FALSE); } } #else /* !__EMSCRIPTEN__ */ VDECLCB(shim_update_inventory,(void), "v") #endif /* Interface definition used in windows.c */ struct window_procs shim_procs = { "shim", (0 | WC_ASCII_MAP | WC_COLOR | WC_HILITE_PET | WC_INVERSE | WC_EIGHT_BIT_IN), (0 #if defined(SELECTSAVED) | WC2_SELECTSAVED #endif #if defined(STATUS_HILITES) | WC2_HILITE_STATUS | WC2_HITPOINTBAR | WC2_FLUSH_STATUS | WC2_RESET_STATUS #endif | WC2_DARKGRAY | WC2_SUPPRESS_HIST | WC2_STATUSLINES), #ifdef TEXTCOLOR {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, /* color availability */ #else {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, #endif shim_init_nhwindows, shim_player_selection, shim_askname, shim_get_nh_event, shim_exit_nhwindows, shim_suspend_nhwindows, shim_resume_nhwindows, shim_create_nhwindow, shim_clear_nhwindow, shim_display_nhwindow, shim_destroy_nhwindow, shim_curs, shim_putstr, genl_putmixed, shim_display_file, shim_start_menu, shim_add_menu, shim_end_menu, shim_select_menu, shim_message_menu, shim_update_inventory, shim_mark_synch, shim_wait_synch, #ifdef CLIPPING shim_cliparound, #endif #ifdef POSITIONBAR shim_update_positionbar, #endif shim_print_glyph, shim_raw_print, shim_raw_print_bold, shim_nhgetch, shim_nh_poskey, shim_nhbell, shim_doprev_message, shim_yn_function, shim_getlin, shim_get_ext_cmd, shim_number_pad, shim_delay_output, #ifdef CHANGE_COLOR /* the Mac uses a palette device */ shim_change_color, #ifdef MAC shim_change_background, set_shim_font_name, #endif shim_get_color_string, #endif /* other defs that really should go away (they're tty specific) */ shim_start_screen, shim_end_screen, genl_outrip, shim_preference_update, shim_getmsghistory, shim_putmsghistory, shim_status_init, genl_status_finish, genl_status_enablefield, #ifdef STATUS_HILITES shim_status_update, #else genl_status_update, #endif genl_can_suspend_yes, }; #ifdef __EMSCRIPTEN__ /* convert the C callback to a JavaScript callback */ EM_JS(void, local_callback, (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args), { // Asyncify.handleAsync() is the more logical choice here; however, the stack unrolling in Asyncify is performed by // function call analysis during compilation. Since we are using an indirect callback (cb_name), it can't predict the stack // unrolling and it crashes. Thus we use Asyncify.handleSleep() and wakeUp() to make sure that async doesn't break // Asyncify. For details, see: https://emscripten.org/docs/porting/asyncify.html#optimizing Asyncify.handleSleep(wakeUp => { // convert callback arguments to proper JavaScript varaidic arguments let name = UTF8ToString(shim_name); let fmt = UTF8ToString(fmt_str); let cbName = UTF8ToString(cb_name); // console.log("local_callback:", cbName, fmt, name); // get pointer / type conversion helpers let getPointerValue = globalThis.nethackGlobal.helpers.getPointerValue; let setPointerValue = globalThis.nethackGlobal.helpers.setPointerValue; reentryMutexLock(name); let argTypes = fmt.split(""); let retType = argTypes.shift(); // build array of JavaScript args from WASM parameters let jsArgs = []; for (let i = 0; i < argTypes.length; i++) { let ptr = args + (4*i); let val = getArg(name, ptr, argTypes[i]); jsArgs.push(val); } // do the callback let userCallback = globalThis[cbName]; runJsEventLoop(() => userCallback.call(this, name, ... jsArgs)).then((retVal) => { // save the return value setPointerValue(name, ret_ptr, retType, retVal); // return setTimeout(() => { reentryMutexUnlock(); wakeUp(); }, 0); }); function getArg(name, ptr, type) { return (type === "o")?ptr:getPointerValue(name, getValue(ptr, "*"), type); } // setTimeout() with value of '0' is similar to setImmediate() (but setImmediate isn't standard) // this lets the JS loop run for a tick so that other events can occur // XXX: I also tried replacing the for(;;) in allmain.c:moveloop() with emscripten_set_main_loop() // unfortunately that won't work -- if the simulate_infinite_loop arg is false, it falls through // and the program ends; // if is true, it throws an exception to break out of main(), but doesn't get caught because // the stack isn't running under main() anymore... // I think this is suboptimal, but we will have to live with it (for now?) async function runJsEventLoop(cb) { return new Promise((resolve) => { setTimeout(() => { resolve(cb()); }, 0); }); } function reentryMutexLock(name) { globalThis.nethackGlobal = globalThis.nethackGlobal || {}; if(globalThis.nethackGlobal.shimFunctionRunning) { throw new Error(`'${name}' attempting second call to 'local_callback' before '${globalThis.nethackGlobal.shimFunctionRunning}' has finished, will crash emscripten Asyncify. For details see: emscripten.org/docs/porting/asyncify.html#reentrancy`); } globalThis.nethackGlobal.shimFunctionRunning = name; } function reentryMutexUnlock() { globalThis.nethackGlobal.shimFunctionRunning = null; } }); }) #endif /* __EMSCRIPTEN__ */ #endif /* SHIM_GRAPHICS */