/* NetHack 5.0	nhlua.c	$NHDT-Date: 1744963460 2025/04/18 00:04:20 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.153 $ */
/*      Copyright (c) 2018 by Pasi Kallinen */
/* NetHack may be freely redistributed.  See license for details. */

#include "hack.h"
#include "dlb.h"

#ifndef LUA_VERSION_RELEASE_NUM
#ifdef NHL_SANDBOX
#undef NHL_SANDBOX
#endif
#endif

#ifdef NHL_SANDBOX
#include <setjmp.h>
#endif

/*
#- include <lua5.3/lua.h>
#- include <lua5.3/lualib.h>
#- include <lua5.3/lauxlib.h>
*/

/*  */

struct e;

#ifndef SFCTOOL
/* lua_CFunction prototypes */
#ifdef DUMPLOG
staticfn int nhl_dump_fmtstr(lua_State *);
#endif /* DUMPLOG */
staticfn int nhl_dnum_name(lua_State *);
staticfn int nhl_int_to_pm_name(lua_State *);
staticfn int nhl_int_to_obj_name(lua_State *);
staticfn int nhl_stairways(lua_State *);
staticfn int nhl_pushkey(lua_State *);
staticfn int nhl_doturn(lua_State *);
staticfn int nhl_debug_flags(lua_State *);
staticfn int nhl_flip_level(lua_State *);
staticfn int nhl_timer_has_at(lua_State *);
staticfn int nhl_timer_peek_at(lua_State *);
staticfn int nhl_timer_stop_at(lua_State *);
staticfn int nhl_timer_start_at(lua_State *);
staticfn int nhl_get_cmd_key(lua_State *);
staticfn int nhl_callback(lua_State *);
staticfn int nhl_gamestate(lua_State *);
staticfn int nhl_test(lua_State *);
staticfn int nhl_getmap(lua_State *);
staticfn char splev_typ2chr(schar);
staticfn int nhl_gettrap(lua_State *);
staticfn int nhl_deltrap(lua_State *);
#if 0
staticfn int nhl_setmap(lua_State *);
#endif
staticfn int nhl_impossible(lua_State *);
staticfn int nhl_pline(lua_State *);
staticfn int nhl_verbalize(lua_State *);
staticfn int nhl_parse_config(lua_State *);
staticfn int nhl_menu(lua_State *);
staticfn int nhl_text(lua_State *);
staticfn int nhl_getlin(lua_State *);
staticfn int nhl_makeplural(lua_State *);
staticfn int nhl_makesingular(lua_State *);
staticfn int nhl_s_suffix(lua_State *);
staticfn int nhl_ing_suffix(lua_State *);
staticfn int nhl_an(lua_State *);
staticfn int nhl_rn2(lua_State *);
staticfn int nhl_random(lua_State *);
staticfn int nhl_level_difficulty(lua_State *);
staticfn int nhl_is_genocided(lua_State *);
staticfn int nhl_get_debug_themerm_name(lua_State *);
staticfn void init_nhc_data(lua_State *);
staticfn int nhl_push_anything(lua_State *, int, void *);
staticfn int nhl_meta_u_index(lua_State *);
staticfn int nhl_meta_u_newindex(lua_State *);
staticfn int nhl_u_clear_inventory(lua_State *);
staticfn int nhl_u_giveobj(lua_State *);
staticfn void init_u_data(lua_State *);
#ifdef notyet
staticfn int nhl_set_package_path(lua_State *, const char *);
#endif
staticfn int traceback_handler(lua_State *);
staticfn uint32_t nhl_getmeminuse(lua_State *);
#ifdef NHL_SANDBOX
staticfn void nhlL_openlibs(lua_State *, uint32_t);
#endif
staticfn void *nhl_alloc(void *, void *, size_t, size_t);
staticfn lua_State *nhlL_newstate (nhl_sandbox_info *, const char *);
staticfn void end_luapat(void);
staticfn int nhl_get_config(lua_State *);
staticfn int nhl_variable(lua_State *);
staticfn void nhl_clearfromtable(lua_State *, int, int, struct e *);
staticfn void nhl_warn(void *, const char *, int);
staticfn void nhl_clearfromtable(lua_State *, int, int, struct e *);
staticfn int nhl_panic(lua_State *);
staticfn void nhl_hookfn(lua_State *, lua_Debug *);
#endif /* !SFCTOOL */

#ifndef SFCTOOL
static const char *const nhcore_call_names[NUM_NHCORE_CALLS] = {
    "start_new_game",
    "restore_old_game",
    "moveloop_turn",
    "game_exit",
    "getpos_tip",
    "enter_tutorial",
    "leave_tutorial",
};
static boolean nhcore_call_available[NUM_NHCORE_CALLS];
#endif

/* internal structure that hangs off L->ud (but use lua_getallocf() )
 * Note that we use it for both memory use tracking and instruction counting.
 */
typedef struct nhl_user_data {
    lua_State *L;       /* because the allocator needs it */
    uint32_t flags;     /* from nhl_sandbox_info */

    uint32_t memlimit;

    uint32_t steps;     /* current counter */
    uint32_t osteps;    /* original steps value */
    uint32_t perpcall;  /* per pcall steps value */

    /* stats */
    uint32_t statctr;   /* stats step reload count */
    int sid;            /* id number (per state) */
    const char *name;   /* for stats logging (per pcall) */

#ifdef NHL_SANDBOX
    jmp_buf jb;
#endif
} nhl_user_data;

#ifndef SFCTOOL
static lua_State *luapat;   /* instance for file pattern matching */

void
l_nhcore_init(void)
{
    nhl_sandbox_info sbi = { NHL_SB_SAFE, 1 * 1024 * 1024, 0,
                             1 * 1024 * 1024 };

    if ((gl.luacore = nhl_init(&sbi)) != 0) {
        if (!nhl_loadlua(gl.luacore, "nhcore.lua")) {
            gl.luacore = (lua_State *) 0;
        } else {
            int i;

            for (i = 0; i < NUM_NHCORE_CALLS; i++)
                nhcore_call_available[i] = TRUE;
        }
    } else
        impossible("l_nhcore_init failed");
}

void
l_nhcore_done(void)
{
    if (gl.luacore) {
        nhl_done(gl.luacore);
        gl.luacore = 0;
    }
    end_luapat();
}

void
l_nhcore_call(int callidx)
{
    int ltyp;

    if (callidx < 0 || callidx >= NUM_NHCORE_CALLS || !gl.luacore
        || !nhcore_call_available[callidx])
        return;

    lua_getglobal(gl.luacore, "nhcore");
    if (!lua_istable(gl.luacore, -1)) {
        /*impossible("nhcore is not a lua table");*/
        nhl_done(gl.luacore);
        gl.luacore = 0;
        return;
    }

    lua_getfield(gl.luacore, -1, nhcore_call_names[callidx]);
    ltyp = lua_type(gl.luacore, -1);
    if (ltyp == LUA_TFUNCTION) {
        nhl_pcall_handle(gl.luacore, 0, 1, "l_nhcore_call", NHLpa_panic);
    } else {
        /*impossible("nhcore.%s is not a lua function",
          nhcore_call_names[callidx]);*/
        nhcore_call_available[callidx] = FALSE;
    }
}

DISABLE_WARNING_UNREACHABLE_CODE

ATTRNORETURN void
nhl_error(lua_State *L, const char *msg)
{
    lua_Debug ar;
    char buf[BUFSZ * 2];

    lua_getstack(L, 1, &ar);
    lua_getinfo(L, "lS", &ar);
    Sprintf(buf, "%s (line %d ", msg, ar.currentline);
    Sprintf(eos(buf), "%.*s)",
            /* (max length of ar.short_src is actually LUA_IDSIZE
               so this is overkill for it, but crucial for ar.source) */
            (int) (sizeof buf - (strlen(buf) + sizeof ")")),
            ar.short_src); /* (used to be 'ar.source' here) */
    lua_pushstring(L, buf);
#if 0 /* defined(PANICTRACE) && !defined(NO_SIGNALS) */
    panictrace_setsignals(FALSE);
#endif
    (void) lua_error(L);
    /*NOTREACHED*/
}

RESTORE_WARNING_UNREACHABLE_CODE

/* Check that parameters are nothing but single table,
   or if no parameters given, put empty table there */
void
lcheck_param_table(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc < 1)
        lua_createtable(L, 0, 0);

    /* discard any extra arguments passed in */
    lua_settop(L, 1);

    luaL_checktype(L, 1, LUA_TTABLE);
}

DISABLE_WARNING_UNREACHABLE_CODE

schar
get_table_mapchr(lua_State *L, const char *name)
{
    char *ter;
    xint8 typ;

    ter = get_table_str(L, name);
    typ = check_mapchr(ter);
    if (typ == INVALID_TYPE)
        nhl_error(L, "Erroneous map char");
    if (ter)
        free(ter);
    return typ;
}

schar
get_table_mapchr_opt(lua_State *L, const char *name, schar defval)
{
    char *ter;
    xint8 typ;

    ter = get_table_str_opt(L, name, emptystr);
    if (ter && *ter) {
        typ = (xint8) check_mapchr(ter);
        if (typ == INVALID_TYPE)
            nhl_error(L, "Erroneous map char");
    } else
        typ = (xint8) defval;
    if (ter)
        free(ter);
    return typ;
}

short
nhl_get_timertype(lua_State *L, int idx)
{
    /* these are in the same order as enum timeout_types in timeout.h and
       ttable timeout_funcs[] in timeout.c, although not spelled the same */
    static const char *const timerstr[NUM_TIME_FUNCS + 1] = {
        "rot-organic", "rot-corpse", "revive-mon",    "zombify-mon",
        "burn-obj",    "hatch-egg",  "fig-transform", "shrink-glob",
        "melt-ice",    NULL
    };
    short ret = luaL_checkoption(L, idx, NULL, timerstr);

    if (ret < 0 || ret >= NUM_TIME_FUNCS)
        nhl_error(L, "Unknown timer type");
    return ret;
}

RESTORE_WARNING_UNREACHABLE_CODE

void
nhl_add_table_entry_int(lua_State *L, const char *name, lua_Integer value)
{
    lua_pushstring(L, name);
    lua_pushinteger(L, value);
    lua_rawset(L, -3);
}

void
nhl_add_table_entry_char(lua_State *L, const char *name, char value)
{
    char buf[2];
    Sprintf(buf, "%c", value);
    lua_pushstring(L, name);
    lua_pushstring(L, buf);
    lua_rawset(L, -3);
}

void
nhl_add_table_entry_str(lua_State *L, const char *name, const char *value)
{
    lua_pushstring(L, name);
    lua_pushstring(L, value);
    lua_rawset(L, -3);
}
void
nhl_add_table_entry_bool(lua_State *L, const char *name, boolean value)
{
    lua_pushstring(L, name);
    lua_pushboolean(L, value);
    lua_rawset(L, -3);
}

void
nhl_add_table_entry_region(lua_State *L, const char *name, coordxy x1,
                           coordxy y1, coordxy x2, coordxy y2)
{
    lua_pushstring(L, name);
    lua_newtable(L);
    nhl_add_table_entry_int(L, "x1", x1);
    nhl_add_table_entry_int(L, "y1", y1);
    nhl_add_table_entry_int(L, "x2", x2);
    nhl_add_table_entry_int(L, "y2", y2);
    lua_rawset(L, -3);
}

/* converting from special level "map character" to levl location type
   and back. order here is important. */
static const struct {
    char ch;
    schar typ;
} char2typ[] = {
                { ' ', STONE },
                { '#', CORR },
                { '.', ROOM },
                { '-', HWALL },
                { '-', TLCORNER },
                { '-', TRCORNER },
                { '-', BLCORNER },
                { '-', BRCORNER },
                { '-', CROSSWALL },
                { '-', TUWALL },
                { '-', TDWALL },
                { '-', TLWALL },
                { '-', TRWALL },
                { '-', DBWALL },
                { '|', VWALL },
                { '+', DOOR },
                { 'A', AIR },
                { 'C', CLOUD },
                { 'S', SDOOR },
                { 'H', SCORR },
                { '{', FOUNTAIN },
                { '\\', THRONE },
                { 'K', SINK },
                { '}', MOAT },
                { 'P', POOL },
                { 'L', LAVAPOOL },
                { 'Z', LAVAWALL },
                { 'I', ICE },
                { 'W', WATER },
                { 'T', TREE },
                { 'F', IRONBARS }, /* Fe = iron */
                { 'x', MAX_TYPE }, /* "see-through" */
                { 'B', CROSSWALL }, /* hack: boundary location */
                { 'w', MATCH_WALL }, /* IS_STWALL() */
                { '\0', STONE },
};

