//////////////////////////////////////////////////////////////////////////
// BBS-Lua Project
//
// Author: Hung-Te Lin(piaip), Jan. 2008.
// <piaip@csie.ntu.edu.tw>
// Create: 2008-01-04 22:02:58
// $Id$
//
// This source is released in MIT License, same as Lua 5.0
// http://www.lua.org/license.html
//
// Copyright 2008 Hung-Te Lin <piaip@csie.ntu.edu.tw>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// TODO:
//  BBSLUA 1.0
//  1. add quick key/val conversion [deprecated]
//  2. add key values (UP/DOWN/...) [done]
//  3. remove i/o libraries [done]
//  4. add system break key (Ctrl-C) [done]
//  5. add version string and script tags [done]
//  6. standalone w32 sdk [done]
//  7. syntax highlight in editor [done]
//  8. prevent loadfile, dofile [done]
//  9. provide local storage
//  ?. modify bbs user data (eg, money)
//  ?. os.date(), os.exit(), abort(), os.time()
//  ?. memory free issue in C library level?
//  ?. add digital signature
//
//  BBSLUA 2.0
//  1. 2 people communication
//
//  BBSLUA 3.0
//  1. n people communication
//////////////////////////////////////////////////////////////////////////

#include "bbs.h"
#include "fnv_hash.h"
#include <sys/time.h>

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

//////////////////////////////////////////////////////////////////////////
// CONST DEFINITION
//////////////////////////////////////////////////////////////////////////

#define BBSLUA_INTERFACE_VER    0.119 // (0.201)
#define BBSLUA_SIGNATURE        "--#BBSLUA"

// BBS-Lua script format:
// $BBSLUA_SIGNATURE
// -- Interface: $interface
// -- Title: $title
// -- Notes: $notes
// -- Author: $author <email@domain>
// -- Version: $version
// -- Date: $date
// -- LatestRef: #AID board
//  [... script ...]
// $BBSLUA_SIGNATURE

//////////////////////////////////////////////////////////////////////////
// CONFIGURATION VARIABLES
//////////////////////////////////////////////////////////////////////////
#define BLAPI_PROTO     int

#define BLCONF_BREAK_KEY    Ctrl('C')
#define BLCONF_EXEC_COUNT   (5000)
#define BLCONF_PEEK_TIME    (0.01)
#define BLCONF_KBHIT_TMIN   (BLCONF_PEEK_TIME)
#define BLCONF_KBHIT_TMAX   (60*10)
#define BLCONF_SLEEP_TMIN   (BLCONF_PEEK_TIME)
#define BLCONF_SLEEP_TMAX   (BLCONF_KBHIT_TMAX)
#define BLCONF_U_SECOND     (1000000L)
#define BLCONF_PRINT_TOC_INDEX (2)

#define BLCONF_MMAP_ATTACH
#define BLCONF_CURRENT_USERID   cuser.userid
#define BLCONF_CURRENT_USERNICK cuser.nickname

// BBS-Lua Storage
enum {
    BLS_INVALID= 0,
    BLS_GLOBAL = 1,
    BLS_USER,
};

// #define BLSCONF_ENABLED
#define BLSCONF_BN_VAL  "global"
#define BLSCONF_USER_VAL    "user"
#define BLSCONF_GMAXSIZE    (16*1024)   // should be aligned to block size
#define BLSCONF_UMAXSIZE    (16*1024)   // should be aligned to block size
#define BLSCONF_GPATH       BBSHOME "/luastore"
#define BLSCONF_UPATH       ".luastore"
#define BLSCONF_PREFIX      "v1_"
#define BLSCONF_MAXIO       32          // prevent bursting system

// #define BBSLUA_USAGE

#ifdef _WIN32
# undef  BLCONF_MMAP_ATTACH
# undef  BLCONF_CURRENT_USERID
# define BLCONF_CURRENT_USERID    "guest"
# undef  BLCONF_CURRENT_USERNICK
# define BLCONF_CURRENT_USERNICK  "測試帳號"
#endif

//////////////////////////////////////////////////////////////////////////
// GLOBAL VARIABLES
//////////////////////////////////////////////////////////////////////////
typedef struct {
    char running;   // prevent re-entrant
    char abort;     // system break key hit
    char iocounter; // prevent bursting i/o
    Fnv32_t storename;  // storage filename
} BBSLuaRT;

// runtime information
// static
BBSLuaRT blrt = {0};

#define BL_INIT_RUNTIME() { \
    memset(&blrt, 0, sizeof(blrt)); \
    blrt.storename = FNV1_32_INIT; \
}

#define BL_END_RUNTIME() { \
    memset(&blrt, 0, sizeof(blrt)); \
}

#ifdef BBSLUA_USAGE
static int bbslua_count;
#endif

//////////////////////////////////////////////////////////////////////////
// UTILITIES
//////////////////////////////////////////////////////////////////////////

static void
bl_double2tv(double d, struct timeval *tv)
{
    tv->tv_sec = d;
    tv->tv_usec = (d - tv->tv_sec) * BLCONF_U_SECOND;
}

static double
bl_tv2double(const struct timeval *tv)
{
    double d = tv->tv_sec;
    d += tv->tv_usec / (double)BLCONF_U_SECOND;
    return d;
}

static int
bl_peekbreak(float f)
{
    if (vkey_is_full())
        vkey_purge();

    if (vkey_prefetch(f * MILLISECONDS) &&
        vkey_is_prefetched(BLCONF_BREAK_KEY)) {
        vkey_purge();
        blrt.abort = 1;
        return 1;
    }
    return 0;
}

