diff --git a/.gitignore b/.gitignore index 366cf938f..aa171205f 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,10 @@ targets/* #test.js #sys/lib/npm-package/build/nethack.js #sys/lib/npm-package/build/nethack.wasm +src/libnethack.a +/libtest.c +/nhlibtest +/run.sh +/test.js +sys/lib/npm-package/build/nethack.js +sys/lib/npm-package/build/nethack.wasm diff --git a/include/global.h b/include/global.h index caa32847b..4581137ef 100644 --- a/include/global.h +++ b/include/global.h @@ -331,8 +331,15 @@ struct version_info { unsigned long incarnation; /* actual version number */ unsigned long feature_set; /* bitmask of config settings */ unsigned long entity_count; /* # of monsters and objects */ +#ifndef __EMSCRIPTEN__ unsigned long struct_sizes1; /* size of key structs */ unsigned long struct_sizes2; /* size of more key structs */ +#else /* __EMSCRIPTEN__ */ + /* 'long' in WASM is 4 bytes, which is too small to hold version numbers + * such as: VERSION_SANITY2 */ + unsigned long long struct_sizes1; /* size of key structs */ + unsigned long long struct_sizes2; /* size of more key structs */ +#endif /* !__EMSCRIPTEN__ */ }; struct savefile_info { @@ -396,7 +403,7 @@ struct savefile_info { /* PANICTRACE: Always defined for NH_DEVEL_STATUS != NH_STATUS_RELEASED but only for supported platforms. */ -#ifdef UNIX +#if defined(UNIX) && !defined(__EMSCRIPTEN__) #if (NH_DEVEL_STATUS != NH_STATUS_RELEASED) /* see end.c */ #if !defined(CROSS_TO_WASM) @@ -414,7 +421,7 @@ struct savefile_info { #if defined(MACOSX) #define PANICTRACE_LIBC #endif -#ifdef UNIX +#if defined(UNIX) && !defined(__EMSCRIPTEN__) /* no popen in WASM */ #if !defined(CROSS_TO_WASM) /* no popen in WASM */ #define PANICTRACE_GDB #endif diff --git a/src/rip.c b/src/rip.c index 634d816d2..c9d518aea 100644 --- a/src/rip.c +++ b/src/rip.c @@ -1,4 +1,4 @@ -/* NetHack 3.7 rip.c $NHDT-Date: 1597967808 2020/08/20 23:56:48 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.33 $ */ +/* NetHack 3.7 rip.c $NHDT-Date: 1596498204 2020/08/03 23:43:24 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.32 $ */ /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ /*-Copyright (c) Robert Patrick Rankin, 2017. */ /* NetHack may be freely redistributed. See license for details. */ @@ -61,10 +61,12 @@ static const char *rip_txt[] = { }; #define STONE_LINE_CENT 19 /* char[] element of center of stone face */ #endif /* NH320_DEDICATION */ -#define STONE_LINE_LEN 16 /* # chars that fit on one line - * (note 1 ' ' border) */ -#define NAME_LINE 6 /* *char[] line # for player name */ -#define GOLD_LINE 7 /* *char[] line # for amount of gold */ +#define STONE_LINE_LEN \ + 16 /* # chars that fit on one line \ + * (note 1 ' ' border) \ + */ +#define NAME_LINE 6 /* *char[] line # for player name */ +#define GOLD_LINE 7 /* *char[] line # for amount of gold */ #define DEATH_LINE 8 /* *char[] line # for death description */ #define YEAR_LINE 12 /* *char[] line # for year */ @@ -89,9 +91,9 @@ time_t when; register char **dp; register char *dpx; char buf[BUFSZ]; + long year; register int x; - int line, year; - long cash; + int line; g.rip = dp = (char **) alloc(sizeof(rip_txt)); for (x = 0; rip_txt[x]; ++x) @@ -99,15 +101,13 @@ time_t when; dp[x] = (char *) 0; /* Put name on stone */ - Sprintf(buf, "%.*s", (int) STONE_LINE_LEN, g.plname); + Sprintf(buf, "%s", g.plname); + buf[STONE_LINE_LEN] = 0; center(NAME_LINE, buf); /* Put $ on stone */ - cash = max(g.done_money, 0L); - /* arbitrary upper limit; practical upper limit is quite a bit less */ - if (cash > 999999999L) - cash = 999999999L; - Sprintf(buf, "%ld Au", cash); + Sprintf(buf, "%ld Au", g.done_money); + buf[STONE_LINE_LEN] = 0; /* It could be a *lot* of gold :-) */ center(GOLD_LINE, buf); /* Put together death description */ @@ -115,11 +115,11 @@ time_t when; /* Put death type on stone */ for (line = DEATH_LINE, dpx = buf; line < YEAR_LINE; line++) { + register int i, i0; char tmpchar; - int i, i0 = (int) strlen(dpx); - if (i0 > STONE_LINE_LEN) { - for (i = STONE_LINE_LEN; (i > 0) && (i0 > STONE_LINE_LEN); --i) + if ((i0 = strlen(dpx)) > STONE_LINE_LEN) { + for (i = STONE_LINE_LEN; ((i0 > STONE_LINE_LEN) && i); i--) if (dpx[i] == ' ') i0 = i; if (!i) @@ -136,8 +136,8 @@ time_t when; } /* Put year on stone */ - year = (int) ((yyyymmdd(when) / 10000L) % 10000L); - Sprintf(buf, "%4d", year); + year = yyyymmdd(when) / 10000L; + Sprintf(buf, "%4ld", year); center(YEAR_LINE, buf); #ifdef DUMPLOG diff --git a/sys/lib/libnethackmain.c b/sys/lib/libnethackmain.c new file mode 100644 index 000000000..cec8c076e --- /dev/null +++ b/sys/lib/libnethackmain.c @@ -0,0 +1,1127 @@ +/* NetHack 3.7 unixmain.c $NHDT-Date: 1596498297 2020/08/03 23:44:57 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.87 $ */ +/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */ +/*-Copyright (c) Robert Patrick Rankin, 2011. */ +/* NetHack may be freely redistributed. See license for details. */ + +/* main.c - Unix NetHack */ + +#include "hack.h" +#include "dlb.h" +#include "date.h" + +#include +#include +#include +#include +#ifndef O_RDONLY +#include +#endif + +/* for cross-compiling to WebAssembly (WASM) */ +#ifdef __EMSCRIPTEN__ +#include +void js_helpers_init(); +void js_constants_init(); +void js_globals_init(); +#endif + +#if !defined(_BULL_SOURCE) && !defined(__sgi) && !defined(_M_UNIX) +#if !defined(SUNOS4) && !(defined(ULTRIX) && defined(__GNUC__)) +#if defined(POSIX_TYPES) || defined(SVR4) || defined(HPUX) +extern struct passwd *FDECL(getpwuid, (uid_t)); +#else +extern struct passwd *FDECL(getpwuid, (int)); +#endif +#endif +#endif +extern struct passwd *FDECL(getpwnam, (const char *)); +#ifdef CHDIR +static void FDECL(chdirx, (const char *, BOOLEAN_P)); +#endif /* CHDIR */ +static boolean NDECL(whoami); +static void FDECL(process_options, (int, char **)); + +#ifdef _M_UNIX +extern void NDECL(check_sco_console); +extern void NDECL(init_sco_cons); +#endif +#ifdef __linux__ +extern void NDECL(check_linux_console); +extern void NDECL(init_linux_cons); +#endif + +static void NDECL(wd_message); +static boolean wiz_error_flag = FALSE; +static struct passwd *NDECL(get_unix_pw); + +#ifdef __EMSCRIPTEN__ +/* if WebAssembly, export this API and don't optimize it out */ +EMSCRIPTEN_KEEPALIVE +int +main(argc, argv) +int argc; +char *argv[]; + +#else /* !__EMSCRIPTEN__ */ + +int +nhmain(argc, argv) +int argc; +char *argv[]; + +#endif /* __EMSCRIPTEN__ */ +{ +#ifdef CHDIR + register char *dir; +#endif + NHFILE *nhfp; + boolean exact_username; + boolean resuming = FALSE; /* assume new game */ + boolean plsel_once = FALSE; + // int i; + // for (i = 0; i < argc; i++) { + // printf ("argv[%d]: %s\n", i, argv[i]); + // } + + early_init(); + + g.hname = argv[0]; + g.hackpid = getpid(); + (void) umask(0777 & ~FCMASK); + + choose_windows(DEFAULT_WINDOW_SYS); + +#ifdef CHDIR /* otherwise no chdir() */ + /* + * See if we must change directory to the playground. + * (Perhaps hack runs suid and playground is inaccessible + * for the player.) + * The environment variable HACKDIR is overridden by a + * -d command line option (must be the first option given). + */ + dir = nh_getenv("NETHACKDIR"); + if (!dir) + dir = nh_getenv("HACKDIR"); + + if (argc > 1) { + if (argcheck(argc, argv, ARG_VERSION) == 2) + exit(EXIT_SUCCESS); + + if (argcheck(argc, argv, ARG_SHOWPATHS) == 2) { +#ifdef CHDIR + chdirx((char *) 0, 0); +#endif + iflags.initoptions_noterminate = TRUE; + initoptions(); + iflags.initoptions_noterminate = FALSE; + reveal_paths(); + exit(EXIT_SUCCESS); + } + if (argcheck(argc, argv, ARG_DEBUG) == 1) { + argc--; + argv++; + } + if (argc > 1 && !strncmp(argv[1], "-d", 2) && argv[1][2] != 'e') { + /* avoid matching "-dec" for DECgraphics; since the man page + * says -d directory, hope nobody's using -desomething_else + */ + argc--; + argv++; + dir = argv[0] + 2; + if (*dir == '=' || *dir == ':') + dir++; + if (!*dir && argc > 1) { + argc--; + argv++; + dir = argv[0]; + } + if (!*dir) + error("Flag -d must be followed by a directory name."); + } + } +#endif /* CHDIR */ + + if (argc > 1) { + /* + * Now we know the directory containing 'record' and + * may do a prscore(). Exclude `-style' - it's a Qt option. + */ + if (!strncmp(argv[1], "-s", 2) && strncmp(argv[1], "-style", 6)) { +#ifdef CHDIR + chdirx(dir, 0); +#endif +#ifdef SYSCF + initoptions(); +#endif +#ifdef PANICTRACE + ARGV0 = g.hname; /* save for possible stack trace */ +#ifndef NO_SIGNAL + panictrace_setsignals(TRUE); +#endif +#endif + prscore(argc, argv); + /* FIXME: shouldn't this be using nh_terminate() to free + up any memory allocated by initoptions() */ + exit(EXIT_SUCCESS); + } + } /* argc > 1 */ + +/* + * Change directories before we initialize the window system so + * we can find the tile file. + */ +#ifdef CHDIR + chdirx(dir, 1); +#endif + +#ifdef _M_UNIX + check_sco_console(); +#endif +#ifdef __linux__ + check_linux_console(); +#endif + initoptions(); +#ifdef PANICTRACE + ARGV0 = g.hname; /* save for possible stack trace */ +#ifndef NO_SIGNAL + panictrace_setsignals(TRUE); +#endif +#endif + exact_username = whoami(); + + /* + * It seems you really want to play. + */ + u.uhp = 1; /* prevent RIP on early quits */ + g.program_state.preserve_locks = 1; +#ifndef NO_SIGNAL + sethanguphandler((SIG_RET_TYPE) hangup); +#endif + + process_options(argc, argv); /* command line options */ +#ifdef WINCHAIN + commit_windowchain(); +#endif +#ifdef __EMSCRIPTEN__ + js_helpers_init(); + js_constants_init(); + js_globals_init(); +#endif + init_nhwindows(&argc, argv); /* now we can set up window system */ +#ifdef _M_UNIX + init_sco_cons(); +#endif +#ifdef __linux__ + init_linux_cons(); +#endif + +#ifdef DEF_PAGER + if (!(g.catmore = nh_getenv("HACKPAGER")) + && !(g.catmore = nh_getenv("PAGER"))) + g.catmore = DEF_PAGER; +#endif +#ifdef MAIL + getmailstatus(); +#endif + + /* wizard mode access is deferred until here */ + set_playmode(); /* sets plname to "wizard" for wizard mode */ + /* hide any hyphens from plnamesuffix() */ + g.plnamelen = exact_username ? (int) strlen(g.plname) : 0; + /* strip role,race,&c suffix; calls askname() if plname[] is empty + or holds a generic user name like "player" or "games" */ + plnamesuffix(); + + if (wizard) { + /* use character name rather than lock letter for file names */ + g.locknum = 0; + } else { +#ifndef NO_SIGNAL + /* suppress interrupts while processing lock file */ + (void) signal(SIGQUIT, SIG_IGN); + (void) signal(SIGINT, SIG_IGN); +#endif + } + + dlb_init(); /* must be before newgame() */ + + /* + * Initialize the vision system. This must be before mklev() on a + * new game or before a level restore on a saved game. + */ + vision_init(); + + display_gamewindows(); + + /* + * First, try to find and restore a save file for specified character. + * We'll return here if new game player_selection() renames the hero. + */ + attempt_restore: + + /* + * getlock() complains and quits if there is already a game + * in progress for current character name (when g.locknum == 0) + * or if there are too many active games (when g.locknum > 0). + * When proceeding, it creates an empty .0 file to + * designate the current game. + * getlock() constructs based on the character + * name (for !g.locknum) or on first available of alock, block, + * clock, &c not currently in use in the playground directory + * (for g.locknum > 0). + */ + if (*g.plname) { + getlock(); + g.program_state.preserve_locks = 0; /* after getlock() */ + } + + if (*g.plname && (nhfp = restore_saved_game()) != 0) { + const char *fq_save = fqname(g.SAVEF, SAVEPREFIX, 1); + + (void) chmod(fq_save, 0); /* disallow parallel restores */ +#ifndef NO_SIGNAL + (void) signal(SIGINT, (SIG_RET_TYPE) done1); +#endif +#ifdef NEWS + if (iflags.news) { + display_file(NEWS, FALSE); + iflags.news = FALSE; /* in case dorecover() fails */ + } +#endif + pline("Restoring save file..."); + mark_synch(); /* flush output */ + if (dorecover(nhfp)) { + resuming = TRUE; /* not starting new game */ + wd_message(); + if (discover || wizard) { + /* this seems like a candidate for paranoid_confirmation... */ + if (yn("Do you want to keep the save file?") == 'n') { + (void) delete_savefile(); + } else { + (void) chmod(fq_save, FCMASK); /* back to readable */ + nh_compress(fq_save); + } + } + } + } + + if (!resuming) { + boolean neednewlock = (!*g.plname); + /* new game: start by choosing role, race, etc; + player might change the hero's name while doing that, + in which case we try to restore under the new name + and skip selection this time if that didn't succeed */ + if (!iflags.renameinprogress || iflags.defer_plname || neednewlock) { + if (!plsel_once) + player_selection(); + plsel_once = TRUE; + if (neednewlock && *g.plname) + goto attempt_restore; + if (iflags.renameinprogress) { + /* player has renamed the hero while selecting role; + if locking alphabetically, the existing lock file + can still be used; otherwise, discard current one + and create another for the new character name */ + if (!g.locknum) { + delete_levelfile(0); /* remove empty lock file */ + getlock(); + } + goto attempt_restore; + } + } + newgame(); + wd_message(); + } + + /* moveloop() never returns but isn't flagged NORETURN */ + moveloop(resuming); + + exit(EXIT_SUCCESS); + /*NOTREACHED*/ + return 0; +} + +/* caveat: argv elements might be arbitrary long */ +static void +process_options(argc, argv) +int argc; +char *argv[]; +{ + int i, l; + + /* + * Process options. + */ + while (argc > 1 && argv[1][0] == '-') { + argv++; + argc--; + l = (int) strlen(*argv); + /* must supply at least 4 chars to match "-XXXgraphics" */ + if (l < 4) + l = 4; + + switch (argv[0][1]) { + case 'D': + case 'd': + if ((argv[0][1] == 'D' && !argv[0][2]) + || !strcmpi(*argv, "-debug")) { + wizard = TRUE, discover = FALSE; + } else if (!strncmpi(*argv, "-DECgraphics", l)) { + load_symset("DECGraphics", PRIMARY); + switch_symbols(TRUE); + } else { + raw_printf("Unknown option: %.60s", *argv); + } + break; + case 'X': + discover = TRUE, wizard = FALSE; + break; +#ifdef NEWS + case 'n': + iflags.news = FALSE; + break; +#endif + case 'u': + if (argv[0][2]) { + (void) strncpy(g.plname, argv[0] + 2, sizeof g.plname - 1); + g.plnamelen = 0; /* plname[] might have -role-race attached */ + } else if (argc > 1) { + argc--; + argv++; + (void) strncpy(g.plname, argv[0], sizeof g.plname - 1); + g.plnamelen = 0; + } else { + raw_print("Player name expected after -u"); + } + break; + case 'I': + case 'i': + if (!strncmpi(*argv, "-IBMgraphics", l)) { + load_symset("IBMGraphics", PRIMARY); + load_symset("RogueIBM", ROGUESET); + switch_symbols(TRUE); + } else { + raw_printf("Unknown option: %.60s", *argv); + } + break; + case 'p': /* profession (role) */ + if (argv[0][2]) { + if ((i = str2role(&argv[0][2])) >= 0) + flags.initrole = i; + } else if (argc > 1) { + argc--; + argv++; + if ((i = str2role(argv[0])) >= 0) + flags.initrole = i; + } + break; + case 'r': /* race */ + if (argv[0][2]) { + if ((i = str2race(&argv[0][2])) >= 0) + flags.initrace = i; + } else if (argc > 1) { + argc--; + argv++; + if ((i = str2race(argv[0])) >= 0) + flags.initrace = i; + } + break; + case 'w': /* windowtype */ + config_error_init(FALSE, "command line", FALSE); + choose_windows(&argv[0][2]); + config_error_done(); + break; + case '@': + flags.randomall = 1; + break; + default: + if ((i = str2role(&argv[0][1])) >= 0) { + flags.initrole = i; + break; + } + /* else raw_printf("Unknown option: %.60s", *argv); */ + } + } + +#ifdef SYSCF + if (argc > 1) + raw_printf("MAXPLAYERS are set in sysconf file.\n"); +#else + /* XXX This is deprecated in favor of SYSCF with MAXPLAYERS */ + if (argc > 1) + g.locknum = atoi(argv[1]); +#endif +#ifdef MAX_NR_OF_PLAYERS + /* limit to compile-time limit */ + if (!g.locknum || g.locknum > MAX_NR_OF_PLAYERS) + g.locknum = MAX_NR_OF_PLAYERS; +#endif +#ifdef SYSCF + /* let syscf override compile-time limit */ + if (!g.locknum || (sysopt.maxplayers && g.locknum > sysopt.maxplayers)) + g.locknum = sysopt.maxplayers; +#endif +} + +#ifdef CHDIR +static void +chdirx(dir, wr) +const char *dir; +boolean wr; +{ + if (dir /* User specified directory? */ +#ifdef HACKDIR + && strcmp(dir, HACKDIR) /* and not the default? */ +#endif + ) { +#ifdef SECURE + (void) setgid(getgid()); + (void) setuid(getuid()); /* Ron Wessels */ +#endif + } else { + /* non-default data files is a sign that scores may not be + * compatible, or perhaps that a binary not fitting this + * system's layout is being used. + */ +#ifdef VAR_PLAYGROUND + int len = strlen(VAR_PLAYGROUND); + + g.fqn_prefix[SCOREPREFIX] = (char *) alloc(len + 2); + Strcpy(g.fqn_prefix[SCOREPREFIX], VAR_PLAYGROUND); + if (g.fqn_prefix[SCOREPREFIX][len - 1] != '/') { + g.fqn_prefix[SCOREPREFIX][len] = '/'; + g.fqn_prefix[SCOREPREFIX][len + 1] = '\0'; + } + +#endif + } + +#ifdef HACKDIR + if (dir == (const char *) 0) + dir = HACKDIR; +#endif + + if (dir && chdir(dir) < 0) { + perror(dir); + error("Cannot chdir to %s.", dir); + } + + /* warn the player if we can't write the record file + * perhaps we should also test whether . is writable + * unfortunately the access system-call is worthless. + */ + if (wr) { +#ifdef VAR_PLAYGROUND + g.fqn_prefix[LEVELPREFIX] = g.fqn_prefix[SCOREPREFIX]; + g.fqn_prefix[SAVEPREFIX] = g.fqn_prefix[SCOREPREFIX]; + g.fqn_prefix[BONESPREFIX] = g.fqn_prefix[SCOREPREFIX]; + g.fqn_prefix[LOCKPREFIX] = g.fqn_prefix[SCOREPREFIX]; + g.fqn_prefix[TROUBLEPREFIX] = g.fqn_prefix[SCOREPREFIX]; +#endif + check_recordfile(dir); + } +} +#endif /* CHDIR */ + +/* returns True iff we set plname[] to username which contains a hyphen */ +static boolean +whoami() +{ + /* + * Who am i? Algorithm: 1. Use name as specified in NETHACKOPTIONS + * 2. Use $USER or $LOGNAME (if 1. fails) + * 3. Use getlogin() (if 2. fails) + * The resulting name is overridden by command line options. + * If everything fails, or if the resulting name is some generic + * account like "games", "play", "player", "hack" then eventually + * we'll ask him. + * Note that we trust the user here; it is possible to play under + * somebody else's name. + */ + if (!*g.plname) { + register const char *s; + + s = nh_getenv("USER"); + if (!s || !*s) + s = nh_getenv("LOGNAME"); + if (!s || !*s) + s = getlogin(); + + if (s && *s) { + (void) strncpy(g.plname, s, sizeof g.plname - 1); + if (index(g.plname, '-')) + return TRUE; + } + } + return FALSE; +} + +void +sethanguphandler(handler) +void FDECL((*handler), (int)); +{ +#ifndef NO_SIGNAL +#ifdef SA_RESTART + /* don't want reads to restart. If SA_RESTART is defined, we know + * sigaction exists and can be used to ensure reads won't restart. + * If it's not defined, assume reads do not restart. If reads restart + * and a signal occurs, the game won't do anything until the read + * succeeds (or the stream returns EOF, which might not happen if + * reading from, say, a window manager). */ + struct sigaction sact; + + (void) memset((genericptr_t) &sact, 0, sizeof sact); + sact.sa_handler = (SIG_RET_TYPE) handler; + (void) sigaction(SIGHUP, &sact, (struct sigaction *) 0); +#ifdef SIGXCPU + (void) sigaction(SIGXCPU, &sact, (struct sigaction *) 0); +#endif +#else /* !SA_RESTART */ + (void) signal(SIGHUP, (SIG_RET_TYPE) handler); +#ifdef SIGXCPU + (void) signal(SIGXCPU, (SIG_RET_TYPE) handler); +#endif +#endif /* ?SA_RESTART */ +#endif /* !NO_SIGNAL */ +} + +#ifdef PORT_HELP +void +port_help() +{ + /* + * Display unix-specific help. Just show contents of the helpfile + * named by PORT_HELP. + */ + display_file(PORT_HELP, TRUE); +} +#endif + +/* validate wizard mode if player has requested access to it */ +boolean +authorize_wizard_mode() +{ + struct passwd *pw = get_unix_pw(); + + if (pw && sysopt.wizards && sysopt.wizards[0]) { + if (check_user_string(sysopt.wizards)) + return TRUE; + } + wiz_error_flag = TRUE; /* not being allowed into wizard mode */ + return FALSE; +} + +static void +wd_message() +{ + if (wiz_error_flag) { + if (sysopt.wizards && sysopt.wizards[0]) { + char *tmp = build_english_list(sysopt.wizards); + pline("Only user%s %s may access debug (wizard) mode.", + index(sysopt.wizards, ' ') ? "s" : "", tmp); + free(tmp); + } else + pline("Entering explore/discovery mode instead."); + wizard = 0, discover = 1; /* (paranoia) */ + } else if (discover) + You("are in non-scoring explore/discovery mode."); +} + +/* + * Add a slash to any name not ending in /. There must + * be room for the / + */ +void +append_slash(name) +char *name; +{ + char *ptr; + + if (!*name) + return; + ptr = name + (strlen(name) - 1); + if (*ptr != '/') { + *++ptr = '/'; + *++ptr = '\0'; + } + return; +} + +boolean +check_user_string(optstr) +char *optstr; +{ + struct passwd *pw; + int pwlen; + char *eop, *w; + char *pwname = 0; + + if (optstr[0] == '*') + return TRUE; /* allow any user */ + if (sysopt.check_plname) + pwname = g.plname; + else if ((pw = get_unix_pw()) != 0) + pwname = pw->pw_name; + if (!pwname || !*pwname) + return FALSE; + pwlen = (int) strlen(pwname); + eop = eos(optstr); + w = optstr; + while (w + pwlen <= eop) { + if (!*w) + break; + if (isspace(*w)) { + w++; + continue; + } + if (!strncmp(w, pwname, pwlen)) { + if (!w[pwlen] || isspace(w[pwlen])) + return TRUE; + } + while (*w && !isspace(*w)) + w++; + } + return FALSE; +} + +static struct passwd * +get_unix_pw() +{ + char *user; + unsigned uid; + static struct passwd *pw = (struct passwd *) 0; + + if (pw) + return pw; /* cache answer */ + + uid = (unsigned) getuid(); + user = getlogin(); + if (user) { + pw = getpwnam(user); + if (pw && ((unsigned) pw->pw_uid != uid)) + pw = 0; + } + if (pw == 0) { + user = nh_getenv("USER"); + if (user) { + pw = getpwnam(user); + if (pw && ((unsigned) pw->pw_uid != uid)) + pw = 0; + } + if (pw == 0) { + pw = getpwuid(uid); + } + } + return pw; +} + +char * +get_login_name() +{ + static char buf[BUFSZ]; + struct passwd *pw = get_unix_pw(); + + buf[0] = '\0'; + if (pw) + (void)strcpy(buf, pw->pw_name); + + return buf; +} + +unsigned long +sys_random_seed() +{ + unsigned long seed = 0L; + unsigned long pid = (unsigned long) getpid(); + boolean no_seed = TRUE; +#ifdef DEV_RANDOM + FILE *fptr; + + fptr = fopen(DEV_RANDOM, "r"); + if (fptr) { + fread(&seed, sizeof (long), 1, fptr); + has_strong_rngseed = TRUE; /* decl.c */ + no_seed = FALSE; + (void) fclose(fptr); + } else { + /* leaves clue, doesn't exit */ + paniclog("sys_random_seed", "falling back to weak seed"); + } +#endif + if (no_seed) { + seed = (unsigned long) getnow(); /* time((TIME_type) 0) */ + /* Quick dirty band-aid to prevent PRNG prediction */ + if (pid) { + if (!(pid & 3L)) + pid -= 1L; + seed *= pid; + } + } + return seed; +} + +#ifdef __EMSCRIPTEN__ +/*** + * Helpers + ***/ +EM_JS(void, js_helpers_init, (), { + globalThis.nethackGlobal = globalThis.nethackGlobal || {}; + globalThis.nethackGlobal.helpers = globalThis.nethackGlobal.helpers || {}; + + installHelper(mapglyphHelper); + installHelper(displayInventory); + installHelper(getPointerValue); + installHelper(setPointerValue); + + // used by print_glyph + function mapglyphHelper(glyph, x, y, mgflags) { + let ochar = _malloc(4); + let ocolor = _malloc(4); + let ospecial = _malloc(4); + + _mapglyph(glyph, ochar, ocolor, ospecial, x, y, mgflags); + + let ch = getValue(ochar, "i32"); + let color = getValue(ocolor, "i32"); + let special = getValue(ospecial, "i32"); + + _free (ochar); + _free (ocolor); + _free (ospecial); + + return { + glyph, + ch, + color, + special, + x, + y, + mgflags + }; + } + + // used by update_inventory + function displayInventory() { + // Asyncify.handleAsync(async () => { + return _display_inventory(0, 0); + // }); + } + + // convert 'ptr' to the type indicated by 'type' + function getPointerValue(name, ptr, type) { + // console.log("getPointerValue", name, "0x" + ptr.toString(16), type); + switch(type) { + case "s": // string + // var value = UTF8ToString(getValue(ptr, "*")); + return UTF8ToString(ptr); + case "p": // pointer + if(!ptr) return 0; // null pointer + return getValue(ptr, "*"); + case "c": // char + return String.fromCharCode(getValue(ptr, "i8")); + case "0": /* 2^0 = 1 byte */ + return getValue(ptr, "i8"); + case "1": /* 2^1 = 2 bytes */ + return getValue(ptr, "i16"); + case "2": /* 2^2 = 4 bytes */ + case "i": // integer + case "n": // number + return getValue(ptr, "i32"); + case "f": // float + return getValue(ptr, "float"); + case "d": // double + return getValue(ptr, "double"); + case "o": // overloaded: multiple types + return ptr; + default: + throw new TypeError ("unknown type:" + type); + } + } + + // sets the return value of the function to the type expected + function setPointerValue(name, ptr, type, value = 0) { + // console.log("setPointerValue", name, "0x" + ptr.toString(16), type, value); + switch (type) { + case "p": + throw new Error("not implemented"); + case "s": + if(typeof value !== "string") + throw new TypeError(`expected ${name} return type to be string`); + // value=value?value:"(no value)"; + // var strPtr = getValue(ptr, "i32"); + stringToUTF8(value, ptr, 1024); // TODO: uhh... danger will robinson + break; + case "i": + if(typeof value !== "number" || !Number.isInteger(value)) + throw new TypeError(`expected ${name} return type to be integer`); + setValue(ptr, value, "i32"); + break; + case "c": + if(typeof value !== "number" || value < 0 || value > 128) + throw new TypeError(`expected ${name} return type to be integer representing an ASCII character`); + setValue(ptr, value, "i8"); + break; + case "f": + if(typeof value !== "number" || isFloat(value)) + throw new TypeError(`expected ${name} return type to be float`); + // XXX: I'm not sure why 'double' works and 'float' doesn't + setValue(ptr, value, "double"); + break; + case "d": + if(typeof value !== "number" || isFloat(value)) + throw new TypeError(`expected ${name} return type to be double`); + setValue(ptr, value, "double"); + break; + case "v": + break; + default: + throw new Error("unknown type"); + } + + function isFloat(n){ + return n === +n && n !== (n|0) && !Number.isInteger(n); + } + } + + + function installHelper(fn, name) { + name = name || fn.name; + globalThis.nethackGlobal.helpers[name] = fn; + } +}) + +/*** + * Constants + ***/ +#define SET_CONSTANT(scope, name) set_const(scope, #name, name); +EM_JS(void, set_const, (char *scope_str, char *name_str, int num), { + let scope = UTF8ToString(scope_str); + let name = UTF8ToString(name_str); + + globalThis.nethackGlobal.constants[scope] = globalThis.nethackGlobal.constants[scope] || {}; + globalThis.nethackGlobal.constants[scope][name] = num; + globalThis.nethackGlobal.constants[scope][num] = name; +}); +#define SET_CONSTANT_STRING(scope, name) set_const_str(scope, #name, name); +EM_JS(void, set_const_str, (char *scope_str, char *name_str, char *input_str), { + let scope = UTF8ToString(scope_str); + let name = UTF8ToString(name_str); + let str = UTF8ToString(input_str); + + globalThis.nethackGlobal.constants[scope] = globalThis.nethackGlobal.constants[scope] || {}; + globalThis.nethackGlobal.constants[scope][name] = str; +}); + +void js_constants_init() { + EM_ASM({ + globalThis.nethackGlobal = globalThis.nethackGlobal || {}; + globalThis.nethackGlobal.constants = globalThis.nethackGlobal.constants || {}; + }); + + // create_nhwindow + SET_CONSTANT("WIN_TYPE", NHW_MESSAGE) + SET_CONSTANT("WIN_TYPE", NHW_STATUS) + SET_CONSTANT("WIN_TYPE", NHW_MAP) + SET_CONSTANT("WIN_TYPE", NHW_MENU) + SET_CONSTANT("WIN_TYPE", NHW_TEXT) + + // status_update + SET_CONSTANT("STATUS_FIELD", BL_CHARACTERISTICS) + SET_CONSTANT("STATUS_FIELD", BL_RESET) + SET_CONSTANT("STATUS_FIELD", BL_FLUSH) + SET_CONSTANT("STATUS_FIELD", BL_TITLE) + SET_CONSTANT("STATUS_FIELD", BL_STR) + SET_CONSTANT("STATUS_FIELD", BL_DX) + SET_CONSTANT("STATUS_FIELD", BL_CO) + SET_CONSTANT("STATUS_FIELD", BL_IN) + SET_CONSTANT("STATUS_FIELD", BL_WI) + SET_CONSTANT("STATUS_FIELD", BL_CH) + SET_CONSTANT("STATUS_FIELD", BL_ALIGN) + SET_CONSTANT("STATUS_FIELD", BL_SCORE) + SET_CONSTANT("STATUS_FIELD", BL_CAP) + SET_CONSTANT("STATUS_FIELD", BL_GOLD) + SET_CONSTANT("STATUS_FIELD", BL_ENE) + SET_CONSTANT("STATUS_FIELD", BL_ENEMAX) + SET_CONSTANT("STATUS_FIELD", BL_XP) + SET_CONSTANT("STATUS_FIELD", BL_AC) + SET_CONSTANT("STATUS_FIELD", BL_HD) + SET_CONSTANT("STATUS_FIELD", BL_TIME) + SET_CONSTANT("STATUS_FIELD", BL_HUNGER) + SET_CONSTANT("STATUS_FIELD", BL_HP) + SET_CONSTANT("STATUS_FIELD", BL_HPMAX) + SET_CONSTANT("STATUS_FIELD", BL_LEVELDESC) + SET_CONSTANT("STATUS_FIELD", BL_EXP) + SET_CONSTANT("STATUS_FIELD", BL_CONDITION) + SET_CONSTANT("STATUS_FIELD", MAXBLSTATS) + + // text attributes + SET_CONSTANT("ATTR", ATR_NONE); + SET_CONSTANT("ATTR", ATR_BOLD); + SET_CONSTANT("ATTR", ATR_DIM); + SET_CONSTANT("ATTR", ATR_ULINE); + SET_CONSTANT("ATTR", ATR_BLINK); + SET_CONSTANT("ATTR", ATR_INVERSE); + SET_CONSTANT("ATTR", ATR_URGENT); + SET_CONSTANT("ATTR", ATR_NOHISTORY); + + // conditions + SET_CONSTANT("CONDITION", BL_MASK_BAREH); + SET_CONSTANT("CONDITION", BL_MASK_BLIND); + SET_CONSTANT("CONDITION", BL_MASK_BUSY); + SET_CONSTANT("CONDITION", BL_MASK_CONF); + SET_CONSTANT("CONDITION", BL_MASK_DEAF); + SET_CONSTANT("CONDITION", BL_MASK_ELF_IRON); + SET_CONSTANT("CONDITION", BL_MASK_FLY); + SET_CONSTANT("CONDITION", BL_MASK_FOODPOIS); + SET_CONSTANT("CONDITION", BL_MASK_GLOWHANDS); + SET_CONSTANT("CONDITION", BL_MASK_GRAB); + SET_CONSTANT("CONDITION", BL_MASK_HALLU); + SET_CONSTANT("CONDITION", BL_MASK_HELD); + SET_CONSTANT("CONDITION", BL_MASK_ICY); + SET_CONSTANT("CONDITION", BL_MASK_INLAVA); + SET_CONSTANT("CONDITION", BL_MASK_LEV); + SET_CONSTANT("CONDITION", BL_MASK_PARLYZ); + SET_CONSTANT("CONDITION", BL_MASK_RIDE); + SET_CONSTANT("CONDITION", BL_MASK_SLEEPING); + SET_CONSTANT("CONDITION", BL_MASK_SLIME); + SET_CONSTANT("CONDITION", BL_MASK_SLIPPERY); + SET_CONSTANT("CONDITION", BL_MASK_STONE); + SET_CONSTANT("CONDITION", BL_MASK_STRNGL); + SET_CONSTANT("CONDITION", BL_MASK_STUN); + SET_CONSTANT("CONDITION", BL_MASK_SUBMERGED); + SET_CONSTANT("CONDITION", BL_MASK_TERMILL); + SET_CONSTANT("CONDITION", BL_MASK_TETHERED); + SET_CONSTANT("CONDITION", BL_MASK_TRAPPED); + SET_CONSTANT("CONDITION", BL_MASK_UNCONSC); + SET_CONSTANT("CONDITION", BL_MASK_WOUNDEDL); + SET_CONSTANT("CONDITION", BL_MASK_HOLDING); + + // menu + SET_CONSTANT("MENU_SELECT", PICK_NONE); + SET_CONSTANT("MENU_SELECT", PICK_ONE); + SET_CONSTANT("MENU_SELECT", PICK_ANY); + + // copyright + SET_CONSTANT_STRING("COPYRIGHT", COPYRIGHT_BANNER_A); + SET_CONSTANT_STRING("COPYRIGHT", COPYRIGHT_BANNER_B); + SET_CONSTANT_STRING("COPYRIGHT", COPYRIGHT_BANNER_C); + SET_CONSTANT_STRING("COPYRIGHT", COPYRIGHT_BANNER_D); + + // glyphs + SET_CONSTANT("GLYPH", GLYPH_MON_OFF); + SET_CONSTANT("GLYPH", GLYPH_PET_OFF); + SET_CONSTANT("GLYPH", GLYPH_INVIS_OFF); + SET_CONSTANT("GLYPH", GLYPH_DETECT_OFF); + SET_CONSTANT("GLYPH", GLYPH_BODY_OFF); + SET_CONSTANT("GLYPH", GLYPH_RIDDEN_OFF); + SET_CONSTANT("GLYPH", GLYPH_OBJ_OFF); + SET_CONSTANT("GLYPH", GLYPH_CMAP_OFF); + SET_CONSTANT("GLYPH", GLYPH_EXPLODE_OFF); + SET_CONSTANT("GLYPH", GLYPH_ZAP_OFF); + SET_CONSTANT("GLYPH", GLYPH_SWALLOW_OFF); + SET_CONSTANT("GLYPH", GLYPH_WARNING_OFF); + SET_CONSTANT("GLYPH", GLYPH_STATUE_OFF); + SET_CONSTANT("GLYPH", GLYPH_UNEXPLORED_OFF); + SET_CONSTANT("GLYPH", GLYPH_NOTHING_OFF); + SET_CONSTANT("GLYPH", MAX_GLYPH); + SET_CONSTANT("GLYPH", NO_GLYPH); + SET_CONSTANT("GLYPH", GLYPH_INVISIBLE); + SET_CONSTANT("GLYPH", GLYPH_UNEXPLORED); + SET_CONSTANT("GLYPH", GLYPH_NOTHING); + + // colors + SET_CONSTANT("COLORS", CLR_BLACK); + SET_CONSTANT("COLORS", CLR_RED); + SET_CONSTANT("COLORS", CLR_GREEN); + SET_CONSTANT("COLORS", CLR_BROWN); + SET_CONSTANT("COLORS", CLR_BLUE); + SET_CONSTANT("COLORS", CLR_MAGENTA); + SET_CONSTANT("COLORS", CLR_CYAN); + SET_CONSTANT("COLORS", CLR_GRAY); + SET_CONSTANT("COLORS", NO_COLOR); + SET_CONSTANT("COLORS", CLR_ORANGE); + SET_CONSTANT("COLORS", CLR_BRIGHT_GREEN); + SET_CONSTANT("COLORS", CLR_YELLOW); + SET_CONSTANT("COLORS", CLR_BRIGHT_BLUE); + SET_CONSTANT("COLORS", CLR_BRIGHT_MAGENTA); + SET_CONSTANT("COLORS", CLR_BRIGHT_CYAN); + SET_CONSTANT("COLORS", CLR_WHITE); + SET_CONSTANT("COLORS", CLR_MAX); + + // color attributes (?) + SET_CONSTANT("COLOR_ATTR", HL_ATTCLR_DIM); + SET_CONSTANT("COLOR_ATTR", HL_ATTCLR_BLINK); + SET_CONSTANT("COLOR_ATTR", HL_ATTCLR_ULINE); + SET_CONSTANT("COLOR_ATTR", HL_ATTCLR_INVERSE); + SET_CONSTANT("COLOR_ATTR", HL_ATTCLR_BOLD); + SET_CONSTANT("COLOR_ATTR", BL_ATTCLR_MAX); +} + +/*** + * Globals + ***/ +#define CREATE_GLOBAL(var, type) create_global(#var, (void *)&var, type); +#define CREATE_GLOBAL_FROM_ARRAY(base, iter, path, end_expr, type) \ + for(iter = 0; end_expr; iter++) { \ + snprintf(buf, BUFSZ, #base ".%d." #path, iter); \ + create_global(buf, (void *)(&(base[iter].path)), type); \ + } + +void create_global (char *name, void *ptr, char *type); + +void js_globals_init() { + // int i; + // char buf[BUFSZ]; + printf("js_globals_init\n"); + + EM_ASM({ + globalThis.nethackGlobal = globalThis.nethackGlobal || {}; + globalThis.nethackGlobal.globals = globalThis.nethackGlobal.globals || {}; + }); + + /* globals */ + CREATE_GLOBAL(g.plname, "s"); + + /* window globals */ + CREATE_GLOBAL(WIN_MAP, "i"); + CREATE_GLOBAL(WIN_MESSAGE, "i"); + CREATE_GLOBAL(WIN_INVEN, "i"); + CREATE_GLOBAL(WIN_STATUS, "i"); +} + +EM_JS(void, create_global, (char *name_str, void *ptr, char *type_str), { + let name = UTF8ToString(name_str); + let type = UTF8ToString(type_str); + + // get helpers + let getPointerValue = globalThis.nethackGlobal.helpers.getPointerValue; + let setPointerValue = globalThis.nethackGlobal.helpers.setPointerValue; + + let { obj, prop } = createPath(globalThis.nethackGlobal.globals, name); + + // setters / getters with bound pointers + Object.defineProperty(obj, prop, { + get: getPointerValue.bind(null, name, ptr, type), + set: setPointerValue.bind(null, name, ptr, type), + configurable: true, + enumerable: true + }); + + function createPath(obj, path) { + path = path.split("."); + let i; + for (i = 0; i < path.length - 1; i++) { + // obj[path[i]] = obj[path[i]] || {}; + if (obj[path[i]] === undefined) { + obj[path[i]] = {}; + } + obj = obj[path[i]]; + } + + return { obj, prop: path[i] }; + } +}) + +#endif + +/*libnethack.c*/ diff --git a/sys/lib/npm-package/README.md b/sys/lib/npm-package/README.md index e39db8292..2c9b96938 100644 --- a/sys/lib/npm-package/README.md +++ b/sys/lib/npm-package/README.md @@ -15,6 +15,11 @@ The main module returns a setup function: `startNethack(uiCallback, moduleOption * `moduleOptions` - An optional [emscripten Module object](https://emscripten.org/docs/api_reference/module.html) for configuring the WASM that will be run. * `Module.arguments` - Of note is the [arguments property](https://emscripten.org/docs/api_reference/module.html#Module.arguments) which gets passed to NetHack as its [command line parameters](https://nethackwiki.com/wiki/Options). +There are a number of auxilary functions and variables that may help with building your applications. All of these are under `globalThis.nethackOptions`. Use `console.log(globalThis.nethackOptions)` for a full list of options. Some worth mentioning are: +* `globalThis.nethackOptions.helpers` - Helper functions that are useful for NetHack windowing ports + * `globalThis.nethackOptions.mapglyphHelper` - Converts an integer glyph into a character to be displayed. Useful if you are using ASCII characters for representing NetHack (as opposed to tiles). Interface is `mapglyphHelper(glyph, x, y, mgflags)` and will typically be called as part of the `shim_print_glyph` function. +* `globalThis.nethackOptions.constants` - A Object full of constants that are `#define`'d in NetHack's C code. Useful for translating to / from numbers in the APIs and return values. + ## Example ``` js let nethackStart = require("nethack"); diff --git a/sys/lib/npm-package/package-lock.json b/sys/lib/npm-package/package-lock.json new file mode 100644 index 000000000..fb5f7b7e1 --- /dev/null +++ b/sys/lib/npm-package/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@neth4ck/neth4ck", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/sys/lib/npm-package/package.json b/sys/lib/npm-package/package.json index b4e931559..e39a7d201 100644 --- a/sys/lib/npm-package/package.json +++ b/sys/lib/npm-package/package.json @@ -1,6 +1,6 @@ { "name": "@neth4ck/neth4ck", - "version": "1.0.0", + "version": "1.0.1", "description": "The original NetHack rogue-like game built as a WebAssembly module", "main": "src/nethackShim.js", "scripts": { @@ -13,7 +13,12 @@ "nethack", "rogue", "rogue-like", - "game" + "roguelike", + "dungeon", + "dungeons", + "game", + "rpg", + "dnd" ], "author": "Adam Powers ", "license": "SEE LICENSE IN LICENSE.md" diff --git a/sys/lib/npm-package/test/test.js b/sys/lib/npm-package/test/test.js new file mode 100644 index 000000000..3032399e3 --- /dev/null +++ b/sys/lib/npm-package/test/test.js @@ -0,0 +1,55 @@ +let nethackStart = require("../src/nethackShim.js"); +Error.stackTraceLimit = 20; + +// debugging to make sure the JavaScript event loop isn't blocked +// const {performance} = require("perf_hooks"); +// let currentTime = 0; +// let lastTime = 0; +// setInterval(() => { +// lastTime = currentTime; +// currentTime = performance.now(); +// console.log("Time since last JavaScript loop:", currentTime-lastTime); +// }, 10); + +let Module = {}; +let winCount = 0; + +/* global globalThis */ +nethackStart(async function (name, ... args) { + switch(name) { + case "shim_init_nhwindows": + console.log("globalThis.nethackGlobal", globalThis.nethackGlobal); + break; + case "shim_create_nhwindow": + winCount++; + console.log("creating window", args, "returning", winCount); + return winCount; + case "shim_print_glyph": + var x = args[1]; + var y = args[2]; + var glyph = args[3]; + + var ret = globalThis.nethackGlobal.helpers.mapglyphHelper(glyph, x, y, 0); + console.log(`GLYPH (${x},${y}): ${String.fromCharCode(ret.ch)}`); + return; + // case "shim_update_inventory": + // globalThis.nethackGlobal.helpers.displayInventory(); + // return; + case "shim_select_menu": + return await selectMenu(...args); + case "shim_yn_function": + case "shim_message_menu": + return 121; // 'y' + case "shim_nhgetch": + case "shim_nh_poskey": + return 0; + default: + console.log(`called doGraphics: ${name} [${args}]`); + return 0; + } +}, Module); + +async function selectMenu(window, how, selected) { + Module.setValue(selected, 0, "*"); + return -1; +} \ No newline at end of file diff --git a/sys/lib/sysconf b/sys/lib/sysconf index 77b4c383f..2e1b66e12 100644 --- a/sys/lib/sysconf +++ b/sys/lib/sysconf @@ -146,5 +146,6 @@ PANICTRACE_LIBC=0 # option settings via NETHACKOPTIONS in their environment or via # ~/.nethackrc run-time configuration file. #OPTIONS=!autopickup,fruit:tomato,symset:DECgraphics +OPTIONS=perm_invent #eof diff --git a/win/shim/winshim.c b/win/shim/winshim.c index eec616f4f..2910f0be6 100644 --- a/win/shim/winshim.c +++ b/win/shim/winshim.c @@ -51,6 +51,7 @@ ret_type name fn_args { \ 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; \ } @@ -60,6 +61,7 @@ void name fn_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__ */ @@ -81,6 +83,7 @@ ret_type name fn_args { \ 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; \ } @@ -89,6 +92,7 @@ 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__ */ @@ -133,9 +137,9 @@ VDECLCB(shim_add_menu, "viipiiisi", A2P window, A2P glyph, 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) -DECLCB(int, shim_select_menu,(winid window, int how, MENU_ITEM_P **menu_list), "iiip", A2P window, A2P how, P2V menu_list) +/* 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_update_inventory,(void), "v") VDECLCB(shim_mark_synch,(void), "v") VDECLCB(shim_wait_synch,(void), "v") VDECLCB(shim_cliparound,(int x, int y), "vii", A2P x, A2P y) @@ -144,11 +148,11 @@ VDECLCB(shim_print_glyph,(winid w, int x, int y, int glyph, int bkglyph), "viiii 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), "ippp", P2V x, P2V y, P2V mod) +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), "vsp", P2V query, P2V bufp) +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") @@ -168,11 +172,24 @@ 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), - "vipiiip", + "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", @@ -234,13 +251,23 @@ struct window_procs shim_procs = { #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(async () => { + // 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 = Module.UTF8ToString(shim_name); - let fmt = Module.UTF8ToString(fmt_str); - let cbName = Module.UTF8ToString(cb_name); + 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(); @@ -248,53 +275,117 @@ EM_JS(void, local_callback, (const char *cb_name, const char *shim_name, void *r let jsArgs = []; for (let i = 0; i < argTypes.length; i++) { let ptr = args + (4*i); - let val = typeLookup(argTypes[i], ptr); + let val = getArg(name, ptr, argTypes[i]); jsArgs.push(val); } + decodeArgs(name, jsArgs); + // do the callback let userCallback = globalThis[cbName]; - let retVal = await runJsLoop(() => userCallback(name, ... jsArgs)); + runJsEventLoop(() => userCallback.call(this, name, ... jsArgs)).then((retVal) => { + // save the return value + setPointerValue(name, ret_ptr, retType, retVal); + // return + setTimeout(() => { + reentryMutexUnlock(); + wakeUp(); + }, 0); + }); - // save the return value - setReturn(name, ret_ptr, retType, retVal); + // make callback arguments friendly: convert numbers to strings where possible + function decodeArgs(name, args) { + // if (!globalThis.nethackGlobal.makeArgsFriendly) return; - // convert 'ptr' to the type indicated by 'type' - function typeLookup(type, ptr) { - switch(type) { - case "s": // string - return Module.UTF8ToString(Module.getValue(ptr, "*")); - case "p": // pointer - ptr = Module.getValue(ptr, "*"); - if(!ptr) return 0; // null pointer - return Module.getValue(ptr, "*"); - case "c": // char - return String.fromCharCode(Module.getValue(Module.getValue(ptr, "*"), "i8")); - case "0": /* 2^0 = 1 byte */ - return Module.getValue(Module.getValue(ptr, "*"), "i8"); - case "1": /* 2^1 = 2 bytes */ - return Module.getValue(Module.getValue(ptr, "*"), "i16"); - case "2": /* 2^2 = 4 bytes */ - case "i": // integer - case "n": // number - return Module.getValue(Module.getValue(ptr, "*"), "i32"); - case "f": // float - return Module.getValue(Module.getValue(ptr, "*"), "float"); - case "d": // double - return Module.getValue(Module.getValue(ptr, "*"), "double"); - default: - throw new TypeError ("unknown type:" + type); + switch(name) { + case "shim_create_nhwindow": + args[0] = globalThis.nethackGlobal.constants["WIN_TYPE"][args[0]]; + break; + case "shim_status_update": + // which field is being updated? + args[0] = globalThis.nethackGlobal.constants["STATUS_FIELD"][args[0]]; + // arg[1] is a string unless it is BL_CONDITION, BL_RESET, BL_FLUSH, BL_CHARACTERISTICS + if(["BL_CONDITION", "BL_RESET", "BL_FLUSH", "BL_CHARACTERISTICS"].indexOf(args[0] && args[1]) < 0) { + args[1] = getArg(name, args[1], "s"); + } else { + args[1] = getArg(name, args[1], "p"); + } + break; + case "shim_display_file": + args[1] = !!args[1]; + case "shim_display_nhwindow": + args[0] = decodeWindow(args[0]); + args[1] = !!args[1]; + break; + case "shim_getmsghistory": + args[0] = !!args[0]; + break; + case "shim_putmsghistory": + args[1] = !!args[1]; + break; + case "shim_status_enablefield": + console.log("shim_status_enablefield arg 1:", args[1]); + args[3] = !!args[3]; + break; + case "shim_add_menu": + args[0] = decodeWindow(args[0]); + // args[1] = mapglyphHelper(args[1]); + // args[5] = decodeAttr(args[5]); + break; + case "shim_putstr": + args[0] = decodeWindow(args[0]); + break; + case "shim_select_menu": + args[0] = decodeWindow(args[0]); + args[1] = decodeSelected(args[1]); + break; + case "shim_clear_nhwindow": + case "shim_destroy_nhwindow": + case "shim_curs": + case "shim_start_menu": + case "shim_end_menu": + case "shim_print_glyph": + args[0] = decodeWindow(args[0]); + break; } } - // setTimeout() with value of '0' is similar to setImmediate() (which isn't standard) + function decodeWindow(winid) { + let { WIN_MAP, WIN_INVEN, WIN_STATUS, WIN_MESSAGE } = globalThis.nethackGlobal.globals; + switch(winid) { + case WIN_MAP: return "WIN_MAP"; + case WIN_MESSAGE: return "WIN_MESSAGE"; + case WIN_STATUS: return "WIN_STATUS"; + case WIN_INVEN: return "WIN_INVEN"; + default: return winid; + } + // return winid; + } + + function decodeSelected(how) { + let { PICK_NONE, PICK_ONE, PICK_ANY } = globalThis.nethackGlobal.constants.MENU_SELECT; + switch(how) { + case PICK_NONE: return "PICK_NONE"; + case PICK_ONE: return "PICK_ONE"; + case PICK_ANY: return "PICK_ANY"; + default: return how; + } + + } + + 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; + // 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 - async function runJsLoop(cb) { + // 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()); @@ -302,48 +393,16 @@ EM_JS(void, local_callback, (const char *cb_name, const char *shim_name, void *r }); } - // sets the return value of the function to the type expected - function setReturn(name, ptr, type, value = 0) { - switch (type) { - case "p": - throw new Error("not implemented"); - case "s": - if(typeof value !== "string") - throw new TypeError(`expected ${name} return type to be string`); - value=value?value:"(no value)"; - var strPtr = Module.getValue(ptr, "i32"); - Module.stringToUTF8(value, strPtr, 1024); - break; - case "i": - if(typeof value !== "number" || !Number.isInteger(value)) - throw new TypeError(`expected ${name} return type to be integer`); - Module.setValue(ptr, value, "i32"); - break; - case "c": - if(typeof value !== "number" || value < 0 || value > 128) - throw new TypeError(`expected ${name} return type to be integer representing an ASCII character`); - Module.setValue(ptr, value, "i8"); - break; - case "f": - if(typeof value !== "number" || isFloat(value)) - throw new TypeError(`expected ${name} return type to be float`); - // XXX: I'm not sure why 'double' works and 'float' doesn't - Module.setValue(ptr, value, "double"); - break; - case "d": - if(typeof value !== "number" || isFloat(value)) - throw new TypeError(`expected ${name} return type to be float`); - Module.setValue(ptr, value, "double"); - break; - case "v": - break; - default: - throw new Error("unknown type"); + 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 isFloat(n){ - return n === +n && n !== (n|0) && !Number.isInteger(n); - } + function reentryMutexUnlock() { + globalThis.nethackGlobal.shimFunctionRunning = null; } }); })