schar
splev_chr2typ(char c)
{
    int i;

    for (i = 0; char2typ[i].ch; i++)
        if (c == char2typ[i].ch)
            return char2typ[i].typ;
    return (INVALID_TYPE);
}

schar
check_mapchr(const char *s)
{
    if (s && strlen(s) == 1)
        return splev_chr2typ(s[0]);
    return INVALID_TYPE;
}

staticfn char
splev_typ2chr(schar typ)
{
    int i;

    for (i = 0; char2typ[i].typ < MAX_TYPE; i++)
        if (typ == char2typ[i].typ)
            return char2typ[i].ch;
    return 'x';
}

DISABLE_WARNING_UNREACHABLE_CODE

/* local t = nh.gettrap(x,y); */
/* local t = nh.gettrap({ x = 10, y = 10 }); */
staticfn int
nhl_gettrap(lua_State *L)
{
    lua_Integer lx, ly;
    coordxy x, y;

    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "Incorrect arguments");
        /*NOTREACHED*/
        return 0;
    }
    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (isok(x, y)) {
        struct trap *ttmp = t_at(x, y);

        if (ttmp) {
            lua_newtable(L);

            nhl_add_table_entry_int(L, "tx", ttmp->tx);
            nhl_add_table_entry_int(L, "ty", ttmp->ty);
            nhl_add_table_entry_int(L, "ttyp", ttmp->ttyp);
            nhl_add_table_entry_str(L, "ttyp_name",
                                    get_trapname_bytype(ttmp->ttyp));
            nhl_add_table_entry_bool(L, "tseen", ttmp->tseen);
            nhl_add_table_entry_bool(L, "madeby_u", ttmp->madeby_u);
            nhl_add_table_entry_bool(L, "once", ttmp->once);
            switch (ttmp->ttyp) {
            case SQKY_BOARD:
                nhl_add_table_entry_int(L, "tnote", ttmp->tnote);
                break;
            case ROLLING_BOULDER_TRAP:
                nhl_add_table_entry_int(L, "launchx", ttmp->launch.x);
                nhl_add_table_entry_int(L, "launchy", ttmp->launch.y);
                nhl_add_table_entry_int(L, "launch2x", ttmp->launch2.x);
                nhl_add_table_entry_int(L, "launch2y", ttmp->launch2.y);
                break;
            case PIT:
            case SPIKED_PIT:
                nhl_add_table_entry_int(L, "conjoined", ttmp->conjoined);
                break;
            }
            return 1;
        } else {
            nhl_error(L, "No trap at location");
        }
    } else {
        nhl_error(L, "Coordinates out of range");
    }
    return 0;
}

/* nh.deltrap(x,y); nh.deltrap({ x = 10, y = 15 }); */
staticfn int
nhl_deltrap(lua_State *L)
{
    lua_Integer lx, ly;
    coordxy x, y;

    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "Incorrect arguments");
        /*NOTREACHED*/
        return 0;
    }
    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (isok(x, y)) {
        struct trap *ttmp = t_at(x, y);

        if (ttmp)
            deltrap(ttmp);
    }
    return 0;
}

RESTORE_WARNING_UNREACHABLE_CODE

/* get parameters (XX,YY) or ({ x = XX, y = YY }) or ({ XX, YY }),
   and set the x and y values.
   return TRUE if there are such params in the stack.
   Note that this does not adjust the values of x and y at all from what is
   specified in the level file; so, it returns absolute coordinates rather
   than map-relative coordinates. Callers of this function must decide if
   they want to interpret the values as absolute or as map-relative, and
   adjust accordingly.
 */
boolean
nhl_get_xy_params(lua_State *L, lua_Integer *x, lua_Integer *y)
{
    int argc = lua_gettop(L);
    boolean ret = FALSE;

    if (argc == 2) {
        *x = lua_tointeger(L, 1);
        *y = lua_tointeger(L, 2);
        ret = TRUE;
    } else if (argc == 1 && lua_type(L, 1) == LUA_TTABLE) {
        lua_Integer ax, ay;

        ret = get_coord(L, 1, &ax, &ay);
        *x = ax;
        *y = ay;
    }
    return ret;
}

DISABLE_WARNING_UNREACHABLE_CODE

/* local loc = nh.getmap(x,y); */
/* local loc = nh.getmap({ x = 10, y = 35 }); */
staticfn int
nhl_getmap(lua_State *L)
{
    lua_Integer lx, ly;
    coordxy x, y;

    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "Incorrect arguments");
        return 0;
    }
    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (isok(x, y)) {
        char buf[BUFSZ];
        lua_newtable(L);

        /* FIXME: some should be boolean values */
        nhl_add_table_entry_int(L, "glyph", levl[x][y].glyph);
        nhl_add_table_entry_int(L, "typ", levl[x][y].typ);
        nhl_add_table_entry_str(L, "typ_name",
                                levltyp_to_name(levl[x][y].typ));
        Sprintf(buf, "%c", splev_typ2chr(levl[x][y].typ));
        nhl_add_table_entry_str(L, "mapchr", buf);
        nhl_add_table_entry_int(L, "seenv", levl[x][y].seenv);
        nhl_add_table_entry_bool(L, "horizontal", levl[x][y].horizontal);
        nhl_add_table_entry_bool(L, "lit", levl[x][y].lit);
        nhl_add_table_entry_bool(L, "waslit", levl[x][y].waslit);
        nhl_add_table_entry_int(L, "roomno", levl[x][y].roomno);
        nhl_add_table_entry_bool(L, "edge", levl[x][y].edge);
        nhl_add_table_entry_bool(L, "candig", levl[x][y].candig);

        nhl_add_table_entry_bool(L, "has_trap", t_at(x, y) ? 1 : 0);

        /* TODO: FIXME: levl[x][y].flags */

        lua_pushliteral(L, "flags");
        lua_newtable(L);

        if (IS_DOOR(levl[x][y].typ)) {
            nhl_add_table_entry_bool(L, "nodoor",
                                     (levl[x][y].flags == D_NODOOR));
            nhl_add_table_entry_bool(L, "broken",
                                     (levl[x][y].flags & D_BROKEN));
            nhl_add_table_entry_bool(L, "isopen",
                                     (levl[x][y].flags & D_ISOPEN));
            nhl_add_table_entry_bool(L, "closed",
                                     (levl[x][y].flags & D_CLOSED));
            nhl_add_table_entry_bool(L, "locked",
                                     (levl[x][y].flags & D_LOCKED));
            nhl_add_table_entry_bool(L, "trapped",
                                     (levl[x][y].flags & D_TRAPPED));
        } else if (IS_ALTAR(levl[x][y].typ)) {
            /* TODO: bits 0, 1, 2 */
            nhl_add_table_entry_bool(L, "shrine",
                                     (levl[x][y].flags & AM_SHRINE));
        } else if (IS_THRONE(levl[x][y].typ)) {
            nhl_add_table_entry_bool(L, "looted",
                                     (levl[x][y].flags & T_LOOTED));
        } else if (levl[x][y].typ == TREE) {
            nhl_add_table_entry_bool(L, "looted",
                                     (levl[x][y].flags & TREE_LOOTED));
            nhl_add_table_entry_bool(L, "swarm",
                                     (levl[x][y].flags & TREE_SWARM));
        } else if (IS_FOUNTAIN(levl[x][y].typ)) {
            nhl_add_table_entry_bool(L, "looted",
                                     (levl[x][y].flags & F_LOOTED));
            nhl_add_table_entry_bool(L, "warned",
                                     (levl[x][y].flags & F_WARNED));
        } else if (IS_SINK(levl[x][y].typ)) {
            nhl_add_table_entry_bool(L, "pudding",
                                     (levl[x][y].flags & S_LPUDDING));
            nhl_add_table_entry_bool(L, "dishwasher",
                                     (levl[x][y].flags & S_LDWASHER));
            nhl_add_table_entry_bool(L, "ring", (levl[x][y].flags & S_LRING));
        }
        /* TODO: drawbridges, walls, ladders, room=>ICED_xxx */

        lua_settable(L, -3);

        return 1;
    } else {
        /* TODO: return zerorm instead? */
        nhl_error(L, "Coordinates out of range");
        return 0;
    }
}

/* impossible("Error!") */
staticfn int
nhl_impossible(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        impossible("%s", luaL_checkstring(L, 1));
    else
        nhl_error(L, "Wrong args");
    return 0;
}

/* pline("It hits!") */
/* pline("It hits!", true) */
staticfn int
nhl_pline(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1 || argc == 2) {
        pline("%s", luaL_checkstring(L, 1));
        if (lua_toboolean(L, 2))
            display_nhwindow(WIN_MESSAGE, TRUE); /* --more-- */
    } else
        nhl_error(L, "Wrong args");

    return 0;
}

/* verbalize("Fool!") */
staticfn int
nhl_verbalize(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        verbalize("%s", luaL_checkstring(L, 1));
    else
        nhl_error(L, "Wrong args");

    return 0;
}

/* parse_config("OPTIONS=!color") */
staticfn int
nhl_parse_config(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        parse_conf_str(luaL_checkstring(L, 1), parse_config_line);
    else
        nhl_error(L, "Wrong args");

    return 0;
}

/* local windowtype = get_config("windowtype"); */
staticfn int
nhl_get_config(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        lua_pushstring(L, get_option_value(luaL_checkstring(L, 1), TRUE));
        return 1;
    } else
        nhl_error(L, "Wrong args");

    return 0;
}

/*
  str = getlin("What do you want to call this dungeon level?");
 */
staticfn int
nhl_getlin(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        const char *prompt = luaL_checkstring(L, 1);
        char buf[BUFSZ];

        getlin(prompt, buf);
        lua_pushstring(L, buf);
        return 1;
    }

    nhl_error(L, "Wrong args");
    /*NOTREACHED*/
    return 0;
}

/*
 selected = menu("prompt",default,pickX,{"a"="option a", "b"="option b",...})
 pickX = 0,1,2, or "none", "one", "any" (PICK_X in code)

 selected = menu("prompt", default, pickX,
                 {{key:"a", text:"option a"},{key:"b", text:"option b"},... })
 */
staticfn int
nhl_menu(lua_State *L)
{
    static const char *const pickX[] = { "none", "one", "any" }; /* PICK_x */
    int argc = lua_gettop(L);
    const char *prompt;
    const char *defval = "";
    int pick = PICK_ONE, pick_cnt;
    winid tmpwin;
    anything any;
    menu_item *picks = (menu_item *) 0;
    int clr = NO_COLOR;

    if (argc < 2 || argc > 4) {
        nhl_error(L, "Wrong args");
        /*NOTREACHED*/
        return 0;
    }

    prompt = luaL_checkstring(L, 1);
    if (lua_isstring(L, 2))
        defval = luaL_checkstring(L, 2);
    if (lua_isstring(L, 3))
        pick = luaL_checkoption(L, 3, "one", pickX);
    luaL_checktype(L, argc, LUA_TTABLE);

    tmpwin = create_nhwindow(NHW_MENU);
    start_menu(tmpwin, MENU_BEHAVE_STANDARD);

    lua_pushnil(L); /* first key */
    while (lua_next(L, argc) != 0) {
        const char *str = "";
        const char *key = "";

        /* key @ index -2, value @ index -1 */
        if (lua_istable(L, -1)) {
            lua_pushliteral(L, "key");
            lua_gettable(L, -2);
            key = lua_tostring(L, -1);
            lua_pop(L, 1);

            lua_pushliteral(L, "text");
            lua_gettable(L, -2);
            str = lua_tostring(L, -1);
            lua_pop(L, 1);

            /* TODO: glyph, attr, accel, group accel (all optional) */
        } else if (lua_isstring(L, -1)) {
            str = luaL_checkstring(L, -1);
            key = luaL_checkstring(L, -2);
        }

        any = cg.zeroany;
        if (*key)
            any.a_char = key[0];
        add_menu(tmpwin, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr, str,
                 (*defval && *key && defval[0] == key[0])
                     ? MENU_ITEMFLAGS_SELECTED
                     : MENU_ITEMFLAGS_NONE);

        lua_pop(L, 1); /* removes 'value'; keeps 'key' for next iteration */
    }

    end_menu(tmpwin, prompt);
    pick_cnt = select_menu(tmpwin, pick, &picks);
    destroy_nhwindow(tmpwin);

    if (pick_cnt > 0) {
        char buf[2];
        buf[0] = picks[0].item.a_char;

        if (pick == PICK_ONE && pick_cnt > 1 && *defval
            && defval[0] == picks[0].item.a_char)
            buf[0] = picks[1].item.a_char;

        buf[1] = '\0';
        lua_pushstring(L, buf);
        /* TODO: pick any */
    } else {
        char buf[2];
        buf[0] = defval[0];
        buf[1] = '\0';
        lua_pushstring(L, buf);
    }

    return 1;
}