static void
bl_k2s(lua_State* L, int v)
{
    if (v <= 0)
        lua_pushnil(L);
    else if (v == KEY_TAB)
        lua_pushstring(L, "TAB");
    else if (v == '\b' || v == 0x7F)
        lua_pushstring(L, "BS");
    else if (v == KEY_ENTER)
        lua_pushstring(L, "ENTER");
    else if (v < ' ')
        lua_pushfstring(L, "^%c", v-1+'A');
    else if (v < 0x100)
        lua_pushfstring(L, "%c", v);
    else if (v >= KEY_F1 && v <= KEY_F12)
        lua_pushfstring(L, "F%d", v - KEY_F1 +1);
    else switch(v)
    {
        case KEY_UP:    lua_pushstring(L, "UP");    break;
        case KEY_DOWN:  lua_pushstring(L, "DOWN");  break;
        case KEY_RIGHT: lua_pushstring(L, "RIGHT"); break;
        case KEY_LEFT:  lua_pushstring(L, "LEFT");  break;
        case KEY_HOME:  lua_pushstring(L, "HOME");  break;
        case KEY_END:   lua_pushstring(L, "END");   break;
        case KEY_INS:   lua_pushstring(L, "INS");   break;
        case KEY_DEL:   lua_pushstring(L, "DEL");   break;
        case KEY_PGUP:  lua_pushstring(L, "PGUP");  break;
        case KEY_PGDN:  lua_pushstring(L, "PGDN");  break;
        default:        lua_pushnil(L);             break;
    }
}

