1 /* NetHack 3.7 winshim.c    $NHDT-Date: 1596498345 2020/08/03 23:45:45 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.259 $ */
2 /* Copyright (c) Adam Powers, 2020                                */
3 /* NetHack may be freely redistributed.  See license for details. */
4 
5 /* not an actual windowing port, but a fake win port for libnethack */
6 
7 #include "hack.h"
8 #include <string.h>
9 
10 #ifdef SHIM_GRAPHICS
11 #include <stdarg.h>
12 /* for cross-compiling to WebAssembly (WASM) */
13 #ifdef __EMSCRIPTEN__
14 #include <emscripten/emscripten.h>
15 #endif
16 
17 #undef SHIM_DEBUG
18 
19 #ifdef SHIM_DEBUG
20 #define debugf printf
21 #else /* !SHIM_DEBUG */
22 #define debugf(...)
23 #endif /* SHIM_DEBUG */
24 
25 
26 /* shim_graphics_callback is the primary interface to shim graphics,
27  * call this function with your declared callback function
28  * and you will receive all the windowing calls
29  */
30 #ifdef __EMSCRIPTEN__
31 /************
32  * WASM interface
33  ************/
34 EMSCRIPTEN_KEEPALIVE
35 static char *shim_callback_name = NULL;
shim_graphics_set_callback(char * cbName)36 void shim_graphics_set_callback(char *cbName) {
37     if (shim_callback_name != NULL) free(shim_callback_name);
38     if(cbName && strlen(cbName) > 0) {
39         debugf("setting shim_callback_name: %s\n", cbName);
40         shim_callback_name = strdup(cbName);
41     } else {
42         debugf("un-setting shim_callback_name\n");
43         shim_callback_name = NULL;
44     }
45     /* TODO: free(shim_callback_name) during shutdown? */
46 }
47 void local_callback (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args);
48 
49 /* A2P = Argument to Pointer */
50 #define A2P &
51 /* P2V = Pointer to Void */
52 #define P2V (void *)
53 #define DECLCB(ret_type, name, fn_args, fmt, ...) \
54 ret_type name fn_args { \
55     void *args[] = { __VA_ARGS__ }; \
56     ret_type ret = (ret_type) 0; \
57     debugf("SHIM GRAPHICS: " #name "\n"); \
58     if (!shim_callback_name) return ret; \
59     local_callback(shim_callback_name, #name, (void *)&ret, fmt, args); \
60     debugf("SHIM GRAPHICS: " #name " done.\n"); \
61     return ret; \
62 }
63 
64 #define VDECLCB(name, fn_args, fmt, ...) \
65 void name fn_args { \
66     void *args[] = { __VA_ARGS__ }; \
67     debugf("SHIM GRAPHICS: " #name "\n"); \
68     if (!shim_callback_name) return; \
69     local_callback(shim_callback_name, #name, NULL, fmt, args); \
70     debugf("SHIM GRAPHICS: " #name " done.\n"); \
71 }
72 
73 #else /* !__EMSCRIPTEN__ */
74 
75 /************
76  * libnethack.a interface
77  ************/
78 typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...);
79 static shim_callback_t shim_graphics_callback = NULL;
shim_graphics_set_callback(shim_callback_t cb)80 void shim_graphics_set_callback(shim_callback_t cb) {
81     shim_graphics_callback = cb;
82 }
83 
84 #define A2P
85 #define P2V
86 #define DECLCB(ret_type, name, fn_args, fmt, ...) \
87 ret_type name fn_args { \
88     ret_type ret = (ret_type) 0; \
89     debugf("SHIM GRAPHICS: " #name "\n"); \
90     if (!shim_graphics_callback) return ret; \
91     shim_graphics_callback(#name, (void *)&ret, fmt, ## __VA_ARGS__); \
92     debugf("SHIM GRAPHICS: " #name " done.\n"); \
93     return ret; \
94 }
95 
96 #define VDECLCB(name, fn_args, fmt, ...) \
97 void name fn_args { \
98     debugf("SHIM GRAPHICS: " #name "\n"); \
99     if (!shim_graphics_callback) return; \
100     shim_graphics_callback(#name, NULL, fmt, ## __VA_ARGS__); \
101     debugf("SHIM GRAPHICS: " #name " done.\n"); \
102 }
103 #endif /* __EMSCRIPTEN__ */
104 
105 VDECLCB(shim_init_nhwindows,(int *argcp, char **argv), "vpp", P2V argcp, P2V argv)
106 VDECLCB(shim_player_selection,(void), "v")
107 VDECLCB(shim_askname,(void), "v")
108 VDECLCB(shim_get_nh_event,(void), "v")
109 VDECLCB(shim_exit_nhwindows,(const char *str), "vs", P2V str)
110 VDECLCB(shim_suspend_nhwindows,(const char *str), "vs", P2V str)
111 VDECLCB(shim_resume_nhwindows,(void), "v")
112 DECLCB(winid, shim_create_nhwindow, (int type), "ii", A2P type)
113 VDECLCB(shim_clear_nhwindow,(winid window), "vi", A2P window)
114 VDECLCB(shim_display_nhwindow,(winid window, boolean blocking), "vii", A2P window, A2P blocking)
115 VDECLCB(shim_destroy_nhwindow,(winid window), "vi", A2P window)
116 VDECLCB(shim_curs,(winid a, int x, int y), "viii", A2P a, A2P x, A2P y)
117 VDECLCB(shim_putstr,(winid w, int attr, const char *str), "viis", A2P w, A2P attr, P2V str)
118 VDECLCB(shim_display_file,(const char *name, boolean complain), "vsi", P2V name, A2P complain)
119 VDECLCB(shim_start_menu,(winid window, unsigned long mbehavior), "vii", A2P window, A2P mbehavior)
120 VDECLCB(shim_add_menu,
121     (winid window, const glyph_info *glyphinfo, const ANY_P *identifier, char ch, char gch, int attr, const char *str, unsigned int itemflags),
122     "vippiiisi",
123     A2P window, P2V glyphinfo, P2V identifier, A2P ch, A2P gch, A2P attr, P2V str, A2P itemflags)
124 VDECLCB(shim_end_menu,(winid window, const char *prompt), "vis", A2P window, P2V prompt)
125 /* XXX: shim_select_menu menu_list is an output */
126 DECLCB(int, shim_select_menu,(winid window, int how, MENU_ITEM_P **menu_list), "iiio", A2P window, A2P how, P2V menu_list)
127 DECLCB(char, shim_message_menu,(char let, int how, const char *mesg), "ciis", A2P let, A2P how, P2V mesg)
128 VDECLCB(shim_mark_synch,(void), "v")
129 VDECLCB(shim_wait_synch,(void), "v")
130 VDECLCB(shim_cliparound,(int x, int y), "vii", A2P x, A2P y)
131 VDECLCB(shim_update_positionbar,(char *posbar), "vp", P2V posbar)
132 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)
133 VDECLCB(shim_raw_print,(const char *str), "vs", P2V str)
134 VDECLCB(shim_raw_print_bold,(const char *str), "vs", P2V str)
135 DECLCB(int, shim_nhgetch,(void), "i")
136 DECLCB(int, shim_nh_poskey,(int *x, int *y, int *mod), "iooo", P2V x, P2V y, P2V mod)
137 VDECLCB(shim_nhbell,(void), "v")
138 DECLCB(int, shim_doprev_message,(void),"iv")
139 DECLCB(char, shim_yn_function,(const char *query, const char *resp, char def), "cssi", P2V query, P2V resp, A2P def)
140 VDECLCB(shim_getlin,(const char *query, char *bufp), "vso", P2V query, P2V bufp)
141 DECLCB(int,shim_get_ext_cmd,(void),"iv")
142 VDECLCB(shim_number_pad,(int state), "vi", A2P state)
143 VDECLCB(shim_delay_output,(void), "v")
144 VDECLCB(shim_change_color,(int color, long rgb, int reverse), "viii", A2P color, A2P rgb, A2P reverse)
145 VDECLCB(shim_change_background,(int white_or_black), "vi", A2P white_or_black)
146 DECLCB(short, set_shim_font_name,(winid window_type, char *font_name),"2is", A2P window_type, P2V font_name)
147 DECLCB(char *,shim_get_color_string,(void),"sv")
148 
149 /* other defs that really should go away (they're tty specific) */
150 VDECLCB(shim_start_screen, (void), "v")
151 VDECLCB(shim_end_screen, (void), "v")
152 VDECLCB(shim_preference_update, (const char *pref), "vp", P2V pref)
153 DECLCB(char *,shim_getmsghistory, (boolean init), "si", A2P init)
154 VDECLCB(shim_putmsghistory, (const char *msg, boolean restoring_msghist), "vsi", P2V msg, A2P restoring_msghist)
155 VDECLCB(shim_status_init, (void), "v")
156 VDECLCB(shim_status_enablefield,
157     (int fieldidx, const char *nm, const char *fmt, boolean enable),
158     "vippi",
159     A2P fieldidx, P2V nm, P2V fmt, A2P enable)
160 /* XXX: the second argument to shim_status_update is sometimes an integer and sometimes a pointer */
161 VDECLCB(shim_status_update,
162     (int fldidx, genericptr_t ptr, int chg, int percent, int color, unsigned long *colormasks),
163     "vioiiip",
164     A2P fldidx, P2V ptr, A2P chg, A2P percent, A2P color, P2V colormasks)
165 
166 #ifdef __EMSCRIPTEN__
167 /* XXX: calling display_inventory() from shim_update_inventory() causes reentrancy that breaks emscripten Asyncify */
168 /* this should be fine since according to windows.doc, the only purpose of shim_update_inventory() is to call display_inventory() */
shim_update_inventory()169 void shim_update_inventory() {
170     if(iflags.perm_invent) {
171         display_inventory(NULL, FALSE);
172     }
173 }
174 #else /* !__EMSCRIPTEN__ */
175 VDECLCB(shim_update_inventory,(void), "v")
176 #endif
177 
178 /* Interface definition used in windows.c */
179 struct window_procs shim_procs = {
180     "shim",
181     (0
182      | WC_ASCII_MAP
183      | WC_COLOR | WC_HILITE_PET | WC_INVERSE | WC_EIGHT_BIT_IN),
184     (0
185 #if defined(SELECTSAVED)
186      | WC2_SELECTSAVED
187 #endif
188 #if defined(STATUS_HILITES)
189      | WC2_HILITE_STATUS | WC2_HITPOINTBAR | WC2_FLUSH_STATUS
190      | WC2_RESET_STATUS
191 #endif
192      | WC2_DARKGRAY | WC2_SUPPRESS_HIST | WC2_STATUSLINES),
193 #ifdef TEXTCOLOR
194     {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},   /* color availability */
195 #else
196     {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1},
197 #endif
198     shim_init_nhwindows, shim_player_selection, shim_askname, shim_get_nh_event,
199     shim_exit_nhwindows, shim_suspend_nhwindows, shim_resume_nhwindows,
200     shim_create_nhwindow, shim_clear_nhwindow, shim_display_nhwindow,
201     shim_destroy_nhwindow, shim_curs, shim_putstr, genl_putmixed,
202     shim_display_file, shim_start_menu, shim_add_menu, shim_end_menu,
203     shim_select_menu, shim_message_menu, shim_update_inventory, shim_mark_synch,
204     shim_wait_synch,
205 #ifdef CLIPPING
206     shim_cliparound,
207 #endif
208 #ifdef POSITIONBAR
209     shim_update_positionbar,
210 #endif
211     shim_print_glyph, shim_raw_print, shim_raw_print_bold, shim_nhgetch,
212     shim_nh_poskey, shim_nhbell, shim_doprev_message, shim_yn_function,
213     shim_getlin, shim_get_ext_cmd, shim_number_pad, shim_delay_output,
214 #ifdef CHANGE_COLOR /* the Mac uses a palette device */
215     shim_change_color,
216 #ifdef MAC
217     shim_change_background, set_shim_font_name,
218 #endif
219     shim_get_color_string,
220 #endif
221 
222     /* other defs that really should go away (they're tty specific) */
223     shim_start_screen, shim_end_screen, genl_outrip,
224     shim_preference_update,
225     shim_getmsghistory, shim_putmsghistory,
226     shim_status_init,
227     genl_status_finish, genl_status_enablefield,
228 #ifdef STATUS_HILITES
229     shim_status_update,
230 #else
231     genl_status_update,
232 #endif
233     genl_can_suspend_yes,
234 };
235 
236 #ifdef __EMSCRIPTEN__
237 /* convert the C callback to a JavaScript callback */
238 EM_JS(void, local_callback, (const char *cb_name, const char *shim_name, void *ret_ptr, const char *fmt_str, void *args), {
239     // Asyncify.handleAsync() is the more logical choice here; however, the stack unrolling in Asyncify is performed by
240     // function call analysis during compilation. Since we are using an indirect callback (cb_name), it can't predict the stack
241     // unrolling and it crashes. Thus we use Asyncify.handleSleep() and wakeUp() to make sure that async doesn't break
242     // Asyncify. For details, see: https://emscripten.org/docs/porting/asyncify.html#optimizing
243     Asyncify.handleSleep(wakeUp => {
244         // convert callback arguments to proper JavaScript varaidic arguments
245         let name = UTF8ToString(shim_name);
246         let fmt = UTF8ToString(fmt_str);
247         let cbName = UTF8ToString(cb_name);
248         // console.log("local_callback:", cbName, fmt, name);
249 
250         // get pointer / type conversion helpers
251         let getPointerValue = globalThis.nethackGlobal.helpers.getPointerValue;
252         let setPointerValue = globalThis.nethackGlobal.helpers.setPointerValue;
253 
254         reentryMutexLock(name);
255 
256         let argTypes = fmt.split("");
257         let retType = argTypes.shift();
258 
259         // build array of JavaScript args from WASM parameters
260         let jsArgs = [];
261         for (let i = 0; i < argTypes.length; i++) {
262             let ptr = args + (4*i);
263             let val = getArg(name, ptr, argTypes[i]);
264             jsArgs.push(val);
265         }
266 
267         // do the callback
268         let userCallback = globalThis[cbName];
269         runJsEventLoop(() => userCallback.call(this, name, ... jsArgs)).then((retVal) => {
270             // save the return value
271             setPointerValue(name, ret_ptr, retType, retVal);
272             // return
273             setTimeout(() => {
274                 reentryMutexUnlock();
275                 wakeUp();
276             }, 0);
277         });
278 
279         function getArg(name, ptr, type) {
280             return (type === "o")?ptr:getPointerValue(name, getValue(ptr, "*"), type);
281         }
282 
283         // setTimeout() with value of '0' is similar to setImmediate() (but setImmediate isn't standard)
284         // this lets the JS loop run for a tick so that other events can occur
285         // XXX: I also tried replacing the for(;;) in allmain.c:moveloop() with emscripten_set_main_loop()
286         // unfortunately that won't work -- if the simulate_infinite_loop arg is false, it falls through
287         // and the program ends;
288         // if is true, it throws an exception to break out of main(), but doesn't get caught because
289         // the stack isn't running under main() anymore...
290         // I think this is suboptimal, but we will have to live with it (for now?)
291         async function runJsEventLoop(cb) {
292             return new Promise((resolve) => {
293                 setTimeout(() => {
294                     resolve(cb());
295                 }, 0);
296             });
297         }
298 
299         function reentryMutexLock(name) {
300             globalThis.nethackGlobal = globalThis.nethackGlobal || {};
301             if(globalThis.nethackGlobal.shimFunctionRunning) {
302                 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`);
303             }
304             globalThis.nethackGlobal.shimFunctionRunning = name;
305         }
306 
307         function reentryMutexUnlock() {
308             globalThis.nethackGlobal.shimFunctionRunning = null;
309         }
310     });
311 })
312 #endif /* __EMSCRIPTEN__ */
313 
314 #endif /* SHIM_GRAPHICS */
315