Merge branch 'libnethack' of https://github.com/apowers313/NetHack into libnethack2

This commit is contained in:
nhmall
2020-10-04 19:51:37 -04:00
10 changed files with 1375 additions and 104 deletions

7
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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

1127
sys/lib/libnethackmain.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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");

5
sys/lib/npm-package/package-lock.json generated Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "@neth4ck/neth4ck",
"version": "1.0.0",
"lockfileVersion": 1
}

View File

@@ -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 <apowers@ato.ms>",
"license": "SEE LICENSE IN LICENSE.md"

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
});
})