BLAPI_PROTO
bl_newwin(int rows, int cols, const char *title)
{
    // input: (rows, cols, title)
    int y = 0, x = 0, n = 0;
    int oy = 0, ox = 0;

    if (rows <= 0 || cols <= 0)
        return 0;

    getyx(&oy, &ox);
    // now, draw the window
    newwin(rows, cols, oy, ox);

    if (!title || !*title)
        return 0;

    // draw center-ed title
    n = strlen_noansi(title);
    x = ox + (cols - n)/2;
    y = oy + (rows)/2;
    move(y, x);
    outs(title);

    move(oy, ox);
    return 0;
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA API IMPLEMENTATION
//////////////////////////////////////////////////////////////////////////

BLAPI_PROTO
bl_getyx(lua_State* L)
{
    int y, x;
    getyx(&y, &x);
    lua_pushinteger(L, y);
    lua_pushinteger(L, x);
    return 2;
}

BLAPI_PROTO
bl_getmaxyx(lua_State* L)
{
    lua_pushinteger(L, t_lines);
    lua_pushinteger(L, t_columns);
    return 2;
}

BLAPI_PROTO
bl_move(lua_State* L)
{
    int n = lua_gettop(L);
    int y = 0, x = 0;
    if (n > 0)
        y = lua_tointeger(L, 1);
    if (n > 1)
        x = lua_tointeger(L, 2);
    move_ansi(y, x);
    return 0;
}

BLAPI_PROTO
bl_moverel(lua_State* L)
{
    int n = lua_gettop(L);
    int y = 0, x = 0;
    getyx(&y, &x);
    if (n > 0)
        y += lua_tointeger(L, 1);
    if (n > 1)
        x += lua_tointeger(L, 2);
    move(y, x);
    getyx(&y, &x);
    lua_pushinteger(L, y);
    lua_pushinteger(L, x);
    return 2;
}

BLAPI_PROTO
bl_clear(lua_State* L)
{
    (void)L;  /* to avoid warnings */
    clear();
    return 0;
}

BLAPI_PROTO
bl_clrtoeol(lua_State* L)
{
    (void)L;  /* to avoid warnings */
    clrtoeol();
    return 0;
}

BLAPI_PROTO
bl_clrtobot(lua_State* L)
{
    (void)L;  /* to avoid warnings */
    clrtobot();
    return 0;
}

BLAPI_PROTO
bl_refresh(lua_State* L)
{
    (void)L;  /* to avoid warnings */
    // refresh();
    // Seems like that most people don't understand the relationship
    // between refresh() and input queue, so let's force update here.
    doupdate();
    return 0;
}

BLAPI_PROTO
bl_addstr(lua_State* L)
{
    int n = lua_gettop(L);
    int i = 1;
    for (i = 1; i <= n; i++)
    {
        const char *s = lua_tostring(L, i);
        if(s)
            outs(s);
    }
    return 0;
}

BLAPI_PROTO
bl_rect(lua_State *L)
{
    // input: (rows, cols, title)
    int rows = 1, cols = 1;
    int n = lua_gettop(L);
    const char *title = NULL;

    if (n > 0)
        rows = lua_tointeger(L, 1);
    if (n > 1)
        cols = lua_tointeger(L, 2);
    if (n > 2)
        title = lua_tostring(L, 3);
    if (rows <= 0 || cols <= 0)
        return 0;

    // now, draw the rectangle
    bl_newwin(rows, cols, title);
    return 0;
}

BLAPI_PROTO
bl_print(lua_State* L)
{
    bl_addstr(L);
    outc('\n');
    return 0;
}

BLAPI_PROTO
bl_getch(lua_State* L)
{
    int c = vkey();
    if (c == BLCONF_BREAK_KEY)
    {
        vkey_purge();
        blrt.abort = 1;
        return lua_yield(L, 0);
    }
    bl_k2s(L, c);
    return 1;
}


BLAPI_PROTO
bl_getstr(lua_State* L)
{
    int y, x;
    // TODO not using fixed length here?
    char buf[PATHLEN] = "";
    int len = 2, echo = 1;
    int n = lua_gettop(L);
    const char *pmsg = NULL;

    if (n > 0)
        len = lua_tointeger(L, 1);
    if (n > 1)
        echo = lua_tointeger(L, 2);
    if (n > 2)
        pmsg = lua_tostring(L, 3);

    if (len < 2)
        len = 2;
    if (len >= (int)sizeof(buf))
        len = sizeof(buf) - 1;
    /*
     * this part now done in getdata_str
    if (pmsg && *pmsg)
    {
        strlcpy(buf, pmsg, sizeof(buf));
    }
    */

    // TODO process Ctrl-C here
    getyx(&y, &x);
    if (!pmsg) pmsg = "";
    len = getdata_str(y, x, NULL, buf, len, echo, pmsg);
    if (len <= 0)
    {
        len = 0;
        // check if we got Ctrl-C? (workaround in getdata)
        // TODO someday write 'ungetch()' in io.c to prevent
        // such workaround.
        if (buf[1] == Ctrl('C'))
        {
            vkey_purge();
            blrt.abort = 1;
            return lua_yield(L, 0);
        }
        lua_pushstring(L, "");
    }
    else
    {
        lua_pushstring(L, buf);
    }
    // return len ? 1 : 0;
    return 1;
}

BLAPI_PROTO
bl_kbhit(lua_State *L)
{
    int n = lua_gettop(L);
    double f = 0.1f;

    if (n > 0)
        f = (double)lua_tonumber(L, 1);

    if (f < BLCONF_KBHIT_TMIN) f = BLCONF_KBHIT_TMIN;
    if (f > BLCONF_KBHIT_TMAX) f = BLCONF_KBHIT_TMAX;

    if (vkey_is_ready() || vkey_poll(f * MILLISECONDS))
        lua_pushboolean(L, 1);
    else
        lua_pushboolean(L, 0);
    return 1;
}

BLAPI_PROTO
bl_kbreset(lua_State *L)
{
    // peek input queue first!
    if (bl_peekbreak(BLCONF_PEEK_TIME))
        return lua_yield(L, 0);

    vkey_purge();
    return 0;
}

BLAPI_PROTO
bl_sleep(lua_State *L)
{
    int n = lua_gettop(L);
    double us = 0, nus = 0;

    // update screen first.
    bl_refresh(L);

    if (n > 0)
        us = lua_tonumber(L, 1);
    if (us < BLCONF_SLEEP_TMIN) us = BLCONF_SLEEP_TMIN;
    if (us > BLCONF_SLEEP_TMAX) us = BLCONF_SLEEP_TMAX;
    nus = us;

#ifdef _WIN32

    Sleep(us * 1000);

#else // !_WIN32
    {
        struct timeval tp, tdest;

        gettimeofday(&tp, NULL);

        // nus is the destination time
        nus = bl_tv2double(&tp) + us;
        bl_double2tv(nus, &tdest);

        while ( (tp.tv_sec < tdest.tv_sec) ||
                ((tp.tv_sec == tdest.tv_sec) && (tp.tv_usec < tdest.tv_usec)))
        {
            // calculate new peek time
            us = nus - bl_tv2double(&tp);

            // check if input key is system break key.
            if (bl_peekbreak(us))
                return lua_yield(L, 0);

            // check time
            gettimeofday(&tp, NULL);
        }
    }
#endif // !_WIN32

    return 0;
}

BLAPI_PROTO
bl_kball(lua_State *L)
{
    // first, sleep by given seconds
    int r = 0, i = 0;

    r = bl_sleep(L);
    if (blrt.abort)
        return r;

    // pop all arguments
    lua_pop(L, lua_gettop(L));


#ifdef _WIN32
    while (i < LUA_MINSTACK && peekch(0))
    {
        bl_k2s(L, vkey());
        i++;
    }
#else
    while (i < LUA_MINSTACK && vkey_is_ready()) {
        bl_k2s(L, vkey());
        i++;
    }
#endif

    return i;
}


BLAPI_PROTO
bl_pause(lua_State* L)
{
    int n = lua_gettop(L);
    const char *s = NULL;
    if (n > 0)
        s = lua_tostring(L, 1);

    n = vmsg(s);
    if (n == BLCONF_BREAK_KEY)
    {
        vkey_purge();
        blrt.abort = 1;
        return lua_yield(L, 0);
    }
    bl_k2s(L, n);
    return 1;
}

BLAPI_PROTO
bl_title(lua_State* L)
{
    int n = lua_gettop(L);
    const char *s = NULL;
    if (n > 0)
        s = lua_tostring(L, 1);
    if (s == NULL)
        return 0;

    vs_hdr(s);
    return 0;
}

BLAPI_PROTO
bl_ansi_color(lua_State *L)
{
    char buf[PATHLEN] = ESC_STR "[";
    char *p = buf + strlen(buf);
    int i = 1;
    int n = lua_gettop(L);
    if (n >= 10) n = 10;
    for (i = 1; i <= n; i++)
    {
        if (i > 1) *p++ = ';';
        sprintf(p, "%d", (int)lua_tointeger(L, i));
        p += strlen(p);
    }
    *p++ = 'm';
    *p   = 0;
    lua_pushstring(L, buf);
    return 1;
}

BLAPI_PROTO
bl_strip_ansi(lua_State *L)
{
    int n = lua_gettop(L);
    const char *s = NULL;
    char *s2 = NULL;
    size_t os2 = 0;

    if (n < 1 || (s = lua_tostring(L, 1)) == NULL ||
            *s == 0)
    {
        lua_pushstring(L, "");
        return 1;
    }

    os2 = strlen(s)+1;
    s2 = (char*) lua_newuserdata(L, os2);
    strip_ansi(s2, s, STRIP_ALL);
    lua_pushstring(L, s2);
    lua_remove(L, -2);
    return 1;
}

BLAPI_PROTO
bl_attrset(lua_State *L)
{
    char buf[PATHLEN] = ESC_STR "[";
    char *p = buf + strlen(buf);
    int i = 1;
    int n = lua_gettop(L);
    if (n == 0)
        outs(ANSI_RESET);
    for (i = 1; i <= n; i++)
    {
        sprintf(p, "%dm",(int)lua_tointeger(L, i));
        outs(buf);
    }
    return 0;
}

BLAPI_PROTO
bl_time(lua_State *L)
{
    syncnow();
    lua_pushinteger(L, now);
    return 1;
}

BLAPI_PROTO
bl_ctime(lua_State *L)
{
    syncnow();
    lua_pushstring(L, ctime4(&now));
    return 1;
}

BLAPI_PROTO
bl_clock(lua_State *L)
{
    double d = 0;

#ifdef _WIN32

    // XXX this is a fast hack because we don't want to do 64bit calculation.
    SYSTEMTIME st;
    GetSystemTime(&st);
    syncnow();
    // XXX the may be some latency between our GetSystemTime and syncnow.
    // So build again the "second" part.
    d = (int)((now / 60) * 60);
    d += st.wSecond;
    d += (st.wMilliseconds / 1000.0f);

#else // !_WIN32

    struct timeval tp;
    gettimeofday(&tp, NULL);
    d = bl_tv2double(&tp);

#endif // !_WIN32

    lua_pushnumber(L, d);
    return 1;
}

//////////////////////////////////////////////////////////////////////////
// BBS-Lua Storage System
//////////////////////////////////////////////////////////////////////////
static int
bls_getcat(const char *s)
{
    if (!s || !*s)
        return BLS_INVALID;
    if (strcmp(s,       BLSCONF_BN_VAL) == 0)
        return          BLS_GLOBAL;
    else if (strcmp(s,  BLSCONF_USER_VAL) == 0)
        return          BLS_USER;
    return BLS_INVALID;
}

static int
bls_getlimit(const char *p)
{
    switch(bls_getcat(p))
    {
        case BLS_GLOBAL:
            return  BLSCONF_GMAXSIZE;
        case BLS_USER:
            return  BLSCONF_UMAXSIZE;
    }
    return 0;
}

static int
bls_setfn(char *fn, size_t sz, const char *p)
{
    *fn = 0;
    switch(bls_getcat(p))
    {
        case BLS_GLOBAL:
            snprintf(fn, sz, "%s/" BLSCONF_PREFIX "U%08x",
                    BLSCONF_GPATH, blrt.storename);
            return  1;

        case BLS_USER:
            setuserfile(fn, BLSCONF_UPATH);
#ifndef DEFAULT_FOLDER_CREATE_PERM
#define DEFAULT_FOLDER_CREATE_PERM (0755)
#endif
            mkdir(fn, DEFAULT_FOLDER_CREATE_PERM);
            assert(strlen(fn) +8 <= sz);
            snprintf(fn + strlen(fn),
                    sz - strlen(fn),
                    "/" BLSCONF_PREFIX "G%08x", blrt.storename);
            return  1;
    }
    return 0;
}

BLAPI_PROTO
bls_iolimit(lua_State *L)
{
    lua_pushinteger(L, BLSCONF_MAXIO);
    return 1;
}

BLAPI_PROTO
bls_limit(lua_State *L)
{
    int n = lua_gettop(L);
    const char *s = NULL;
    if (n > 0)
        s = lua_tostring(L, 1);
    lua_pushinteger(L, bls_getlimit(s));
    return 1;
}

BLAPI_PROTO
bls_load(lua_State *L)
{
    int n = lua_gettop(L);
    const char *cat = NULL;
    char fn[PATHLEN];

    if (blrt.iocounter >= BLSCONF_MAXIO)
    {
        lua_pushnil(L);
        return 1;
    }

    if (n != 1)
    {
        lua_pushnil(L);
        return 1;
    }
    cat = lua_tostring(L, 1); // category
    if (!cat)
    {
        lua_pushnil(L);
        return 1;
    }

    blrt.iocounter++;
    // read file!
    if (bls_setfn(fn, sizeof(fn), cat))
    {
        int fd = open(fn, O_RDONLY);
        if (fd >= 0)
        {
            char buf[2048];
            luaL_Buffer b;

            luaL_buffinit(L, &b);
            while ((n = read(fd, buf, sizeof(buf))) > 0)
                luaL_addlstring(&b, buf, n);
            close(fd);
            luaL_pushresult(&b);
        } else {
            lua_pushnil(L);
        }
    } else {
            lua_pushnil(L);
    }
    return 1;
}

BLAPI_PROTO
bls_save(lua_State *L)
{
    int n = lua_gettop(L), fd = -1;
    int limit = 0, slen = 0;
    const char *s = NULL, *cat = NULL;
    char fn[PATHLEN] = "", ret = 0;

    if (blrt.iocounter >= BLSCONF_MAXIO)
    {
        lua_pushboolean(L, 0);
        return 1;
    }

    if (n != 2) { lua_pushboolean(L, 0); return 1; }

    cat = lua_tostring(L, 1); // category
    s   = lua_tostring(L, 2); // data
    limit = bls_getlimit(cat);

    if (!cat || !s || limit < 1)
    {
        lua_pushboolean(L, 0);
        return 1;
    }

    slen = lua_objlen(L, 2);
    if (slen >= limit) slen = limit;

    blrt.iocounter++;
    // write file!
    if (bls_setfn(fn, sizeof(fn), cat))
    {
        fd = OpenCreate(fn, O_WRONLY);
        if (fd >= 0)
        {
            write(fd, s, slen);
            close(fd);
            ret = 1;
        }
    }

    lua_pushboolean(L, ret);
    return 1;
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA LIBRARY
//////////////////////////////////////////////////////////////////////////

static const struct luaL_reg lib_bbslua [] = {
    /* curses output */
    { "getyx",      bl_getyx },
    { "getmaxyx",   bl_getmaxyx },
    { "move",       bl_move },
    { "moverel",    bl_moverel },
    { "clear",      bl_clear },
    { "clrtoeol",   bl_clrtoeol },
    { "clrtobot",   bl_clrtobot },
    { "refresh",    bl_refresh },
    { "addstr",     bl_addstr },
    { "outs",       bl_addstr },
    { "print",      bl_print },
    /* input */
    { "getch",      bl_getch },
    { "getdata",    bl_getstr },
    { "getstr",     bl_getstr },
    { "kbhit",      bl_kbhit },
    { "kbreset",    bl_kbreset },
    { "kball",      bl_kball },
    /* advanced output */
    { "rect",       bl_rect },
    /* BBS utilities */
    { "pause",      bl_pause },
    { "title",      bl_title },
    /* time */
    { "time",       bl_time },
    { "now",        bl_time },
    { "clock",      bl_clock },
    { "ctime",      bl_ctime },
    { "sleep",      bl_sleep },
    /* ANSI helpers */
    { "ANSI_COLOR", bl_ansi_color },
    { "color",      bl_attrset },
    { "attrset",    bl_attrset },
    { "strip_ansi", bl_strip_ansi },
    { NULL, NULL},
};

static const struct luaL_reg lib_store [] = {
    { "load",       bls_load },
    { "save",       bls_save },
    { "limit",      bls_limit },
    { "iolimit",    bls_iolimit },
    { NULL, NULL},
};

// non-standard modules in bbsluaext.c
LUALIB_API int luaopen_bit (lua_State *L);

static const luaL_Reg bbslualibs[] = {
  // standard modules
  {"", luaopen_base},

  // {LUA_LOADLIBNAME, luaopen_package},
  {LUA_TABLIBNAME, luaopen_table},
  // {LUA_IOLIBNAME, luaopen_io},
  // {LUA_OSLIBNAME, luaopen_os},
  {LUA_STRLIBNAME, luaopen_string},
  {LUA_MATHLIBNAME, luaopen_math},
  // {LUA_DBLIBNAME, luaopen_debug},

  // bbslua-ext modules
  {"bit", luaopen_bit},

  {NULL, NULL}
};


LUALIB_API void bbsluaL_openlibs (lua_State *L) {
  const luaL_Reg *lib = bbslualibs;
  for (; lib->func; lib++) {
    lua_pushcfunction(L, lib->func);
    lua_pushstring(L, lib->name);
    lua_call(L, 1, 0);
  }
}


// Constant registration

typedef struct bbsluaL_RegStr {
  const char *name;
  const char *val;
} bbsluaL_RegStr;

typedef struct bbsluaL_RegNum {
  const char *name;
  lua_Number val;
} bbsluaL_RegNum;

static const bbsluaL_RegStr bbsluaStrs[] = {
    {"ESC",         ESC_STR},
    {"ANSI_RESET",  ANSI_RESET},
    {"sitename",    BBSNAME},
    {NULL,          NULL},
};

static const  bbsluaL_RegNum bbsluaNums[] = {
    {"interface",   BBSLUA_INTERFACE_VER},
    {NULL,          0},
};

static void
bbsluaRegConst(lua_State *L)
{
    int i = 0;

    // unbind unsafe API
    lua_pushnil(L); lua_setglobal(L, "dofile");
    lua_pushnil(L); lua_setglobal(L, "loadfile");

    // global
    lua_pushcfunction(L, bl_print);
    lua_setglobal(L, "print");

    // bbs.*
    lua_getglobal(L, "bbs");
    for (i = 0; bbsluaStrs[i].name; i++)
    {
        lua_pushstring(L, bbsluaStrs[i].val);
        lua_setfield(L, -2, bbsluaStrs[i].name);
    }

    for (i = 0; bbsluaNums[i].name; i++)
    {
        lua_pushnumber(L, bbsluaNums[i].val);
        lua_setfield(L, -2, bbsluaNums[i].name);
    }
    // dynamic info
    lua_pushstring(L, BLCONF_CURRENT_USERID);
    lua_setfield(L, -2, "userid");
    lua_pushstring(L, BLCONF_CURRENT_USERNICK);
    lua_setfield(L, -2, "usernick");

    lua_pop(L, 1);

#ifdef BLSCONF_ENABLED
    // store.*
    lua_getglobal(L, "store");
    lua_pushstring(L, BLSCONF_USER_VAL);
    lua_setfield(L, -2, "USER");
    lua_pushstring(L, BLSCONF_BN_VAL);
    lua_setfield(L, -2, "GLOBAL");
    lua_pop(L, 1);
#endif // BLSCONF_ENABLED
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA ENGINE UTILITIES
//////////////////////////////////////////////////////////////////////////

static void
bbsluaHook(lua_State *L, lua_Debug* ar)
{
    // vmsg("bbslua HOOK!");
    if (blrt.abort)
    {
        vkey_purge();
        lua_yield(L, 0);
        return;
    }

    if (ar->event != LUA_HOOKCOUNT)
        return;
#ifdef BBSLUA_USAGE
    bbslua_count += BLCONF_EXEC_COUNT;
#endif

    // now, peek and check
    if (vkey_is_full())
        vkey_purge();

    // refresh();

    // check if input key is system break key.
    if (bl_peekbreak(BLCONF_PEEK_TIME))
    {
        lua_yield(L, 0);
        return;
    }
}

static char *
bbslua_attach(const char *fpath, size_t *plen)
{
    char *buf = NULL;

#ifdef BLCONF_MMAP_ATTACH
    struct stat st;
    int fd = open(fpath, O_RDONLY);

    *plen = 0;

    if (fd < 0) return buf;
    if (fstat(fd, &st) || ((*plen = st.st_size) < 1) || S_ISDIR(st.st_mode))
    {
        close(fd);
        return buf;
    }
    *plen = *plen +1;

    buf = mmap(NULL, *plen, PROT_READ, MAP_SHARED, fd, 0);
    close(fd);

    if (buf == NULL || buf == MAP_FAILED)
    {
        *plen = 0;
        return  NULL;
    }
    madvise(buf, *plen, MADV_SEQUENTIAL);

#else // !BLCONF_MMAP_ATTACH

    FILE *fp = fopen(fpath, "rt");
    *plen = 0;
    if (!fp)
        return NULL;
    fseek(fp, 0, SEEK_END);
    *plen = ftell(fp);
    buf = (char*) malloc (*plen);
    rewind(fp);
    fread(buf, *plen, 1, fp);

#endif // !BLCONF_MMAP_ATTACH

    return buf;
}

static void
bbslua_detach(char *p, int len)
{
    assert(p);

#ifdef BLCONF_MMAP_ATTACH
    munmap(p, len);
#else // !BLCONF_MMAP_ATTACH
    free(p);
#endif // !BLCONF_MMAP_ATTACH
}

int
bbslua_isHeader(const char *ps, const char *pe)
{
    int szsig = strlen(BBSLUA_SIGNATURE);
    if (ps + szsig > pe)
        return 0;
    if (strncmp(ps, BBSLUA_SIGNATURE, szsig) == 0)
        return 1;
    return 0;
}

static int
bbslua_detect_range(char **pbs, char **pbe, int *lineshift)
{
    int szsig = strlen(BBSLUA_SIGNATURE);
    char *be, *ps, *pe;
    int line = 0;

    ps = *pbs;
    be = pe = *pbe;

    // find start
    while (ps + szsig < pe)
    {
        if (strncmp(ps, BBSLUA_SIGNATURE, szsig) == 0)
            break;
        // else, skip to next line
        while (ps + szsig < pe && *ps++ != '\n');
        line++;
    }
    *lineshift = line;

    if (!(ps + szsig < pe))
        return 0;

    *pbs = ps;
    *pbe = be;

    // find tail by SIGNATURE
    pe = ps + 1;
    while (pe + szsig < be)
    {
        if (strncmp(pe, BBSLUA_SIGNATURE, szsig) == 0)
            break;
        // else, skip to next line
        while (pe + szsig < be && *pe++ != '\n');
    }

    if (pe + szsig < be)
    {
        // found sig, end at such line.
        pe--;
        *pbe = pe;
    } else {
        // abort.
        *pbe = NULL;
        *pbs = NULL;
        return 0;
    }

    // prevent trailing zeros
    pe = *pbe;
    while (pe > ps && !*pe)
        pe--;
    *pbe = pe;

    return 1;
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA TOC Processing
//////////////////////////////////////////////////////////////////////////

static const char *bbsluaTocTags[] =
{
    "interface",
    "latestref",

    // BLCONF_PRINT_TOC_INDEX
    "title",
    "notes",
    "author",
    "version",
    "date",
    NULL
};

static const char *bbsluaTocPrompts[] =
{
    "界面版本",
    "最新版本",

    // BLCONF_PRINT_TOC_INDEX
    "名稱",
    "說明",
    "作者",
    "版本",
    "日期",
    NULL
};

int
bbslua_load_TOC(lua_State *L, const char *bs, const char *be, char **ppc)
{
    unsigned char *ps = NULL, *pe = NULL;
    int i = 0;

    lua_newtable(L);
    *ppc = NULL;

    while (bs < be)
    {
        // find stripped line start, end
        ps = pe = (unsigned char *) bs;
        while (pe < (unsigned char*)be && *pe != '\n' && *pe != '\r')
            pe ++;
        bs = (char*)pe+1;
        while (ps < pe && *ps <= ' ') ps++;
        while (pe > ps && *(pe-1) <= ' ') pe--;
        // at least "--"
        if (pe < ps+2)
            break;
        if (*ps++ != '-' || *ps++ != '-')
            break;
        while (ps < pe && *ps <= ' ') ps++;
        // empty entry?
        if (ps >= pe)
            continue;
        // find pattern
        for (i = 0; bbsluaTocTags[i]; i++)
        {
            int l = strlen(bbsluaTocTags[i]);
            if (ps + l > pe)
                continue;
            if (strncasecmp((char*)ps, bbsluaTocTags[i], l) != 0)
                continue;
            ps += l;
            // found matching pattern, now find value
            while (ps < pe && *ps <= ' ') ps++;
            if (ps >= pe || *ps++ != ':')
                break;
            while (ps < pe && *ps <= ' ') ps++;
            // finally, (ps, pe) is the value!
            if (ps >= pe)
                break;

            lua_pushlstring(L, (char*)ps, pe-ps);

            // accept only real floats for interface ([0])
            if (i == 0 && lua_tonumber(L, -1) <= 0)
            {
                lua_pop(L, 1);
            }
            else
            {
                lua_setfield(L, -2, bbsluaTocTags[i]);
            }
            break;
        }
    }

    if (ps >= (unsigned char*)bs && ps < (unsigned char*)be)
        *ppc = (char*)ps;
    lua_setglobal(L, "toc");
    return 0;
}

static void
fullmsg(const char *s)
{
    clrtoeol();
    prints("%-*.*s\n", t_columns-1, t_columns-1, s);
}

void
bbslua_logo(lua_State *L)
{
    int y, by = b_lines -1; // print - back from bottom
    int i = 0;
    double tocinterface = 0;
    int tocs = 0;
    char msg[STRLEN];

    // get toc information
    lua_getglobal(L, "toc");
    lua_getfield(L, -1, bbsluaTocTags[0]);
    tocinterface = lua_tonumber(L, -1); lua_pop(L, 1);

    // query print-able toc tags
    for (i = BLCONF_PRINT_TOC_INDEX; bbsluaTocTags[i]; i++)
    {
        lua_getfield(L, -1, bbsluaTocTags[i]);
        if (lua_tostring(L, -1))
            tocs++;
        lua_pop(L, 1);
    }

    // prepare logo window
    grayout(0, b_lines, GRAYOUT_DARK);

    // print compatibility test
    // now (by) is the base of new information
    if (tocinterface == 0)
    {
        by -= 4; y = by+2;
        move(y-1, 0);
        outs(ANSI_COLOR(0;31;47));
        fullmsg("");
        fullmsg(" ▲ 此程式缺少相容性資訊，您可能無法正常執行");
        fullmsg("    若執行出現錯誤，請向原作者取得新版");
        fullmsg("");
    }
    else if (tocinterface > BBSLUA_INTERFACE_VER)
    {
        by -= 4; y = by+2;
        move(y-1, 0);
        outs(ANSI_COLOR(0;1;37;41));
        fullmsg("");
        snprintf(msg, sizeof(msg),
                " ▲ 此程式使用新版的 BBS-Lua 規格 (%0.3f)，您可能無法正常執行",
                tocinterface);
        fullmsg(msg);
        fullmsg("   若執行出現錯誤，建議您重新登入 BBS 後再重試");
        fullmsg("");
    }
    else if (tocinterface == BBSLUA_INTERFACE_VER)
    {
        // do nothing!
    }
    else
    {
        // should be comtaible
        // prints("相容 (%.03f)", tocinterface);
    }

    // print toc, if any.
    if (tocs)
    {
        y = by - 1 - tocs;
        by = y-1;

        move(y, 0); outs(ANSI_COLOR(0;1;30;47));
        fullmsg("");

        // now try to print all TOC infos
        for (i = BLCONF_PRINT_TOC_INDEX; bbsluaTocTags[i]; i++)
        {
            lua_getfield(L, -1, bbsluaTocTags[i]);
            if (!lua_isstring(L, -1))
            {
                lua_pop(L, 1);
                continue;
            }
            move(++y, 0);
            snprintf(msg, sizeof(msg), "  %s: %-.*s",
                    bbsluaTocPrompts[i], STRLEN-12, lua_tostring(L, -1));
            msg[sizeof(msg)-1] = 0;
            fullmsg(msg);
            lua_pop(L, 1);
        }
        fullmsg("");
    }

    // print caption
    move(by-2, 0); outc('\n');
    outs(ANSI_COLOR(0;1;37;44));
    snprintf(msg, sizeof(msg),
            " ■ BBS-Lua %.03f  (Build " __DATE__ " " __TIME__") ",
            (double)BBSLUA_INTERFACE_VER);
    fullmsg(msg);

    // system break key prompt
    {
        int sz = t_columns -1;
        const char
            *prompt1 = "    提醒您執行中隨時可按 ",
            *prompt2 = "[Ctrl-C]",
            *prompt3 = " 強制中斷 BBS-Lua 程式";
        sz -= strlen(prompt1);
        sz -= strlen(prompt2);
        sz -= strlen(prompt3);
        outs(ANSI_COLOR(22;37));  outs(prompt1);
        outs(ANSI_COLOR(1;31));   outs(prompt2);
        outs(ANSI_COLOR(0;37;44));outs(prompt3);
        prints("%*s", sz, "");
        outs(ANSI_RESET);
    }
    lua_pop(L, 1);
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA Script Loader
//////////////////////////////////////////////////////////////////////////

typedef struct LoadS {
    const char *s;
    size_t size;
    int lineshift;
} LoadS;

static const char* bbslua_reader(lua_State *L, void *ud, size_t *size)
{
    LoadS *ls = (LoadS *)ud;
    (void)L;
    if (ls->size == 0) return NULL;
    if (ls->lineshift > 0) {
        const char *linefeed = "\n";
        *size = 1;
        ls->lineshift--;
        return linefeed;
    }
    *size = ls->size;
    ls->size = 0;
    return ls->s;
}

static int bbslua_loadbuffer (lua_State *L, const char *buff, size_t size,
        const char *name, int lineshift) {
    LoadS ls;
    ls.s = buff;
    ls.size = size;
    ls.lineshift = lineshift;
    return lua_load(L, bbslua_reader, &ls, name);
}

typedef struct AllocData {
    size_t alloc_size;
    size_t max_alloc_size;
    size_t alloc_limit;
} AllocData;

static void alloc_init(AllocData *ad)
{
    memset(ad, 0, sizeof(*ad));
    // real limit is not determined yet, just assign a big value
    ad->alloc_limit = 20*1048576;
}

static void *allocf (void *ud, void *ptr, size_t osize, size_t nsize) {
    /* TODO use our own allocator, for better memory control, avoid fragment and leak */
    AllocData *ad = (AllocData*)ud;

    if (ad->alloc_size + nsize - osize > ad->alloc_limit) {
        return NULL;
    }
    ad->alloc_size += nsize - osize;
    if (ad->alloc_size > ad->max_alloc_size) {
        ad->max_alloc_size = ad->alloc_size;
    }
    if (nsize == 0) {
        free(ptr);
        return NULL;
    }
    else
        return realloc(ptr, nsize);
}
static int panic (lua_State *L) {
    (void)L;  /* to avoid warnings */
    fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n",
            lua_tostring(L, -1));
    return 0;
}

void bbslua_loadLatest(lua_State *L,
        char **pbs, char **pps, char **ppe, char **ppc, int *psz,
        int *plineshift, char *bfpath, const char **pfpath)
{
    int r = 1; // max redirect for one article only.

    do {
        char *xbs = NULL, *xps = NULL, *xpe = NULL, *xpc = NULL;
        int xlineshift = 0;
        size_t xsz;
        const char *lastref = NULL;
        char loadnext = 0, isnewver = 0;

        // detect file
        xbs = bbslua_attach(bfpath, &xsz);
        if (!xbs)
            break;

        xps = xbs;
        xpe = xps + xsz;

        if(!bbslua_detect_range(&xps, &xpe, &xlineshift))
        {
            // not detected
            bbslua_detach(xbs, xsz);
            break;
        }

        // detect and verify TOC meta data
        lua_getglobal(L, "toc");    // stack 1
        if (lua_istable(L, -1))
        {
            const char *oref, *otitle, *nref, *ntitle;
            // load and verify tocinfo
            lua_getfield(L, -1, "latestref");   // stack 2
            oref = lua_tostring(L, -1);
            lua_getfield(L, -2, "title");       // stack 3
            otitle = lua_tostring(L, -1);
            bbslua_load_TOC(L, xps, xpe, &xpc);
            lua_getglobal(L, "toc");            // stack 4
            lua_getfield(L, -1, "latestref");   // stack 5
            nref = lua_tostring(L, -1);
            lua_getfield(L, -2, "title");       // stack 6
            ntitle = lua_tostring(L, -1);

            if (oref && nref && otitle && ntitle &&
                    strcmp(oref, nref) == 0 &&
                    strcmp(otitle, ntitle) == 0)
                isnewver = 1;

            // pop all
            lua_pop(L, 5);              // stack = 1 (old toc)
            if (!isnewver)
                lua_setglobal(L, "toc");
            else
                lua_pop(L, 1);
        } else {
            lua_pop(L, 1);
            bbslua_load_TOC(L, xps, xpe, &xpc);
        }

        if (*pbs && !isnewver)
        {
            // different.
            bbslua_detach(xbs, xsz);
            break;
        }

        // now, apply current buffer
        if (*pbs)
        {
            bbslua_detach(*pbs, *psz);
            // XXX fpath=bfpath, supporting only one level redirection now.
            *pfpath = bfpath;
        }
        *pbs = xbs; *pps = xps; *ppe = xpe; *psz = xsz;
        *ppc = xpc;
        *plineshift = xlineshift;

#ifdef AID_DISPLAYNAME
        // quick exit
        if (r <= 0)
            break;

        // LatestRef only works if system supports AID.
        // try to load next reference.
        lua_getglobal(L, "toc");            // stack 1
        lua_getfield(L, -1, "latestref");   // stack 2
        lastref = lua_tostring(L, -1);

        while (lastref && *lastref)
        {
            // try to load by AID
            char bn[IDLEN+1] = "";
            aidu_t aidu = 0;
            unsigned char *p = (unsigned char*)bn;

            if (*lastref == '#') lastref++;
            aidu = aidc2aidu((char*)lastref);
            if (aidu <= 0) break;

            while (*lastref > ' ') lastref ++;              // lastref points to zero of space
            while (*lastref && *lastref <= ' ') lastref++;  // lastref points to zero or board name
            if (*lastref == '(') lastref ++;

            if (!*lastref) break;
            strlcpy(bn, lastref, sizeof(bn));
            // truncate board name
            // (not_alnum(ch) && ch != '_' && ch != '-' && ch != '.')
            while (*p &&
                (isalnum(*p) || *p == '_' || *p == '-' || *p == '.')) p++;
            *p = 0;
            if (bn[0])
            {
                bfpath[0] = 0;
                setaidfile(bfpath, bn, aidu);
            }

            if (bfpath[0])
                loadnext = 1;
            break;
        }
        lua_pop(L, 2);

        if (loadnext) continue;
#endif // AID_DISPLAYNAME
        break;

    } while (r-- > 0);
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA Hash
//////////////////////////////////////////////////////////////////////////

#if 0
static int
bbslua_hashWriter(lua_State *L, const void *p, size_t sz, void *ud)
{
    Fnv32_t *phash = (Fnv32_t*) ud;
    *phash = fnv_32_buf(p, sz, *phash);
    return 0;
}

static Fnv32_t
bbslua_f2hash(lua_State *L)
{
    Fnv32_t fnvseed = FNV1_32_INIT;
    lua_dump(L, bbslua_hashWriter, &fnvseed);
    return fnvseed;
}

static Fnv32_t
bbslua_str2hash(const char *str)
{
    if (!str)
        return 0;
    return fnv_32_str(str, FNV1_32_INIT);
}
#endif

static Fnv32_t
bbslua_path2hash(const char *path)
{
    Fnv32_t seed = FNV1_32_INIT;
    if (!path)
        return 0;
    if (path[0] != '/') // relative, append BBSHOME
        seed = fnv_32_str(BBSHOME "/", seed);
    return fnv_32_str(path, seed);
}

//////////////////////////////////////////////////////////////////////////
// BBSLUA Main
//////////////////////////////////////////////////////////////////////////

int
bbslua(const char *fpath)
{
    int r = 0;
    lua_State *L;
    char *bs = NULL, *ps = NULL, *pe = NULL, *pc = NULL;
    char bfpath[PATHLEN] = "";
    int sz = 0;
    int lineshift = 0;
    AllocData ad;
#ifdef BBSLUA_USAGE
    struct rusage rusage_begin, rusage_end;
    struct timeval lua_begintime, lua_endtime;
    gettimeofday(&lua_begintime, NULL);
    getrusage(0, &rusage_begin);
#endif

#ifdef UMODE_BBSLUA
    unsigned int prevmode = getutmpmode();
#endif

    // re-entrant not supported!
    if (blrt.running)
        return 0;

    // initialize runtime
    BL_INIT_RUNTIME();

#ifdef BBSLUA_USAGE
    bbslua_count = 0;
#endif

    // init lua
    alloc_init(&ad);
    L = lua_newstate(allocf, &ad);
    if (!L)
        return 0;
    lua_atpanic(L, &panic);

    strlcpy(bfpath, fpath, sizeof(bfpath));

    // load file
    bbslua_loadLatest(L, &bs, &ps, &pe, &pc, &sz, &lineshift, bfpath, &fpath);

    if (!ps || !pe || ps >= pe)
    {
        if (bs)
            bbslua_detach(bs, sz);
        lua_close(L);
        vmsg("BBS-Lua 載入錯誤: 未含 BBS-Lua 程式");
        return 0;
    }

    // init library
    bbsluaL_openlibs(L);
    luaL_openlib(L,   "bbs",    lib_bbslua, 0);

#ifdef BLSCONF_ENABLED
    luaL_openlib(L,   "store",  lib_store, 0);
#endif // BLSCONF_ENABLED

    bbsluaRegConst(L);

    // load script
    r = bbslua_loadbuffer(L, ps, pe-ps, "BBS-Lua", lineshift);

    // build hash or store name
    blrt.storename = bbslua_path2hash(fpath);
    // vmsgf("BBS-Lua Hash: %08X", blrt.storename);

    // unmap
    bbslua_detach(bs, sz);

    if (r != 0)
    {
        const char *errmsg = lua_tostring(L, -1);
        outs(ANSI_RESET);
        move(b_lines-3, 0); clrtobot();
        outs("\n");
        outs(errmsg);
        lua_close(L); // delay closing because we need to print out error message
        vmsg("BBS-Lua 載入錯誤: 請通知作者修正程式碼。");
        return 0;
    }

#ifdef UMODE_BBSLUA
    setutmpmode(UMODE_BBSLUA);
#endif

    bbslua_logo(L);
    vmsgf("提醒您執行中隨時可按 [Ctrl-C] 強制中斷 BBS-Lua 程式");

    // ready for running
    clear();
    blrt.running =1;
    lua_sethook(L, bbsluaHook, LUA_MASKCOUNT, BLCONF_EXEC_COUNT );

    refresh();
    // check is now done inside hook
    r = lua_resume(L, 0);

    // even if r == yield, let's abort - you cannot yield main thread.

    if (r != 0)
    {
        const char *errmsg = lua_tostring(L, -1);
        move(b_lines-3, 0); clrtobot();
        outs("\n");
        if (errmsg) outs(errmsg);
    }

    lua_close(L);
    blrt.running =0;
    vkey_purge();
#ifdef BBSLUA_USAGE
    {
        double cputime;
        double load;
        double walltime;
        getrusage(0, &rusage_end);
        gettimeofday(&lua_endtime, NULL);
        cputime = bl_tv2double(&rusage_end.ru_utime) - bl_tv2double(&rusage_begin.ru_utime);
        walltime = bl_tv2double(&lua_endtime) - bl_tv2double(&lua_begintime);
        load = cputime / walltime;
        log_filef("log/bbslua.log", LOG_CREAT,
                "maxalloc=%d leak=%d op=%d cpu=%.3f Mop/s=%.1f load=%f file=%s\n",
                (int)ad.max_alloc_size, (int)ad.alloc_size,
                bbslua_count, cputime, bbslua_count / cputime / 1000000.0, load * 100,
                fpath);
    }
#endif

    // grayout(0, b_lines, GRAYOUT_DARK);
    move(b_lines, 0); clrtoeol();
    vmsgf("BBS-Lua 執行結束%s。",
            blrt.abort ? " (使用者中斷)" : r ? " (程式錯誤)" : "");
    BL_END_RUNTIME();
    clear();

#ifdef UMODE_BBSLUA
    setutmpmode(prevmode);
#endif

    return 0;
}

// vim:ts=4:sw=4:expandtab
