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