Files
nethack/win/shim/winshim.c
nhmall c9673b3d9e more window port interface adjustments
further adjustments to the window port interface to pass a pointer
to a glyph_info struct which describes not just the glyph number
itself, but also the ttychar, the color, the glyphflags, and the
symset index.

This affects two existing window port calls that get passed glyphs
and does the parameter consistently for both of them using the
glyph_info struct pointer:
	print_glyph()
	add_menu().

The recently added glyphmod parameter is now unnecessary and has been
removed.
2021-01-05 10:09:37 -05:00

315 lines
13 KiB
C

/* 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 <string.h>
#ifdef SHIM_GRAPHICS
#include <stdarg.h>
/* for cross-compiling to WebAssembly (WASM) */
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#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_P 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_P 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_P ch, CHAR_P gch, int attr, const char *str, unsigned int itemflags),
"viipiiisi",
A2P window, P2VP 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_P 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, int x, int y, const glyph_info *glyph, const glyph_info *bkglyph), "viiiii", A2P w, A2P x, A2P y, V2P glyphinfo, V2P 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_P 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_P init), "si", A2P init)
VDECLCB(shim_putmsghistory, (const char *msg, BOOLEAN_P 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_P 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 */