/* text("foo\nbar\nbaz") */
staticfn int
nhl_text(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc > 0) {
        menu_item *picks = (menu_item *) 0;
        winid tmpwin;

        tmpwin = create_nhwindow(NHW_MENU);
        start_menu(tmpwin, MENU_BEHAVE_STANDARD);

        while (lua_gettop(L) > 0) {
            char *ostr = dupstr(luaL_checkstring(L, 1));
            char *ptr, *str = ostr;
            char *lstr = str + strlen(str) - 1;

            do {
                char *nlp = strchr(str, '\n');

                if (nlp && (nlp - str) <= 76) {
                    ptr = nlp;
                } else {
                    ptr = str + 76;
                    if (ptr > lstr)
                        ptr = lstr;
                }
                while ((ptr > str) && !(*ptr == ' ' || *ptr == '\n'))
                    ptr--;
                *ptr = '\0';
                add_menu_str(tmpwin, str);
                str = ptr + 1;
            } while (*str && str <= lstr);
            lua_pop(L, 1);
            free(ostr);
        }

        end_menu(tmpwin, (char *) 0);
        (void) select_menu(tmpwin, PICK_NONE, &picks);
        destroy_nhwindow(tmpwin);
    }
    return 0;
}

/* makeplural("zorkmid") */
staticfn int
nhl_makeplural(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushstring(L, makeplural(luaL_checkstring(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* makesingular("zorkmids") */
staticfn int
nhl_makesingular(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushstring(L, makesingular(luaL_checkstring(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* s_suffix("foo") */
staticfn int
nhl_s_suffix(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushstring(L, s_suffix(luaL_checkstring(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* ing_suffix("foo") */
staticfn int
nhl_ing_suffix(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushstring(L, ing_suffix(luaL_checkstring(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* an("foo") */
staticfn int
nhl_an(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushstring(L, an(luaL_checkstring(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* rn2(10) */
staticfn int
nhl_rn2(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushinteger(L, rn2((int) luaL_checkinteger(L, 1)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* random(10);  -- is the same as rn2(10); */
/* random(5,8); -- same as 5 + rn2(8); */
staticfn int
nhl_random(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1)
        lua_pushinteger(L, rn2((int) luaL_checkinteger(L, 1)));
    else if (argc == 2)
        lua_pushinteger(L, luaL_checkinteger(L, 1)
                               + rn2((int) luaL_checkinteger(L, 2)));
    else
        nhl_error(L, "Wrong args");

    return 1;
}

/* level_difficulty() */
staticfn int
nhl_level_difficulty(lua_State *L)
{
    int argc = lua_gettop(L);
    if (argc == 0) {
        lua_pushinteger(L, level_difficulty());
    } else {
        nhl_error(L, "level_difficulty should not have any args");
    }
    return 1;
}

/* local x = nh.is_genocided("vampire") */
staticfn int
nhl_is_genocided(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        const char *paramstr = luaL_checkstring(L, 1);
        int mgend;
        int i = name_to_mon(paramstr, &mgend);

        lua_pushboolean(L, (i != NON_PM)
                        && (svm.mvitals[i].mvflags & G_GENOD)
                        ? TRUE : FALSE);
    } else {
        nhl_error(L, "Wrong args");
    }
    return 1;
}

/* local debug_themerm = nh.debug_themerm(isfill)
 * if isfill is false, returns value of env variable THEMERM
 * if isfill is true,  returns value of env variable THEMERMFILL
 * return nil if not in wizard mode or the variable isn't set */
staticfn int
nhl_get_debug_themerm_name(lua_State *L)
{
    int argc = lua_gettop(L);
    if (argc == 1) {
        char *dbg_themerm = (char *) 0;
        boolean is_fill = lua_toboolean(L, 1);
        lua_pop(L, 1);
        if (wizard)
            dbg_themerm = getenv(is_fill ? "THEMERMFILL" : "THEMERM");
        if (!dbg_themerm || !*dbg_themerm) {
            lua_pushnil(L);
        } else {
            lua_pushstring(L, dbg_themerm);
        }
    } else {
        nhl_error(L, "debug_themerm should have 1 boolean arg");
    }
    return 1;
}

RESTORE_WARNING_UNREACHABLE_CODE

/* get mandatory integer value from table */
int
get_table_int(lua_State *L, const char *name)
{
    int ret;

    lua_getfield(L, -1, name);
    ret = (int) luaL_checkinteger(L, -1);
    lua_pop(L, 1);
    return ret;
}

/* get optional integer value from table */
int
get_table_int_opt(lua_State *L, const char *name, int defval)
{
    int ret = defval;

    lua_getfield(L, -1, name);
    if (!lua_isnil(L, -1)) {
        ret = (int) luaL_checkinteger(L, -1);
    }
    lua_pop(L, 1);
    return ret;
}

char *
get_table_str(lua_State *L, const char *name)
{
    char *ret;

    lua_getfield(L, -1, name);
    ret = dupstr(luaL_checkstring(L, -1));
    lua_pop(L, 1);
    return ret;
}

/* get optional string value from table.
   return value must be freed by caller. */
char *
get_table_str_opt(lua_State *L, const char *name, char *defval)
{
    const char *ret;
    int ltyp;

    lua_getfield(L, -1, name);
    ltyp = lua_type(L, -1);
    if (ltyp == LUA_TSTRING || ltyp == LUA_TNIL) {
        ret = luaL_optstring(L, -1, defval);
    } else if (ltyp == LUA_TFUNCTION) {
        nhl_pcall_handle(L, 0, 1, "get_table_str_opt", NHLpa_panic);
        ret = luaL_optstring(L, -1, defval);
    } else {
        nhl_error(L, "get_table_str_opt: no string");
    }
    if (ret) {
        lua_pop(L, 1);
        return dupstr(ret);
    }
    lua_pop(L, 1);
    return NULL;
}

int
get_table_boolean(lua_State *L, const char *name)
{
    static const char *const boolstr[] = {
        "true", "false", "yes", "no", NULL
    };
    /* static const int boolstr2i[] = { TRUE, FALSE, TRUE, FALSE, -1 }; */
    int ltyp;
    int ret = -1;

    lua_getfield(L, -1, name);
    ltyp = lua_type(L, -1);
    if (ltyp == LUA_TSTRING) {
        ret = luaL_checkoption(L, -1, NULL, boolstr);
        /* nhUse(boolstr2i[0]); */
    } else if (ltyp == LUA_TBOOLEAN) {
        ret = lua_toboolean(L, -1);
    } else if (ltyp == LUA_TNUMBER) {
        ret = (int) luaL_checkinteger(L, -1);
        if (ret < 0 || ret > 1)
            ret = -1;
    }
    lua_pop(L, 1);
    if (ret == -1)
        nhl_error(L, "Expected a boolean");
    return ret;
}

int
get_table_boolean_opt(lua_State *L, const char *name, int defval)
{
    int ret = defval;

    lua_getfield(L, -1, name);
    if (lua_type(L, -1) != LUA_TNIL) {
        lua_pop(L, 1);
        return get_table_boolean(L, name);
    }
    lua_pop(L, 1);
    return ret;
}

/* opts[] is a null-terminated list */
int
get_table_option(lua_State *L,
                 const char *name,
                 const char *defval,
                 const char *const opts[])
{
    int ret;

    lua_getfield(L, -1, name);
    ret = luaL_checkoption(L, -1, defval, opts);
    lua_pop(L, 1);
    return ret;
}

#ifdef DUMPLOG
/* local fname = dump_fmtstr("/tmp/nethack.%n.%d.log"); */
staticfn int
nhl_dump_fmtstr(lua_State *L)
{
    int argc = lua_gettop(L);
    char buf[512];

    if (argc == 1)
        lua_pushstring(L, dump_fmtstr(luaL_checkstring(L, 1), buf, TRUE));
    else
        nhl_error(L, "Expected a string parameter");
    return 1;
}
#endif /* DUMPLOG */

/* local dungeon_name = dnum_name(u.dnum); */
staticfn int
nhl_dnum_name(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        lua_Integer dnum = luaL_checkinteger(L, 1);

        if (dnum >= 0 && dnum < svn.n_dgns)
            lua_pushstring(L, svd.dungeons[dnum].dname);
        else
            lua_pushstring(L, "");
    } else
        nhl_error(L, "Expected an integer parameter");
    return 1;
}

/* return gender-neutral monster type name by integer value,
   or empty string if outside LOW_PM - HIGH_PM range */
/* local montypename = int_to_pmname(12); */
staticfn int
nhl_int_to_pm_name(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        lua_Integer i = luaL_checkinteger(L, 1);

        if (i >= LOW_PM && i <= HIGH_PM)
            lua_pushstring(L, mons[i].pmnames[NEUTRAL]);
        else
            lua_pushstring(L, "");
    } else
        nhl_error(L, "Expected an integer parameter");
    return 1;
}

/* convert integer to object type name and class */
/* local oname,oclass = int_to_objname(25); */
staticfn int
nhl_int_to_obj_name(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        char buf[8];
        lua_Integer i = luaL_checkinteger(L, 1);

        if (i >= 0 && i < NUM_OBJECTS && OBJ_NAME(objects[i])) {
            lua_pushstring(L, OBJ_NAME(objects[i]));
            buf[0] = def_oc_syms[(int)objects[i].oc_class].sym;
            buf[1] = '\0';
            lua_pushstring(L, buf);
        } else {
            lua_pushstring(L, "");
            lua_pushstring(L, "");
        }
    } else
        nhl_error(L, "Expected an integer parameter");
    return 2;
}

DISABLE_WARNING_UNREACHABLE_CODE
/* because nhl_error() does not return */

/* set or get variables which are saved and restored along with the game.
   nh.variable("test", 10);
   local ten = nh.variable("test"); */
staticfn int
nhl_variable(lua_State *L)
{
    int argc = lua_gettop(L);
    int typ;
    const char *key;

    if (!gl.luacore) {
        panic("nh luacore not inited");
        /*NOTREACHED*/
        return 0;
    }

    lua_getglobal(gl.luacore, "nh_lua_variables");
    if (!lua_istable(gl.luacore, -1)) {
        impossible("nh_lua_variables is not a lua table");
        return 0;
    }

    if (argc == 1) {
        key = luaL_checkstring(L, 1);

        lua_getfield(gl.luacore, -1, key);
        typ = lua_type(gl.luacore, -1);
        if (typ == LUA_TSTRING)
            lua_pushstring(L, lua_tostring(gl.luacore, -1));
        else if (typ == LUA_TNIL)
            lua_pushnil(L);
        else if (typ == LUA_TBOOLEAN)
            lua_pushboolean(L, lua_toboolean(gl.luacore, -1));
        else if (typ == LUA_TNUMBER)
            lua_pushinteger(L, lua_tointeger(gl.luacore, -1));
        else if (typ == LUA_TTABLE) {
            lua_getglobal(gl.luacore, "nh_get_variables_string");
            lua_pushvalue(gl.luacore, -2);
            nhl_pcall_handle(gl.luacore, 1, 1, "nhl_variable", NHLpa_panic);
            luaL_loadstring(L, lua_tostring(gl.luacore, -1));
            nhl_pcall_handle(L, 0, 1, "nhl_variable-1", NHLpa_panic);
        } else
            nhl_error(L, "Cannot get variable of that type");
        return 1;
    } else if (argc == 2) {
        /* set nh_lua_variables[key] = value;
           nh.variable("key", value); */
        key = luaL_checkstring(L, 1);
        //pline("SETVAR:%s", key);
        typ = lua_type(L, -1);

        if (typ == LUA_TSTRING) {
            lua_pushstring(gl.luacore, lua_tostring(L, -1));
            lua_setfield(gl.luacore, -2, key);
        } else if (typ == LUA_TNIL) {
            lua_pushnil(gl.luacore);
            lua_setfield(gl.luacore, -2, key);
        } else if (typ == LUA_TBOOLEAN) {
            lua_pushboolean(gl.luacore, lua_toboolean(L, -1));
            lua_setfield(gl.luacore, -2, key);
        } else if (typ == LUA_TNUMBER) {
            lua_pushinteger(gl.luacore, lua_tointeger(L, -1));
            lua_setfield(gl.luacore, -2, key);
        } else if (typ == LUA_TTABLE) {
            lua_getglobal(L, "nh_set_variables_string");
            lua_pushstring(L, key);
            lua_pushvalue(L, -3); /* copy value to top */
            nhl_pcall_handle(L, 2, 1, "nhl_variable-2", NHLpa_panic);
            luaL_loadstring(gl.luacore, lua_tostring(L, -1));
            nhl_pcall_handle(gl.luacore, 0, 0, "nhl_variable-3", NHLpa_panic);
        } else
            nhl_error(L, "Cannot set variable of that type");
        return 0;
    } else
        nhl_error(L, "Wrong number of arguments");
    return 1;
}

/* return nh_lua_variable lua table as a string */
char *
get_nh_lua_variables(void)
{
    char *key = NULL;

    if (!gl.luacore) {
        panic("nh luacore not inited");
        /*NOTREACHED*/
        return key;
    }
    lua_getglobal(gl.luacore, "get_variables_string");
    if (lua_type(gl.luacore, -1) == LUA_TFUNCTION) {
        if (nhl_pcall_handle(gl.luacore, 0, 1, "get_nh_lua_variables",
                             NHLpa_impossible)) {
            return key;
        }
        key = dupstr(lua_tostring(gl.luacore, -1));
    }
    lua_pop(gl.luacore, 1);
    return key;
}

RESTORE_WARNING_UNREACHABLE_CODE

#endif /* !SFCTOOL */

#ifdef SFCTOOL
char *lua_data;
#endif

/* save nh_lua_variables table to file */
void
save_luadata(NHFILE *nhfp)
{
    unsigned lua_data_len;
#ifndef SFCTOOL
    char *lua_data = get_nh_lua_variables(); /* note: '\0' terminated */
#endif

    if (!lua_data)
        lua_data = dupstr(emptystr);
    lua_data_len = Strlen(lua_data) + 1; /* +1: include the terminator */
    Sfo_unsigned(nhfp, &lua_data_len, "luadata-lua_data_len");
    Sfo_char(nhfp, lua_data, "luadata", lua_data_len);
    free(lua_data);
}

/* restore nh_lua_variables table from file */
void
restore_luadata(NHFILE *nhfp)
{
    unsigned lua_data_len = 0;
#ifndef SFCTOOL
    char *lua_data;
#endif /* !SFCTOOL */

    Sfi_unsigned(nhfp, &lua_data_len, "luadata-lua_data_len");
    lua_data = (char *) alloc(lua_data_len);
    Sfi_char(nhfp, lua_data, "luadata", lua_data_len);

#ifndef SFCTOOL
    if (!gl.luacore)
        l_nhcore_init();
    luaL_loadstring(gl.luacore, lua_data);
    free(lua_data);
    nhl_pcall_handle(gl.luacore, 0, 0, "restore_luadata", NHLpa_panic);
#endif /* !SFCTOOL */
}

#ifndef SFCTOOL

/* local stairs = stairways(); */
staticfn int
nhl_stairways(lua_State *L)
{
    stairway *tmp = gs.stairs;
    int i = 1; /* lua arrays should start at 1 */

    lua_newtable(L);

    while (tmp) {
        lua_pushinteger(L, i);
        lua_newtable(L);

        nhl_add_table_entry_bool(L, "up", tmp->up);
        nhl_add_table_entry_bool(L, "ladder", tmp->isladder);
        nhl_add_table_entry_int(L, "x", tmp->sx);
        nhl_add_table_entry_int(L, "y", tmp->sy);
        nhl_add_table_entry_int(L, "dnum", tmp->tolev.dnum);
        nhl_add_table_entry_int(L, "dlevel", tmp->tolev.dlevel);

        lua_settable(L, -3);

        tmp = tmp->next;
        i++;
    }

    return 1;
}

/*
  test( { x = 123, y = 456 } );
*/
staticfn int
nhl_test(lua_State *L)
{
    coordxy x, y;
    char *name, Player[] = "Player";

    /* discard any extra arguments passed in */
    lua_settop(L, 1);

    luaL_checktype(L, 1, LUA_TTABLE);

    x = (coordxy) get_table_int(L, "x");
    y = (coordxy) get_table_int(L, "y");
    name = get_table_str_opt(L, "name", Player);

    pline("TEST:{ x=%i, y=%i, name=\"%s\" }", (int) x, (int) y, name);

    free(name);

    return 1;
}

/* push a key into command queue */
/* nh.pushkey("i"); */
staticfn int
nhl_pushkey(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        const char *key = luaL_checkstring(L, 1);

        while (*key) {
            cmdq_add_key(CQ_CANNED, *key);
            key++;
        }
    }

    return 0;
}

/* do a turn of moveloop, or until gm.multi is done if param is true. */
/* nh.doturn(); nh.doturn(true); */
staticfn int
nhl_doturn(lua_State *L)
{
    int argc = lua_gettop(L);
    boolean domulti = FALSE;

    if (argc == 1)
        domulti = lua_toboolean(L, 1);

    do {
        moveloop_core();
    } while (domulti && gm.multi);

    return 0;
}

/* set debugging flags. debugging use only, of course. */
/* nh.debug_flags({ mongen = false,
                    hunger = false,
                    overwrite_stairs = true }); */
staticfn int
nhl_debug_flags(lua_State *L)
{
    int val;

    lcheck_param_table(L);

    /* disable monster generation */
    val = get_table_boolean_opt(L, "mongen", -1);
    if (val != -1) {
        iflags.debug_mongen = !(boolean) val; /* value in lua is negated */
        if (iflags.debug_mongen) {
            struct monst *mtmp, *mtmp2;

            for (mtmp = fmon; mtmp; mtmp = mtmp2) {
                mtmp2 = mtmp->nmon;
                if (DEADMONSTER(mtmp))
                    continue;
                mongone(mtmp);
            }
        }
    }

    /* prevent hunger */
    val = get_table_boolean_opt(L, "hunger", -1);
    if (val != -1) {
        iflags.debug_hunger = !(boolean) val; /* value in lua is negated */
    }

    /* allow overwriting stairs */
    val = get_table_boolean_opt(L, "overwrite_stairs", -1);
    if (val != -1) {
        iflags.debug_overwrite_stairs = (boolean) val;
    }

    /* prevent pline going out to the UI */
    val = get_table_boolean_opt(L, "prevent_pline", -1);
    if (val != -1) {
        iflags.debug_prevent_pline = (boolean) val;
    }

    return 0;
}

/* flip level */
/* nh.flip_level(n); */
staticfn int
nhl_flip_level(lua_State *L)
{
    int argc = lua_gettop(L);
    int flp = 0;

    if (argc == 1)
        flp = lua_tointeger(L, 1);

    flip_level(flp, !gi.in_mklev);

    return 0;
}

DISABLE_WARNING_UNREACHABLE_CODE

/* does location at x,y have timer? */
/* local has_melttimer = nh.has_timer_at(x,y, "melt-ice"); */
/* local has_melttimer = nh.has_timer_at({x=4,y=7}, "melt-ice"); */
staticfn int
nhl_timer_has_at(lua_State *L)
{
    boolean ret = FALSE;
    short timertype = nhl_get_timertype(L, -1);
    lua_Integer lx, ly;
    coordxy x, y;
    long when;

    lua_pop(L, 1); /* remove timertype */
    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "nhl_timer_has_at: Wrong args");
        /*NOTREACHED*/
        return 0;
    }

    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (isok(x, y)) {
        when = spot_time_expires(x, y, timertype);
        ret = (when > 0L);
    }
    lua_pushboolean(L, ret);
    return 1;
}

/* when does location at x,y timer trigger? */
/* local melttime = nh.peek_timer_at(x,y, "melt-ice"); */
/* local melttime = nh.peek_timer_at({x=5,y=6}, "melt-ice"); */
staticfn int
nhl_timer_peek_at(lua_State *L)
{
    long when = 0L;
    short timertype = nhl_get_timertype(L, -1);
    lua_Integer lx, ly;
    coordxy x, y;

    lua_pop(L, 1); /* remove timertype */
    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "nhl_timer_peek_at: Wrong args");
        /*NOTREACHED*/
        return 0;
    }

    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (timer_is_pos(timertype) && isok(x, y))
        when = spot_time_expires(x, y, timertype);
    lua_pushinteger(L, when);
    return 1;
}

/* stop timer at location x,y */
/* nh.stop_timer_at(x,y, "melt-ice"); */
/* nh.stop_timer_at({x=6,y=8}, "melt-ice"); */
staticfn int
nhl_timer_stop_at(lua_State *L)
{
    short timertype = nhl_get_timertype(L, -1);
    lua_Integer lx, ly;
    coordxy x, y;

    lua_pop(L, 1); /* remove timertype */
    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "nhl_timer_stop_at: Wrong args");
        /*NOTREACHED*/
        return 0;
    }

    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (timer_is_pos(timertype) && isok(x, y))
        spot_stop_timers(x, y, timertype);
    return 0;
}

/* start timer at location x,y */
/* nh.start_timer_at(x,y, "melt-ice", 10); */
staticfn int
nhl_timer_start_at(lua_State *L)
{
    short timertype = nhl_get_timertype(L, -2);
    long when = lua_tointeger(L, -1);
    lua_Integer lx, ly;
    coordxy x, y;

    lua_pop(L, 2); /* remove when and timertype */
    if (!nhl_get_xy_params(L, &lx, &ly)) {
        nhl_error(L, "nhl_timer_start_at: Wrong args");
        /*NOTREACHED*/
        return 0;
    }

    x = (coordxy) lx;
    y = (coordxy) ly;
    cvt_to_abscoord(&x, &y);

    if (timer_is_pos(timertype) && isok(x, y)) {
        long where = ((long) x << 16) | (long) y;

        spot_stop_timers(x, y, timertype);
        (void) start_timer((long) when, TIMER_LEVEL, MELT_ICE_AWAY,
                           long_to_any(where));
    }
    return 0;
}

/* returns the visual interpretation of the key bound to an extended command,
   or the ext cmd name if not bound to any key */
/* local helpkey = eckey("help"); */
staticfn int
nhl_get_cmd_key(lua_State *L)
{
    int argc = lua_gettop(L);

    if (argc == 1) {
        const char *cmd = luaL_checkstring(L, 1);
        char *key = cmd_from_ecname(cmd);

        lua_pushstring(L, key);
        return 1;
    }

    return 0;
}

/* add or remove a lua function callback */
/* callback("level_enter", "function_name"); */
/* callback("level_enter", "function_name", true); */
/* level_enter, level_leave, cmd_before */
staticfn int
nhl_callback(lua_State *L)
{
    int argc = lua_gettop(L);
    int i;
    boolean rm;
    const char *fn, *cb;

    if (!gl.luacore) {
        panic("nh luacore not inited");
        /*NOTREACHED*/
        return 0;
    }
    if (argc == 2 || argc == 3) {
        if (argc == 2) {
            rm = FALSE;
            fn = luaL_checkstring(L, -1);
            cb = luaL_checkstring(L, -2);
        } else {
            rm = lua_toboolean(L, -1);
            fn = luaL_checkstring(L, -2);
            cb = luaL_checkstring(L, -3);
        }
        for (i = 0; i < NUM_NHCB; i++)
            if (!strcmp(cb, nhcb_name[i]))
                break;
        if (i >= NUM_NHCB)
            return 0;

        if (rm) {
            nhcb_counts[i]--;
            if (nhcb_counts[i] < 0)
                impossible("nh.callback counts are wrong");
        } else {
            nhcb_counts[i]++;
        }

        lua_getglobal(gl.luacore, rm ? "nh_callback_rm" : "nh_callback_set");
        lua_pushstring(gl.luacore, cb);
        lua_pushstring(gl.luacore, fn);
        nhl_pcall_handle(gl.luacore, 2, 0, "nhl_callback", NHLpa_panic);
    }
    return 0;
}

/* store or restore game state */
/*
 * Currently handles
 *   turn counter,
 *   hero inventory and hunger,
 *   hero attributes and skills and conducts (all via 'struct u'),
 *   object discoveries,
 *   monster generation and vanquished statistics.
 * NOTE: wouldn't work after restore if game has been saved, so the
 * #save command ('S') is disabled during the tutorial.
 */
/* gamestate(); -- save state */
/* gamestate(true); -- restore state */
staticfn int
nhl_gamestate(lua_State *L)
{
    long wornmask;
    struct obj *otmp;
    int argc = lua_gettop(L);
    boolean reststate = (argc > 0) ? lua_toboolean(L, -1) : FALSE;
    int otyp;

    debugpline4("gamestate: %d:%d (%c vs %c)", u.uz.dnum, u.uz.dlevel,
                reststate ? 'T' : 'F', gg.gmst_stored ? 't' : 'f');

    if (reststate && gg.gmst_stored) {
        d_level cur_uz = u.uz, cur_uz0 = u.uz0;

        /* restore game state */
        svm.moves = gg.gmst_moves;
        pline("Resetting time to move #%ld.", svm.moves);
        gg.gmst_moves = 0L;

        gl.lastinvnr = 51;
        while (gi.invent)
            useupall(gi.invent);
        while ((otmp = gg.gmst_invent) != NULL) {
            wornmask = otmp->owornmask;
            otmp->owornmask = 0L;
            extract_nobj(otmp, &gg.gmst_invent);
            addinv_nomerge(otmp);
            if (wornmask)
                setworn(otmp, wornmask);
        }
        assert(gg.gmst_ubak != NULL);
        (void) memcpy((genericptr_t) &u, gg.gmst_ubak, sizeof u);
        assert(gg.gmst_disco != NULL);
        (void) memcpy((genericptr_t) &svd.disco, gg.gmst_disco,
                      sizeof svd.disco);
        assert(gg.gmst_mvitals != NULL);
        (void) memcpy((genericptr_t) &svm.mvitals, gg.gmst_mvitals,
                      sizeof svm.mvitals);
        /* clear user-given object type names */
        for (otyp = 0; otyp < NUM_OBJECTS; otyp++)
            if (objects[otyp].oc_uname) {
                free(objects[otyp].oc_uname);
                objects[otyp].oc_uname = (char *) 0;
            }
        /* some restored state would confuse the level change in progress */
        u.uz = cur_uz, u.uz0 = cur_uz0;
        init_uhunger();
        free_tutorial(); /* release gg.gmst_XYZ */
        gg.gmst_stored = FALSE;
        (void) memcpy(svs.spl_book, gg.gmst_spl_book, sizeof svs.spl_book);
    } else if (!reststate && !gg.gmst_stored) {
        /* store game state */
        gg.gmst_moves = svm.moves;
        while ((otmp = gi.invent) != NULL) {
            wornmask = otmp->owornmask;
            setnotworn(otmp);
            freeinv(otmp);
            otmp->owornmask = wornmask; /* flag for later restore */
            otmp->nobj = gg.gmst_invent;
            gg.gmst_invent = otmp;
        }
        gl.lastinvnr = 51; /* next inv letter to try to use will be 'a' */
        gg.gmst_ubak = (genericptr_t) alloc(sizeof u);
        (void) memcpy(gg.gmst_ubak, (genericptr_t) &u, sizeof u);
        gg.gmst_disco = (genericptr_t) alloc(sizeof svd.disco);
        (void) memcpy(gg.gmst_disco, (genericptr_t) &svd.disco,
                      sizeof svd.disco);
        gg.gmst_mvitals = (genericptr_t) alloc(sizeof svm.mvitals);
        (void) memcpy(gg.gmst_mvitals, (genericptr_t) &svm.mvitals,
                      sizeof svm.mvitals);
        (void) memcpy(gg.gmst_spl_book, svs.spl_book, sizeof svs.spl_book);
        (void) memset(svs.spl_book, 0, sizeof(svs.spl_book));
        gg.gmst_stored = TRUE;
    } else {
        impossible("nhl_gamestate: inconsistent state (%s vs %s)",
                   reststate ? "restore" : "save",
                   gg.gmst_stored ? "already stored" : "not stored");
    }
    update_inventory();
    return 0;
}

RESTORE_WARNING_UNREACHABLE_CODE

/* free dynamic date allocated when entering tutorial;
   called when exiting tutorial normally or if player quits while in it */
void
free_tutorial(void)
{
    struct obj *otmp;

    /* for normal tutorial exit, gmst_invent will already be Null */
    while ((otmp = gg.gmst_invent) != 0) {
        /* set otmp->where = OBJ_FREE, otmp->nobj = NULL */
        extract_nobj(otmp, &gg.gmst_invent);
        /* gmst_invent is a list of invent items sequestered when entering
           the tutorial; for them, owornmask is used as a flag to re-wear
           them when exiting tutorial, not that they are currently worn;
           clear it to avoid a "deleting worn obj" complaint from obfree() */
        otmp->owornmask = 0L;
        /* dealloc_obj() isn't enough (for containers, it assumes that
           caller has already freed their contents) */
        obfree(otmp, (struct obj *) 0);
    }

    if (gg.gmst_ubak)
        free(gg.gmst_ubak), gg.gmst_ubak = NULL;
    if (gg.gmst_disco)
        free(gg.gmst_disco), gg.gmst_disco = NULL;
    if (gg.gmst_mvitals)
        free(gg.gmst_mvitals), gg.gmst_mvitals = NULL;
}

/* called from gotolevel(do.c) */
void
tutorial(boolean entering)
{
    l_nhcore_call(entering ? NHCORE_ENTER_TUTORIAL : NHCORE_LEAVE_TUTORIAL);

    if (!entering) { /* after leaving, can't go back */
        nhcore_call_available[NHCORE_ENTER_TUTORIAL]
            = nhcore_call_available[NHCORE_LEAVE_TUTORIAL]
                = FALSE;
    }
}

static const struct luaL_Reg nhl_functions[] = {
    { "test", nhl_test },

    { "getmap", nhl_getmap },
#if 0
    { "setmap", nhl_setmap },
#endif
    { "gettrap", nhl_gettrap },
    { "deltrap", nhl_deltrap },

    { "has_timer_at", nhl_timer_has_at },
    { "peek_timer_at", nhl_timer_peek_at },
    { "stop_timer_at", nhl_timer_stop_at },
    { "start_timer_at", nhl_timer_start_at },

    { "abscoord", nhl_abs_coord },

    { "impossible", nhl_impossible },
    { "pline", nhl_pline },
    { "verbalize", nhl_verbalize },
    { "menu", nhl_menu },
    { "text", nhl_text },
    { "getlin", nhl_getlin },
    { "eckey", nhl_get_cmd_key },
    { "callback", nhl_callback },
    { "gamestate", nhl_gamestate },

    { "makeplural", nhl_makeplural },
    { "makesingular", nhl_makesingular },
    { "s_suffix", nhl_s_suffix },
    { "ing_suffix", nhl_ing_suffix },
    { "an", nhl_an },
    { "rn2", nhl_rn2 },
    { "random", nhl_random },
    { "level_difficulty", nhl_level_difficulty },
    { "is_genocided", nhl_is_genocided },
    { "debug_themerm", nhl_get_debug_themerm_name },
    { "parse_config", nhl_parse_config },
    { "get_config", nhl_get_config },
    { "get_config_errors", l_get_config_errors },
#ifdef DUMPLOG
    { "dump_fmtstr", nhl_dump_fmtstr },
#endif /* DUMPLOG */
    { "dnum_name", nhl_dnum_name },
    { "int_to_pmname", nhl_int_to_pm_name },
    { "int_to_objname", nhl_int_to_obj_name },
    { "variable", nhl_variable },
    { "stairways", nhl_stairways },
    { "pushkey", nhl_pushkey },
    { "doturn", nhl_doturn },
    { "debug_flags", nhl_debug_flags },
    { "flip_level", nhl_flip_level },
    { NULL, NULL }
};

static const struct {
    const char *name;
    long value;
} nhl_consts[] = {
    { "COLNO",  COLNO },
    { "ROWNO",  ROWNO },
    { "NUMMONS", NUMMONS },
    { "LOW_PM", LOW_PM },
    { "HIGH_PM", HIGH_PM },
    { "FIRST_OBJECT", FIRST_OBJECT },
    { "LAST_OBJECT", NUM_OBJECTS-1 },
#ifdef DLB
    { "DLB", 1 },
#else
    { "DLB", 0 },
#endif /* DLB */
    { NULL, 0 },
};

/* register and init the constants table */
staticfn void
init_nhc_data(lua_State *L)
{
    int i;

    lua_newtable(L);

    for (i = 0; nhl_consts[i].name; i++) {
        lua_pushstring(L, nhl_consts[i].name);
        lua_pushinteger(L, nhl_consts[i].value);
        lua_rawset(L, -3);
    }

    lua_setglobal(L, "nhc");
}

staticfn int
nhl_push_anything(lua_State *L, int anytype, void *src)
{
    anything any = cg.zeroany;

    switch (anytype) {
    case ANY_INT:
        any.a_int = *(int *) src;
        lua_pushinteger(L, any.a_int);
        break;
    case ANY_UCHAR:
        any.a_uchar = *(uchar *) src;
        lua_pushinteger(L, any.a_uchar);
        break;
    case ANY_SCHAR:
        any.a_schar = *(schar *) src;
        lua_pushinteger(L, any.a_schar);
        break;
    }
    return 1;
}

DISABLE_WARNING_UNREACHABLE_CODE

staticfn int
nhl_meta_u_index(lua_State *L)
{
    static const struct {
        const char *name;
        void *ptr;
        int type;
    } ustruct[] = {
        { "ux", &(u.ux), ANY_UCHAR },
        { "uy", &(u.uy), ANY_UCHAR },
        { "dx", &(u.dx), ANY_SCHAR },
        { "dy", &(u.dy), ANY_SCHAR },
        { "dz", &(u.dz), ANY_SCHAR },
        { "tx", &(u.tx), ANY_UCHAR },
        { "ty", &(u.ty), ANY_UCHAR },
        { "ulevel", &(u.ulevel), ANY_INT },
        { "ulevelmax", &(u.ulevelmax), ANY_INT },
        { "uhunger", &(u.uhunger), ANY_INT },
        { "nv_range", &(u.nv_range), ANY_INT },
        { "xray_range", &(u.xray_range), ANY_INT },
        { "umonster", &(u.umonster), ANY_INT },
        { "umonnum", &(u.umonnum), ANY_INT },
        { "mh", &(u.mh), ANY_INT },
        { "mhmax", &(u.mhmax), ANY_INT },
        { "mtimedone", &(u.mtimedone), ANY_INT },
        { "dlevel", &(u.uz.dlevel), ANY_SCHAR }, /* actually coordxy */
        { "dnum", &(u.uz.dnum), ANY_SCHAR },     /* actually coordxy */
        { "uluck", &(u.uluck), ANY_SCHAR },
        { "uhp", &(u.uhp), ANY_INT },
        { "uhpmax", &(u.uhpmax), ANY_INT },
        { "uen", &(u.uen), ANY_INT },
        { "uenmax", &(u.uenmax), ANY_INT },
    };
    const char *tkey = luaL_checkstring(L, 2);
    int i;

    /* FIXME: doesn't really work, eg. negative values for u.dx */
    for (i = 0; i < SIZE(ustruct); i++)
        if (!strcmp(tkey, ustruct[i].name)) {
            return nhl_push_anything(L, ustruct[i].type, ustruct[i].ptr);
        }

    if (!strcmp(tkey, "inventory")) {
        nhl_push_obj(L, gi.invent);
        return 1;
    } else if (!strcmp(tkey, "role")) {
        lua_pushstring(L, gu.urole.name.m);
        return 1;
    } else if (!strcmp(tkey, "moves")) {
        lua_pushinteger(L, svm.moves);
        return 1;
    } else if (!strcmp(tkey, "uhave_amulet")) {
        lua_pushinteger(L, u.uhave.amulet);
        return 1;
    } else if (!strcmp(tkey, "depth")) {
        lua_pushinteger(L, depth(&u.uz));
        return 1;
    } else if (!strcmp(tkey, "invocation_level")) {
        lua_pushboolean(L, Invocation_lev(&u.uz));
        return 1;
    }

    nhl_error(L, "Unknown u table index");
    /*NOTREACHED*/
    return 0;
}

staticfn int
nhl_meta_u_newindex(lua_State *L)
{
    nhl_error(L, "Cannot set u table values");
    /*NOTREACHED*/
    return 0;
}

RESTORE_WARNING_UNREACHABLE_CODE

staticfn int
nhl_u_clear_inventory(lua_State *L UNUSED)
{
    while (gi.invent)
        useupall(gi.invent);
    return 0;
}

/* Put object into player's inventory */
/* u.giveobj(obj.new("rock")); */
staticfn int
nhl_u_giveobj(lua_State *L)
{
    return nhl_obj_u_giveobj(L);
}

static const struct luaL_Reg nhl_u_functions[] = {
    { "clear_inventory", nhl_u_clear_inventory },
    { "giveobj", nhl_u_giveobj },
    { NULL, NULL }
};

staticfn void
init_u_data(lua_State *L)
{
    lua_newtable(L);
    luaL_setfuncs(L, nhl_u_functions, 0);
    lua_newtable(L);
    lua_pushcfunction(L, nhl_meta_u_index);
    lua_setfield(L, -2, "__index");
    lua_pushcfunction(L, nhl_meta_u_newindex);
    lua_setfield(L, -2, "__newindex");
    lua_setmetatable(L, -2);
    lua_setglobal(L, "u");
}

#ifdef notyet
staticfn int
nhl_set_package_path(lua_State *L, const char *path)
{
    if (LUA_TTABLE != lua_getglobal(L, "package")) {
        impossible("package not a table in nhl_set_package_path");
        return 1;
    };
    lua_pushstring(L, path);
    lua_setfield(L, -2, "path");
    lua_pop(L, 1);
    return 0;
}
#endif

staticfn int
traceback_handler(lua_State *L)
{
    luaL_traceback(L, L, lua_tostring(L, 1), 0);
    /* TODO: call impossible() if fuzzing? */
    return 1;
}

staticfn uint32_t
nhl_getmeminuse(lua_State *L)
{
    return lua_gc(L, LUA_GCCOUNT) * 1024 + lua_gc(L, LUA_GCCOUNTB);
}

/* lua_pcall with our traceback handler and memory and instruction step
 * limiting.
 *  On error, traceback will be on top of stack */
int
nhl_pcall(lua_State *L, int nargs, int nresults, const char *name)
{
    struct nhl_user_data *nud;
    int rv;

    lua_pushcfunction(L, traceback_handler);
    lua_insert(L, 1);
    (void) lua_getallocf(L, (void **) &nud);
#ifdef NHL_SANDBOX
    if (nud && name) {
        nud->name = name;
    }
    /* NB: We don't need to deal with nud->memlimit - Lua handles that. */
    if (nud && (nud->steps || nud->perpcall)) {
        if (nud->perpcall) {
            nud->steps = nud->perpcall;
            nud->statctr = 0;
        }
        if (setjmp(nud->jb)) {
            /* panic, because we don't know if the game state is corrupt */
            /* XXX can we get a lua stack trace as well? */
            panic("Lua time exceeded %d:%s", nud->sid,
                  nud->name ? nud->name : "(unknown)");
        }
    }
#endif

    rv = lua_pcall(L, nargs, nresults, 1);

    lua_remove(L, 1); /* remove handler */

#ifdef NHL_SANDBOX
    if (nud && nud->perpcall && gl.loglua) {
        long ic = nud->statctr * NHL_SB_STEPSIZE; // an approximation
        livelog_printf(LL_DEBUG, "LUASTATS PCAL %d:%s %ld", nud->sid,
                       nud->name, ic);
    }
    if (nud && nud->memlimit && gl.loglua) {
        livelog_printf(LL_DEBUG, "LUASTATS PMEM %d:%s %lu", nud->sid,
                       nud->name, (long unsigned) nhl_getmeminuse(L));
    }
#endif
    return rv;
}

int
nhl_pcall_handle(lua_State *L, int nargs, int nresults, const char *name,
                 NHL_pcall_action npa)
{
    int rv = nhl_pcall(L, nargs, nresults, name);
    if (rv) {
        nhl_user_data *nud;
        (void) lua_getallocf(L, (void **) &nud);
        /* XXX can we get a lua stack trace as well? */
        switch (npa) {
        case NHLpa_panic:
            panic("Lua error %d:%s %s", nud->sid,
                  nud->name ? nud->name : "(unknown)", lua_tostring(L, -1));
            /*NOTREACHED*/
            break;
        case NHLpa_impossible:
            impossible("Lua error: %d:%s %s", nud->sid,
                       nud->name ? nud->name : "(unknown)",
                       lua_tostring(L, -1));
                /* Drop the error.  If the caller cares, use nhl_pcall(). */
            lua_pop(L, 1);
        }
    }
    return rv;
}

/* XXX impossible() should be swappable with pline/nothing/panic via flag */
/* read lua code/data from a dlb module or an external file
   into a string buffer and feed that to lua */
boolean
nhl_loadlua(lua_State *L, const char *fname)
{
#define LOADCHUNKSIZE (1L << 13) /* 8K */
    boolean ret = TRUE;
    dlb *fh;
    char *buf = (char *) 0, *bufin, *bufout, *p, *nl, *altfname;
    long buflen, ct, cnt;
    int llret;

    altfname = (char *) alloc(Strlen(fname) + 3); /* 3: '('...')\0' */
    /* don't know whether 'fname' is inside a dlb container;
       if we did, we could choose between "nhdat(<fname>)" and "<fname>"
       but since we don't, compromise */
    Sprintf(altfname, "(%s)", fname);
    fh = dlb_fopen(fname, RDBMODE);
    if (!fh) {
        impossible("nhl_loadlua: Error opening %s", altfname);
        ret = FALSE;
        goto give_up;
    }

    dlb_fseek(fh, 0L, SEEK_END);
    buflen = dlb_ftell(fh);
    dlb_fseek(fh, 0L, SEEK_SET);

    /* extra +1: room to add final '\n' if missing */
    buf = bufout = (char *) alloc(FITSint(buflen + 1 + 1));
    buf[0] = '\0';
    bufin = bufout = buf;

    ct = 0L;
    while (buflen > 0 || ct) {
        /*
         * Semi-arbitrarily limit reads to 8K at a time.  That's big
         * enough to cover the majority of our Lua files in one bite
         * but small enough to fully exercise the partial record
         * handling (when processing the castle's level description).
         *
         * [For an external file (non-DLB), VMS may only be able to
         * read at most 32K-1 at a time depending on the file format
         * in use, and fseek(SEEK_END) only yields an upper bound on
         * the actual amount of data in that situation.]
         */
        if ((cnt = dlb_fread(bufin, 1, min((int) buflen, LOADCHUNKSIZE), fh))
            < 0L)
            break;
        buflen -= cnt; /* set up for next iteration, if any */
        if (cnt == 0L) {
            *bufin = '\n'; /* very last line is unterminated? */
            cnt = 1;
        }
        bufin[cnt] = '\0'; /* fread() doesn't do this */

        /* in case partial line was leftover from previous fread */
        bufin -= ct, cnt += ct, ct = 0;

        while (cnt > 0) {
            if ((nl = strchr(bufin, '\n')) != 0) {
                /* normal case, newline is present */
                ct = (long) (nl - bufin + 1L); /* +1: keep the newline */
                for (p = bufin; p <= nl; ++p)
                    *bufout++ = *bufin++;
                if (*bufin == '\r')
                    ++bufin, ++ct;
                /* update for next loop iteration */
                cnt -= ct;
                ct = 0;
            } else if (strlen(bufin) < LOADCHUNKSIZE) {
                /* no newline => partial record; move unprocessed chars
                   to front of input buffer (bufin portion of buf[]) */
                ct = cnt = (long) (eos(bufin) - bufin);
                for (p = bufout; cnt > 0; --cnt)
                    *p++ = *bufin++;
                *p = '\0';
                bufin = p; /* next fread() populates buf[] starting here */
                /* cnt==0 so inner loop will terminate */
            } else {
                /* LOADCHUNKSIZE portion of buffer already completely full */
                impossible("(%s) line too long", altfname);
                goto give_up;
            }
        }
    }
    *bufout = '\0';
    (void) dlb_fclose(fh);

    llret = luaL_loadbuffer(L, buf, strlen(buf), altfname);
    if (llret != LUA_OK) {
        impossible("luaL_loadbuffer: Error loading %s: %s", altfname,
                   lua_tostring(L, -1));
        ret = FALSE;
        goto give_up;
    } else {
        if (nhl_pcall_handle(L, 0, LUA_MULTRET, fname, NHLpa_impossible)) {
            ret = FALSE;
            goto give_up;
        }
    }

 give_up:
    if (altfname)
        free((genericptr_t) altfname);
    if (buf)
        free((genericptr_t) buf);
    return ret;
#undef LOADCHUNKSIZE
}

DISABLE_WARNING_CONDEXPR_IS_CONSTANT

lua_State *
nhl_init(nhl_sandbox_info *sbi)
{
    /* It would be nice to import EXPECTED from each build system. XXX */
    /* And it would be nice to do it only once, but it's cheap. */
#ifndef NHL_VERSION_EXPECTED
#if LUA_VERSION_NUM >= 505
#define NHL_VERSION_EXPECTED 50500
#else
#define NHL_VERSION_EXPECTED 50408
#endif
#endif

#ifdef NHL_SANDBOX
    if (NHL_VERSION_EXPECTED != LUA_VERSION_RELEASE_NUM) {
        panic(
            "sandbox doesn't know this Lua version: this=%d != expected=%d ",
            LUA_VERSION_RELEASE_NUM, NHL_VERSION_EXPECTED);
    }
#endif

    lua_State *L = nhlL_newstate(sbi, "nhl_init");
    if(!L) return 0;

    iflags.in_lua = TRUE;
    /* Temporary for development XXX */
    /* Turn this off in config.h to disable the sandbox. */
#ifdef NHL_SANDBOX
    nhlL_openlibs(L, sbi->flags);
#else
    luaL_openlibs(L);
#endif

#ifdef notyet
    if (sbi->flags & NHL_SB_PACKAGE) {
        /* XXX Is this still needed? */
        if (nhl_set_package_path(L, "./?.lua"))
            return 0;
    }
#endif

    /* register nh -table, and functions for it */
    lua_newtable(L);
    luaL_setfuncs(L, nhl_functions, 0);
    lua_setglobal(L, "nh");

    /* init nhc -table */
    init_nhc_data(L);

    /* init u -table */
    init_u_data(L);

    l_selection_register(L);
    l_register_des(L);

    l_obj_register(L);

    /* nhlib.lua assumes the math table exists. */
    if (LUA_TTABLE != lua_getglobal(L, "math")) {
        lua_newtable(L);
        lua_setglobal(L, "math");
    }

    if (!nhl_loadlua(L, "nhlib.lua")) {
        nhl_done(L);
        return (lua_State *) 0;
    }

    return L;
}

RESTORE_WARNING_CONDEXPR_IS_CONSTANT

void
nhl_done(lua_State *L)
{
    if (L) {
        nhl_user_data *nud = 0;
        (void) lua_getallocf(L, (void **) &nud);
        if (gl.loglua) {
            if (nud && nud->osteps) {
                long ic = nud->statctr * NHL_SB_STEPSIZE; // an approximation
                livelog_printf(LL_DEBUG, "LUASTATS DONE %d:%s %ld", nud->sid,
                               nud->name, ic);
            }
            if (nud && nud->memlimit && !nud->perpcall) {
                livelog_printf(LL_DEBUG, "LUASTATS DMEM %d:%s %lu", nud->sid,
                               nud->name, (long unsigned) nhl_getmeminuse(L));
            }
        }
        lua_close(L);
        if (nud)
            nhl_alloc(NULL, nud, 0, 0); // free nud
    }
    iflags.in_lua = FALSE;
}

boolean
load_lua(const char *name, nhl_sandbox_info *sbi)
{
    boolean ret = TRUE;
    lua_State *L = nhl_init(sbi);

    if (!L) {
        ret = FALSE;
        goto give_up;
    }

    if (!nhl_loadlua(L, name)) {
        ret = FALSE;
        goto give_up;
    }

 give_up:
    nhl_done(L);

    return ret;
}

DISABLE_WARNING_CONDEXPR_IS_CONSTANT

const char *
get_lua_version(void)
{
    nhl_sandbox_info sbi = { NHL_SB_VERSION, 1 * 1024 * 1024, 0,
                             1 * 1024 * 1024 };

    if (gl.lua_ver[0] == 0) {
        lua_State *L = nhl_init(&sbi);

        if (L) {
            size_t len = 0;
            const char *vs = (const char *) 0;

            /* LUA_VERSION yields "<major>.<minor>" although we check to see
               whether it is "Lua-<major>.<minor>" and strip prefix if so;
               LUA_RELEASE is <LUA_VERSION>.<LUA_VERSION_RELEASE> but doesn't
               get set up as a lua global */
            lua_getglobal(L, "_RELEASE");
            if (lua_isstring(L, -1))
                vs = lua_tolstring(L, -1, &len);
#ifdef LUA_RELEASE
            else
                vs = LUA_RELEASE, len = strlen(vs);
#endif
            if (!vs) {
                lua_getglobal(L, "_VERSION");
                if (lua_isstring(L, -1))
                    vs = lua_tolstring(L, -1, &len);
#ifdef LUA_VERSION
                else
                    vs = LUA_VERSION, len = strlen(vs);
#endif
            }
            if (vs && len < sizeof gl.lua_ver) {
                if (!strncmpi(vs, "Lua", 3)) {
                    vs += 3;
                    if (*vs == '-' || *vs == ' ')
                        vs += 1;
                }
                Strcpy(gl.lua_ver, vs);
            }
        }
        nhl_done(L);
#ifdef LUA_COPYRIGHT
        if (sizeof LUA_COPYRIGHT <= sizeof gl.lua_copyright)
            Strcpy(gl.lua_copyright, LUA_COPYRIGHT);
#endif
    }
    return (const char *) gl.lua_ver;
}

RESTORE_WARNING_CONDEXPR_IS_CONSTANT

/***
 *** SANDBOX / HARDENING CODE
 ***/
/*
 * Tracing the sandbox:
 * 1. Make sure CHRONICLE and LIVELOG are defined in config.h.
 * 2. Rebuild the game.
 * 3. Run the game and do whatever you want to check with lua.
 * 4. Find the logged information in livelog.
 *
 * Logging format in livelog:
 * <livelog leader>message=LUASTATS <TYPE> <SID>:<TAG> <DATA>
 * where:
 *  TYPE
 *     DONE rough number of step taken during the life of the VM
 *     DMEM memory in use at destruction of VM
 *     PCAL rough number of steps during lua_pcall
 *     PMEM memory in use after lua_pcall returns
 *  SID     a small integer identifying the Lua VM instance
 *  TAG     a string from the nhl_luapcall call
 *  DATA    memory: rough number of bytes in use by the VM
 *          steps: rough number of steps by the VM(see NHL_SB_STEPSIZE)
 */
#ifdef NHL_SANDBOX

enum ewhen {NEVER, IFFLAG, EOT};
struct e {
    enum ewhen when;
    const char *fnname;
};

/* NHL_BASE_BASE - safe things */
static struct e ct_base_base[] = {
    { IFFLAG, "ipairs" },
    { IFFLAG, "next" },
    { IFFLAG, "pairs" },
    { IFFLAG, "pcall" },
    { IFFLAG, "select" },
    { IFFLAG, "tonumber" },
    { IFFLAG, "tostring" },
    { IFFLAG, "type" },
    { IFFLAG, "xpcall" },
    { EOT, NULL }
};

/* NHL_BASE_ERROR - not really safe--might not want Lua to kill the process */
static struct e ct_base_error[] = {
    { IFFLAG, "assert" }, /* ok, calls error */
    { IFFLAG, "error" },  /* ok, calls G->panic */
    { NEVER, "print" },   /* not ok - calls lua_writestring/lua_writeline
                           * which write to stdout */
    { NEVER, "warn" },    /* not ok - calls lua_writestringerror which writes
                           * to stderr */
    { EOT, NULL }
};

/* NHL_BASE_META - metatable access */
static struct e ct_base_meta[] = {
    { IFFLAG, "getmetatable" },
    { IFFLAG, "rawequal" },
    { IFFLAG, "rawget" },
    { IFFLAG, "rawlen" },
    { IFFLAG, "rawset" },
    { IFFLAG, "setmetatable" },
    { EOT, NULL }
};

/* NHL_BASE_GC - questionable safety */
static struct e ct_base_iffy[] = {
    { IFFLAG, "collectgarbage" },
    { EOT, NULL }
};

/* NHL_BASE_UNSAFE - include only if required */
/* TODO: if NHL_BASE_UNSAFE is ever used, we need to wrap lua_load with
 * something to forbid mode=="b" */
static struct e ct_base_unsafe[] = {
    { IFFLAG, "dofile" },
    { IFFLAG, "loadfile" },
    { IFFLAG, "load" },
    { EOT, NULL }
};

/* no ct_co_ tables; all fns at same level of concern */
/* no ct_string_ tables; all fns at same level of concern */
/* no ct_table_ tables; all fns at same level of concern (but
   sort can take a lot of time and can't be caught by the step limit */
/* no ct_utf8_ tables; all fns at same level of concern */


/* possible ct_debug tables - likely to need changes */
static struct e ct_debug_debug[] = {
    { NEVER,  "debug" },          /* uses normal I/O so needs re-write */
    { IFFLAG, "getuservalue" },
    { NEVER,  "gethook" },        /* see sethook */
    { IFFLAG, "getinfo" },
    { IFFLAG, "getlocal" },
    { IFFLAG, "getregistry" },
    { IFFLAG, "getmetatable" },
    { IFFLAG, "getupvalue" },
    { IFFLAG, "upvaluejoin" },
    { IFFLAG, "upvalueid" },
    { IFFLAG, "setuservalue" },
    { NEVER,  "sethook" },        /* used for memory and step limits */
    { IFFLAG, "setlocal" },
    { IFFLAG, "setmetatable" },
    { IFFLAG, "setupvalue" },
    { IFFLAG, "setcstacklimit" },
    { EOT, NULL }
};
static struct e ct_debug_safe[] = {
    { IFFLAG, "traceback" },
    { EOT, NULL }
};

/* possible ct_os_ tables */
static struct e ct_os_time[] = {
    { IFFLAG, "clock" },          /* is this portable? XXX */
    { IFFLAG, "date" },
    { IFFLAG, "difftime" },
    { IFFLAG, "time" },
    { EOT, NULL }
};

static struct e ct_os_files[] = {
    { NEVER, "execute" },         /* not portable */
    { NEVER, "exit" },
    { NEVER, "getenv" },
    { IFFLAG, "remove" },
    { IFFLAG, "rename" },
    { NEVER, "setlocale" },
    { NEVER, "tmpname" },
    { EOT, NULL }
};


#define DROPIF(flag, lib, ct) \
    nhl_clearfromtable(L, !!(lflags & flag), lib, ct)

staticfn void
nhl_clearfromtable(lua_State *L, int flag, int tndx, struct e *todo)
{
    while (todo->when != EOT) {
        lua_pushnil(L);
        /* if we load the library at all, NEVER items must be erased
         * and IFFLAG items should be erased if !flag */
        if (todo->when == NEVER || !flag) {
            lua_setfield(L, tndx, todo->fnname);
        }
        todo++;
    }
}
#endif

/*
XXX
registry["org.nethack.nethack.sb.fs"][N]=
    CODEOBJECT
    {
        modepat: PATTERN,
        filepat: PATTERN
    }
CODEOBJECT
    if string then if pcall(string,mode, dir, file)
    if table then if mode matches pattern and filepat ma....
or do we use a real regex engine? (which we don't have and I just
 argued against adding)

return values from "call it":
    accept - file access granted
    reject - file access denied
    continue - try next element
    fail - error.  deny and call impossible/panic
*/

/* stack indexes:
 * -1       table to index with ename
 * params   file
 * params+1 [mode]
 */
/*
 * Problem: NetHack doesn't have a regex engine and Lua doesn't give
 *  C access to pattern matching.  There are 3 poor solutions:
 * 1. Import ~5K lines of code in a dozen files from FreeBSD. (Upside - we
 *    could use it in other places in NetHack.)
 * 2. Hack up lstrlib.c to give C direct access to the pattern matching code.
 * 3. Create a Lua state just to do pattern matching.
 * We're going to do #3.
 */
#ifdef notyet
staticfn boolean
start_luapat(void)
{
    int rv;
    /* XXX set memory and step limits */
    nhl_sandbox_info sbi = { NHL_SB_STRING, 0, 0, 0 };

    if ((luapat = nhl_init(&sbi)) == NULL)
        return FALSE;

    /* load a pattern matching function */
    rv = luaL_loadstring(
                         luapat,
              "function matches(s,p) return not not stringm.match(s,p) end");
    if (rv != LUA_OK) {
        panic("start_luapat: %d", rv);
    }
    return TRUE;
}
#endif

staticfn void
end_luapat(void)
{
    if (luapat) {
        lua_close(luapat);
        luapat = NULL;
    }
}

#ifdef notyet
staticfn int
opencheckpat(lua_State *L, const char *ename, int param)
{
    /* careful - we're using 2 different and unrelated Lua states */
    const char *string;
    int rv;

    lua_pushliteral(luapat, "matches");     /* function             -0,+1 */

    string = lua_tolstring(L, param, NULL); /* mode or filename     -0,+0 */
    lua_pushstring(luapat, string);                              /* -0,+1 */


    (void) lua_getfield(L, -1, ename);       /* pattern              -0,+1 */
    lua_pop(L, 1);                                               /* -1,+0 */
    string = lua_tolstring(L, -1, NULL);                         /* -0,+0 */
    lua_pushstring(luapat, string);                              /* -0,+1 */

    if (nhl_pcall(luapat, 2, 1)) {                               /* -3,+1 */
        /* impossible("access check internal error"); */
        return NHL_SBRV_FAIL;
    }
    rv = lua_toboolean(luapat, -1);                              /* -0,+0 */
#if 0
    if (lua_resetthread(luapat) != LUA_OK)
        return NHL_SBRV_FAIL;
is pop sufficient? XXX or wrong - look at the balance
#else
    lua_pop(luapat, 1);                                          /* -1,+0 */
#endif
    return rv ? NHL_SBRV_ACCEPT : NHL_SBRV_DENY;
}
#endif

/* put the table open uses to check its arguments on the top of the stack,
 * creating it if needed
 */
#define HOOKTBLNAME "org.nethack.nethack.sb.fs"
#ifdef notyet
static int (*io_open)(lua_State *) = NULL; /* XXX this may have to be in
                                            * struct g (gi now) TBD */
#endif

void
nhl_pushhooked_open_table(lua_State *L)
{
    int hot = lua_getfield(L, LUA_REGISTRYINDEX, HOOKTBLNAME);

    if (hot == LUA_TNONE) {
        lua_newtable(L);
        lua_pushvalue(L, -1);
        lua_setfield(L, LUA_REGISTRYINDEX, HOOKTBLNAME);
    }
}

#ifdef notyet
staticfn int
hooked_open(lua_State *L)
{
    const char *mode;
    static boolean never = TRUE;
    const char *filename;
    int params;
    int hot;

    if (never) {
        if (!start_luapat())
            return NHL_SBRV_FAIL;
        never = FALSE;
    }
    filename = luaL_checkstring(L, 1);

    /* Unlike io.open, we want to treat mode as non-optional. */
    if (lua_gettop(L) < 2) {
        lua_pushstring(L, "r");
    }
    mode = luaL_optstring(L, 2, "r");

    /* sandbox checks */
    /* Do we need some ud from the calling state to let this be different for
       each call without redoing the HO table??  Maybe for version 2. XXX */

    params = lua_gettop(L)-1;  /* point at first param */
    nhl_pushhooked_open_table(L);
    hot = lua_gettop(L);

    if (lua_type(L, hot) == LUA_TTABLE) {
        int idx;
        for (idx = 1;
             lua_pushinteger(L, idx),
                 lua_geti(L, hot, idx),
                 !lua_isnoneornil(L, -1);
             ++idx) {
            /* top of stack is our configtbl[idx] */
            switch (lua_type(L, -1)) {
                /* lots of options to expand this with other types XXX */
            case LUA_TTABLE: {
                int moderv, filerv;
                moderv = opencheckpat(L, "modepat", params+1);
                if (moderv == NHL_SBRV_FAIL)
                    return moderv;
                filerv = opencheckpat(L, "filepat", params);
                if (filerv == NHL_SBRV_FAIL)
                    return moderv;
                if (filerv == moderv) {
                    if (filerv == NHL_SBRV_DENY)
                        return NHL_SBRV_DENY;
                    if (filerv == NHL_SBRV_ACCEPT)
                        goto doopen;
                }
                break; /* try next entry */
            }
            default:
                return NHL_SBRV_FAIL;
            }
        }
    } else
        return NHL_SBRV_DENY; /* default to "no" */

 doopen:
    lua_settop(L, params + 1);
    return (*io_open)(L);
}

staticfn boolean
hook_open(lua_State *L)
{
    boolean rv = FALSE;
    if (!io_open) {
        int tos = lua_gettop(L);
        lua_pushglobaltable(L);
        if (lua_getfield(L, -1, "io") != LUA_TTABLE)
            goto out;
        lua_getfield(L, -1, "open");
        /* The only way this can happen is if someone is messing with us,
         * and I'm not sure even that is possible. */
        if (!lua_iscfunction(L, -1))
            goto out;
        /* XXX This is fragile: C11 says casting func* to void*
         * doesn't have to work, but POSIX says it does.  So it
         * _should_ work everywhere but all we can do without messing
         * around inside Lua is to try to keep the compiler quiet. */
        io_open = (int (*)(lua_State *)) lua_topointer(L, -1);
        lua_pushcfunction(L, hooked_open);
        lua_setfield(L, -1, "open");
        rv = TRUE;
 out:
        lua_settop(L, tos);
    }
    return rv;
}
#endif

DISABLE_WARNING_CONDEXPR_IS_CONSTANT

#ifdef NHL_SANDBOX
staticfn void
nhlL_openlibs(lua_State *L, uint32_t lflags)
{
    /* translate lflags from user-friendly to internal */
    if (NHL_SB_DEBUGGING & lflags) {
        lflags |= NHL_SB_DB_SAFE;
    }
    /* only for debugging the sandbox integration */
    if (NHL_SB_ALL & lflags) {
        lflags = -1;
    } else if (NHL_SB_SAFE & lflags) {
        lflags |= NHL_SB_BASE_BASE;
        lflags |= NHL_SB_COROUTINE;
        lflags |= NHL_SB_TABLE;
        lflags |= NHL_SB_STRING;
        lflags |= NHL_SB_MATH;
        lflags |= NHL_SB_UTF8;
    } else if (NHL_SB_VERSION) {
        lflags |= NHL_SB_BASE_BASE;
    }
#ifdef notyet
/*  Handling I/O is complex, so it's not available yet.  I'll
finish it if and when we need it. (keni)
    - hooked open; array of tuples of (r/w/rw/a/etc, directory pat, file pat)

{"close", io_close},    but with no args closes default output, so needs hook
{"flush", io_flush},
{"lines", io_lines},    hook due to filename
{"open", io_open},  hooked version:
    only safe if mode not present or == "r"
        or WRITEIO
    only safe if path has no slashes
        XXX probably need to be: matches port-specific list of paths
        WRITEIO needs a different list
    dlb integration?
    may need to #define l_getc (but that wouldn't hook core)
        may need to #define fopen/fread/fwrite/feof/ftell (etc?)
        ugh: lauxlib.c uses getc() below luaL_loadfilex
            override in lua.h?
        ugh: liolib.c uses getc() below g_read->test_eof
            override in lua.h?
{"read", io_read},
{"type", io_type},
{"input", io_input},    safe with a complex hook, but may be needed for read?
WRITEIO:    needs changes to hooked open?
{"output", io_output},  do we want to allow access to default output?
  {"write", io_write},
UNSAFEIO:
  {"popen", io_popen},
  {"tmpfile", io_tmpfile},
*/
#endif

    if (lflags & NHL_SB_BASEMASK) {
        int baselib;
        /* load the entire library ... */
        luaL_requiref(L, LUA_GNAME, luaopen_base, 1);

        baselib = lua_gettop(L);

        /* ... and remove anything unsupported or not requested */
        DROPIF(NHL_SB_BASE_BASE, baselib, ct_base_base);
        DROPIF(NHL_SB_BASE_ERROR, baselib, ct_base_error);
        DROPIF(NHL_SB_BASE_META, baselib, ct_base_meta);
        DROPIF(NHL_SB_BASE_GC, baselib, ct_base_iffy);
        DROPIF(NHL_SB_BASE_UNSAFE, baselib, ct_base_unsafe);

        lua_pop(L, 1);
    }

    if (lflags & NHL_SB_COROUTINE) {
        luaL_requiref(L, LUA_COLIBNAME, luaopen_coroutine, 1);
        lua_pop(L, 1);
    }
    if (lflags & NHL_SB_TABLE) {
        luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1);
        lua_pop(L, 1);
    }
#ifdef notyet
    if (lflags & NHL_SB_IO) {
        luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, 1);
        lua_pop(L, 1);
        if (!hook_open(L))
            panic("can't hook io.open");
    }
#endif
    if (lflags & NHL_SB_OSMASK) {
        int oslib;
        luaL_requiref(L, LUA_OSLIBNAME, luaopen_os, 1);
        oslib = lua_gettop(L);
        DROPIF(NHL_SB_OS_TIME, oslib, ct_os_time);
        DROPIF(NHL_SB_OS_FILES, oslib, ct_os_files);
        lua_pop(L, 1);
    }

    if (lflags & NHL_SB_STRING) {
        luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1);
        lua_pop(L, 1);
    }
    if (lflags & NHL_SB_MATH) {
        luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1);
        /* XXX Note that math.random uses Lua's built-in xoshiro256**
         * algorithm regardless of what the rest of the game uses.
         * Fixing this would require changing lmathlib.c. */
        lua_pop(L, 1);
    }
    if (lflags & NHL_SB_UTF8) {
        luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1);
        lua_pop(L, 1);
    }
    if (lflags & NHL_SB_DBMASK) {
        int dblib;
        luaL_requiref(L, LUA_DBLIBNAME, luaopen_debug, 1);
        dblib = lua_gettop(L);
        DROPIF(NHL_SB_DB_DB, dblib, ct_debug_debug);
        DROPIF(NHL_SB_DB_SAFE, dblib, ct_debug_safe);
        lua_pop(L, 1);
    }
}
#endif

RESTORE_WARNING_CONDEXPR_IS_CONSTANT

staticfn void *
nhl_alloc(void *ud, void *ptr, size_t osize UNUSED, size_t nsize)
{
    nhl_user_data *nud = ud;

    if (nsize == 0) {
        if (ptr != NULL) {
            free(ptr);
        }
        return NULL;
    }

    /* Check nud->L because it will be NULL during state init. */
    if (nud && nud->L && nud->memlimit) { /* this state is size limited */
        if (nhl_getmeminuse(nud->L) > nud->memlimit)
            return NULL;
    }

    return re_alloc(ptr, (unsigned) nsize);
}

DISABLE_WARNING_UNREACHABLE_CODE

staticfn int
nhl_panic(lua_State *L)
{
    const char *msg = lua_tostring(L, -1);

    if (msg == NULL)
        msg = "error object is not a string";
    panic("unprotected error in call to Lua API (%s)\n", msg);
    /*NOTREACHED*/
    return 0; /* return to Lua to abort */
}

RESTORE_WARNING_UNREACHABLE_CODE

/* called when lua issues a warning message; the text of the message
   is passed to us in pieces across multiple function calls */
staticfn void
nhl_warn(
    void *userdata UNUSED,
    const char *msg_fragment,
    int to_be_continued) /* 0: last fragment; 1: more to come */
{
    size_t fraglen, buflen = strlen(gl.lua_warnbuf);

    if (msg_fragment && buflen < sizeof gl.lua_warnbuf - 1) {
        fraglen = strlen(msg_fragment);
        if (buflen + fraglen > sizeof gl.lua_warnbuf - 1)
            fraglen = sizeof gl.lua_warnbuf - 1 - buflen;
        (void) strncat(gl.lua_warnbuf, msg_fragment, fraglen);
    }
    if (!to_be_continued) {
        paniclog("[lua]", gl.lua_warnbuf);
        gl.lua_warnbuf[0] = '\0';
    }
}

#ifdef NHL_SANDBOX
staticfn void
nhl_hookfn(lua_State *L, lua_Debug *ar UNUSED)
{
    nhl_user_data *nud;

    (void) lua_getallocf(L, (void **) &nud);

    if (nud->steps <= NHL_SB_STEPSIZE)
        longjmp(nud->jb, 1);

    nud->steps -= NHL_SB_STEPSIZE;
    nud->statctr++;
}
#endif

staticfn lua_State *
nhlL_newstate(nhl_sandbox_info *sbi, const char *name)
{
    nhl_user_data *nud = 0;

    if (sbi->memlimit || sbi->steps || sbi->perpcall) {
        nud = nhl_alloc(NULL, NULL, 0, sizeof (struct nhl_user_data));
        if (!nud)
            return 0;
        nud->L = NULL;
        nud->memlimit = sbi->memlimit;
        nud->perpcall = 0; /* set up below, if needed */
        nud->steps = 0;
        nud->osteps = 0;
        nud->flags = sbi->flags; /* save reporting flags */
        nud->statctr = 0;

        if (name) {
            nud->name = name;
        }
        nud->sid = ++gl.lua_sid;
    }

    lua_State *L = lua_newstate(nhl_alloc, nud
#if LUA_VERSION_NUM >= 505
                               , 0
#endif
                               );
    if (!L)
        panic("NULL lua_newstate");

    if(nud) nud->L = L;
    lua_atpanic(L, nhl_panic);
#if LUA_VERSION_NUM == 504
    lua_setwarnf(L, nhl_warn, L);
#endif

#ifdef NHL_SANDBOX
    if (nud && (sbi->steps || sbi->perpcall)) {
        if (sbi->steps && sbi->perpcall)
            impossible("steps and perpcall both non-zero");
        if (sbi->perpcall) {
            nud->perpcall = sbi->perpcall;
        } else {
            nud->steps = sbi->steps;
            nud->osteps = sbi->steps;
        }
        lua_sethook(L, nhl_hookfn, LUA_MASKCOUNT, NHL_SB_STEPSIZE);
    }
#endif

    return L;
}

#endif /* !SFCTOOL */

/*
(See end of comment for conclusion.)
to make packages safe, we need something like:
    if setuid/setgid (but does NH drop privs before we can check? TBD)
        unsetenv LUA_CPATH, LUA_CPATH_5_4 (and this needs to change with
          version) maybe more
    luaopen_package calls getenv
        unsetenv(LUA_PATH_VAR)
        unsetenv(LUA_CPATH_VAR)
        unsetenv(LUA_PATH_VAR LUA_VERSUFFIX)
        unsetenv(LUA_CPATH_VAR LUA_VERSUFFIX)
                    package.config
                    oackage[fieldname] = path
            NB: LUA_PATH_DEFAULT and LUA_CPATH_DEFAULT must be safe
                or we must setenv LUA_PATH_VAR and LUA_CPATH_VAR to something
                safe
            or we could just clean out the searchers table?
                package.searchers[preload,Lua,C,Croot]
also, can setting package.path to something odd get Lua to load files
 it shouldn't? (see docs package.searchers)
set (and disallow changing) package.cpath (etc?)
loadlib.c:
    lsys_load -> dlopen     Kill with undef LUA_USE_DLOPEN LUA_DL_DLL
    searchpath -> readable -> fopen
        <- ll_searchpath
        <- findfile <- {searchers C, Croot, Lua}
Probably the best thing to do is replace G.require with our own function
that does whatever it is we need and completely ignore the package library.
*/
/*
TODO:
docs
unfinished functionality & design
commit, cleanup, commit with SHA1 of full code version
BUT how do we compact the current history?
    new branch, then compress there
XXX
*/

/*nhlua.c*/
