code:
-- Savegame facilities for Lunatic.

local string = require("string")
local table = require("table")

local format = string.format

local assert = assert
local getmetatable = getmetatable
local pairs = pairs
local setmetatable = setmetatable
local tostring = tostring
local type = type

local cansave_cdata = require("lprivate").cansave_cdata

module(...)

---=== Serialization, based on the idea from PiL ===---

assert(tostring(0/0)=="nan")
assert(tostring(1/0)=="inf")

-- Serialize a 'primitive' Lua value.
local function basicSerialize(o)
    -- Compare with sb_get_initial_strbuf() below.
    -- XXX: nil?
    if (type(o) == "number") then
        -- NOTE: NaN and infinity handled.
        return tostring(o)
    elseif (type(o) == "boolean") then
        return o and "t" or "f"
    elseif (type(o) == "string") then
        -- TODO: return refname if it's shorter
        return format("%q", o)
    end
end

local function isSerializeable(obj)
    return (getmetatable(obj)=="serializeable" or cansave_cdata(obj))
end

-- 'Save buffer' class.  Objects of this class keep track of a string buffer
-- contiaining Lua code to recreate the values of particular variables from a
-- user Lunatic environment.
local savebuffer_mt = {
    __index = {
        -- Called on starting processing one module.
        startmod = function(self, modname)
            self:addrawf("if (N==%q) then", modname)
            -- Will contain all tables and cdata refs of this module, indexed
            -- by number.
            self:addraw("local T={}")
            self:resetRefs()
        end,

        -- Called on finishing processing one module.
        endmod = function(self)
            self:addraw("end")
        end,

        --== vvv INTERNAL vvv ===---
        resetRefs = function(self)
            self.numrefs = 0
            self.recursionDepth = 0
            self.val2ref = {}
            self.havereq = {}
        end,

        -- Get the code necessary to create this object, usually 'require'ing a
        -- module into a local variable.
        getRequire = function(self, value)
            local reqcode = value:_get_require()
            if (self.havereq[reqcode] == nil) then
                self.havereq[reqcode] = true
                self.strbuf[#self.strbuf+1] = reqcode
            end
        end,

        emitT = function(self, refcode, valcode, obj)
            self:addrawf("%s=%s", refcode, valcode)
            self.val2ref[obj] = refcode
        end,

        emitAssign = function(self, lhscode, refcode)
            self:addrawf("%s=%s", lhscode, refcode)
        end,
        --== ^^^ INTERNAL ^^^ ===---

        -- Add an entry of Lua object <value> that can be referenced by
        -- <lhscode> on the left hand side of an assignment.
        -- <excludedKeys>: if non-nil, should be table
        --  [<key_string>] = true. See EXCLUDE_KEYS below.
        -- Returns 'true' if <value> cannot be serialized.
        add = function(self, lhscode, value, excludedKeys)
            local valcode = basicSerialize(value)

            if (valcode == nil) then
                -- <value> is a not a 'basic' Lua object, but one passed by
                -- reference.
                if (not self.val2ref[value]) then
                    -- Object is being serialized for the first time.

                    -- Create a slot in 'T' for this object by which we can
                    -- refer to it in the following.
                    self.numrefs = self.numrefs+1
                    local refcode = format("T[%d]", self.numrefs)

                    if (isSerializeable(value)) then
                        -- We have a serializeable object from Lunatic
                        -- (e.g. actorvar).
                        self:getRequire(value)
                        local restoreCode = value:_serialize()
                        if (restoreCode == nil) then  -- XXX: check UNUSED?
                            return true
                        end
                        self:emitT(refcode, restoreCode, value)
                        valcode = refcode

                    elseif (type(value)=="table") then
                        -- Create a new table for this gamevar.
                        -- TODO: emit table initializations where possible.
                        self:emitT(refcode, "{}", value)

                        for k,v in pairs(value) do
                            local keystr = basicSerialize(k)
                            if (keystr == nil) then
                                return true
                            end

                            -- EXCLUDE_KEYS
                            local isNORESET = (self.recursionDepth == 0 and
                                                   excludedKeys and excludedKeys[k])

                            -- Generate the name under which the table element
                            -- is referenced.
                            local refcode2 = format("%s[%s]", refcode, keystr)

                            if (isNORESET) then
                                self:emitAssign(refcode2, format("M._V[%s]", keystr))
                            else
                                -- Recurse!
                                self.recursionDepth = self.recursionDepth+1
                                local ret = self:add(refcode2, v)
                                self.recursionDepth = self.recursionDepth-1

                                if (ret) then
                                    return true
                                end
                            end
                        end

                        valcode = refcode
                    else
                        -- We have anything else: can't serialize.
                        return true
                    end
                else
                    -- Object was previously serialized, get Lua expression it
                    -- can be referenced with.
                    valcode = self.val2ref[value]
                end
            end

            self:emitAssign(lhscode, valcode)
        end,

        -- Add a single string to the buffer.
        addraw = function(self, str)
            self.strbuf[#self.strbuf+1] = str
        end,

        -- Add a single formatted string to the buffer.
        addrawf = function(self, fmt, ...)
            self:addraw(format(fmt, ...))
        end,

        -- Get the Lua code recreating the values as a string.
        getcode = function(self)
            self.strbuf[#self.strbuf+1] = ""  -- add newline at EOS
            return table.concat(self.strbuf, "\n")
        end,
    }
}

local function sb_get_initial_strbuf()
    return {
        "local nan, inf = 0/0, 1/0",
        "local t, f = true, false",
    }
end

-- Create a new savebuffer object.
function savebuffer()
    -- .numrefs: how many table or cdata objects we have serialized
    -- .val2ref: [<Lua object>] = <Lua code string>
    -- .havereq = [<string>] = true
    -- .strbuf: array of Lua code pieces
    local sb = {
        numrefs=0, val2ref={}, havereq={},
        recursionDepth = 0,
        strbuf=sb_get_initial_strbuf()
    }

    return setmetatable(sb, savebuffer_mt)
end

repo_name: orochi/nblood

license: mit


code:
/* The Lunatic Interpreter, part of EDuke32. Editor stuff. */

#ifdef USE_LUAJIT_2_1
# include <luajit-2.1/lualib.h>
#else
# include <luajit-2.0/lualib.h>
#endif

#include "lunatic_editor.h"

int Em_CreateState(L_State *estate)
{
    return L_CreateState(estate, "m32", NULL);
}

void Em_DestroyState(L_State *estate)
{
    L_DestroyState(estate);
}


code:
/* The Lunatic Interpreter, part of EDuke32. Editor stuff. */

#ifndef EDUKE32_LUNATIC_M32_H_
#define EDUKE32_LUNATIC_M32_H_

#include "lunatic.h"

#ifdef __cplusplus
extern "C" {
#endif

int Em_CreateState(L_State *estate);
void Em_DestroyState(L_State *estate);

#ifdef __cplusplus
}
#endif

#endif


code:
/* The Lunatic Interpreter, part of EDuke32. Game-side stuff. */

#include "compat.h"

#ifdef __cplusplus
extern "C" {
#endif

#ifdef USE_LUAJIT_2_1
# include <luajit-2.1/lualib.h>
# include <luajit-2.1/lauxlib.h>
#else
# include <luajit-2.0/lualib.h>
# include <luajit-2.0/lauxlib.h>
#endif

#ifdef __cplusplus
}
#endif

#include "build.h"  // printext256

#include "lunatic_game.h"

#include "osd.h"
#include "gamedef.h"  // EventNames[]

L_State g_ElState;

// this serves two purposes:
// the values as booleans and the addresses as keys to the Lua registry
uint8_t g_elEvents[MAXEVENTS];

// same thing for actors:
el_actor_t g_elActors[MAXTILES];

// Session variable. Never restored except by 'readgamevar'.
int32_t g_elSessionVar[8];  // MAXSESSIONVARS, KEEPINSYNC con_lang.lua

// Set to 1 on error in event.
int32_t g_elEventError;

// Will be set to 0 after the first time that user Lua modules are run.
int32_t g_elFirstTime = 1;

int32_t g_elCallDepth = 0;
int32_t g_RETURN;

// for timing events and actors
static int32_t g_timingInited = 0;

// Used as Lua registry key to the tweak_traceback_msg() function, set to 1 if
// such a function has been registered.
static uint8_t g_tweakTracebackMsg = 0;

// forward-decls...
static int32_t SetEvent_CF(lua_State *L);
static int32_t SetActor_CF(lua_State *L);

#ifdef __cplusplus
extern "C" {
#endif

// in lpeg.o
extern int luaopen_lpeg(lua_State *L);

#ifdef __cplusplus
}
#endif

// See: Good Practice in (Pseudo) Random Number Generation for
//      Bioinformatics Applications, by David Jones
ATTRIBUTE_OPTIMIZE("O2")
LUNATIC_EXTERN uint32_t rand_jkiss_u32(rng_jkiss_t *s)
{
    uint64_t t;
    s->x = 314527869 * s->x + 1234567;
    s->y ^= s->y << 5; s->y ^= s->y >> 7; s->y ^= s->y << 22;
    t = 4294584393ULL * s->z + s->c; s->c = t >> 32; s->z = t;
    return s->x + s->y + s->z;
}

ATTRIBUTE_OPTIMIZE("O2")
LUNATIC_EXTERN double rand_jkiss_dbl(rng_jkiss_t *s)
{
    double x;
    unsigned int a, b;
    a = rand_jkiss_u32(s) >> 6; /* Upper 26 bits */
    b = rand_jkiss_u32(s) >> 5; /* Upper 27 bits */
    x = (a * 134217728.0 + b) / 9007199254740992.0;
    return x;
}

void El_PrintTimes(void)
{
    int32_t i;
    const char nn = Bstrlen("EVENT_");

    // Try environment variable specifying the base name (sans ".actors.csv" or
    // ".events.csv") for a CSV file to output, for further processing in e.g.
    // GSL shell: http://www.nongnu.org/gsl-shell/
    const char *basefn = getenv("LUNATIC_TIMING_BASEFN");

    if (basefn != NULL)
    {
        const int32_t baselen = Bstrlen(basefn);
        const int32_t addnlen = Bstrlen(".actors.csv");  // MUST equal that of ".events.csv"

        char *fullfn = (char *)Xmalloc(baselen + addnlen + 1);
        BFILE *outf;

        if (fullfn == NULL)
            return;

        Bmemcpy(fullfn, basefn, baselen);

        // EVENTS
        Bmemcpy(fullfn+baselen, ".events.csv", addnlen+1);
        outf = Bfopen(fullfn, "w");
        if (outf == NULL)
        {
            OSD_Printf("Couldn't open \"%s\" for writing timing data: %s", fullfn, strerror(errno));
            goto finish;
        }

        Bfprintf(outf, "evtname,numcalls,total_ms,mean_us\n");  // times in usecs are per-call
        for (i=0; i<MAXEVENTS; i++)
            if (g_eventCalls[i])
                Bfprintf(outf, "%s,%d,%f,%f\n", EventNames[i]+nn, g_eventCalls[i], g_eventTotalMs[i],
                         1000*g_eventTotalMs[i]/g_eventCalls[i]);
        Bfclose(outf);

        // ACTORS
        Bmemcpy(fullfn+baselen, ".actors.csv", addnlen+1);
        outf = Bfopen(fullfn, "w");
        if (outf == NULL)
        {
            OSD_Printf("Couldn't open \"%s\" for writing timing data: %s", fullfn, strerror(errno));
            goto finish;
        }

        Bfprintf(outf, "tilenum,numcalls,total_ms,min_us,mean_us,max_us\n");
        for (i=0; i<MAXTILES; i++)
            if (g_actorCalls[i])
                Bfprintf(outf, "%d,%d,%f,%f,%f,%f\n", i, g_actorCalls[i], g_actorTotalMs[i],
                         1000*g_actorMinMs[i],
                         1000*g_actorTotalMs[i]/g_actorCalls[i],
                         1000*g_actorMaxMs[i]);
        Bfclose(outf);

        OSD_Printf("Wrote timing data to \"%s.*.csv\"\n", basefn);
finish:
        Bfree(fullfn);
        return;
    }
    else
    {
        // If not writing out CSV files, print timing data to log instead.

        char buf[32];
        int32_t maxlen = 0;
        int32_t haveev=0, haveac=0;

        for (i=0; i<MAXEVENTS; i++)
        {
            int32_t len = Bstrlen(EventNames[i]+nn);
            Bassert(len < (int32_t)sizeof(buf));
            maxlen = max(len, maxlen);
        }

        for (i=0; i<MAXEVENTS; i++)
            if (g_eventCalls[i])
            {
                int32_t n=Bsprintf(buf, "%s", EventNames[i]+nn);

                if (!haveev)
                {
                    haveev = 1;
                    OSD_Printf("\n  -- event times: [event]={ total calls, total time [ms], mean time/call [us] }\n");
                }

                for (; n<maxlen; n++)
                    buf[n] = ' ';
                buf[maxlen] = 0;

                OSD_Printf("  [%s]={ %8d, %10.3f, %10.3f },\n",
                           buf, g_eventCalls[i], g_eventTotalMs[i],
                           1000*g_eventTotalMs[i]/g_eventCalls[i]);
            }

        for (i=0; i<MAXTILES; i++)
            if (g_actorCalls[i])
            {
                if (!haveac)
                {
                    haveac = 1;
                    OSD_Printf("\n  -- actor times: [tile]={ total calls, total time [ms], {min,mean,max} time/call [us] }\n");
                }

                OSD_Printf("  [%5d]={ %8d, %9.3f, %9.3f, %9.3f, %9.3f },\n",
                           i, g_actorCalls[i], g_actorTotalMs[i],
                           1000*g_actorMinMs[i],
                           1000*g_actorTotalMs[i]/g_actorCalls[i],
                           1000*g_actorMaxMs[i]);
            }
    }
}

////////// ERROR REPORTING //////////
#define EL_MAXERRORS 20
static int32_t el_numErrors=0, el_tooMuchErrors;
static char *el_errorMsgs[EL_MAXERRORS];
int8_t el_addNewErrors = 1;  // add new errors to display?

// Compare against all other error messages.
// Strictly seen, this is quadratic-time, but EL_MAXERRORS is small and
// errors should be fixed anyway.
static int32_t cmp_against_others(const char *str, int32_t slen)
{
    int32_t i;
    for (i=0; i<el_numErrors; i++)
        if (!Bstrncmp(str, el_errorMsgs[i], slen))
            return 1;
    return 0;
}

LUNATIC_EXTERN void El_OnError(const char *str)
{
    if (el_addNewErrors && !el_tooMuchErrors)
    {
        char *errstr = NULL;
        const char *nl = Bstrchr(str, '\n');

        // First, check whether the error message matches an already saved one
        if (nl)
        {
            // cut off string after the newline
            if (cmp_against_others(str, nl-str))
                return;
        }
        else
        {
            // save string fully
            if (cmp_against_others(str, Bstrlen(str)))
                return;
        }

        // If the (EL_MAXERRORS+1)'th distinct error appeared, we have too many.
        if (el_numErrors==EL_MAXERRORS)
        {
            el_tooMuchErrors = 1;
            return;
        }

        // Otherwise, allocate storage for the potentially clipped error string...
        if (nl)
        {
            errstr = (char *)Xmalloc(nl-str+1);
            Bmemcpy(errstr, str, nl-str);
            errstr[nl-str] = 0;
        }
        else
        {
            errstr = Xstrdup(str);
        }

        // ...and save it:
        el_errorMsgs[el_numErrors++] = errstr;
    }
}

void El_ClearErrors(void)
{
    int32_t i;
    for (i=0; i<EL_MAXERRORS; i++)
        DO_FREE_AND_NULL(el_errorMsgs[i]);
    el_numErrors = el_tooMuchErrors = 0;
}

void El_DisplayErrors(void)
{
    int32_t i;
    for (i=0; i<el_numErrors; i++)
        printext256(8, 8+8*i, 242, 0, el_errorMsgs[i], 0);
    if (el_tooMuchErrors)
        printext256(8, 8+8*EL_MAXERRORS, 242, 0, "(more distinct errors ...)", 0);
}

////////// STATE CREATION/DESTRUCTIION //////////

static int our_traceback_CF(lua_State *L)
{
    Bassert(lua_gettop(L)==1);

    if (lua_type(L, 1)==LUA_TBOOLEAN)
    {
        lua_pushvalue(L, 1);  // duplicate it
        return 1;  // and tell Lua to return it
    }

    Bassert(lua_type(L, 1)==LUA_TSTRING);

    // call debug.traceback with the string
    L_PushDebugTraceback(L);
    lua_pushvalue(L, 1);
    lua_call(L, 1, 1);
    Bassert(lua_gettop(L)==2);  // Lua will pop off args

    if (g_tweakTracebackMsg)
    {
        // Get tweak_traceback_msg() onto the stack.
        lua_pushlightuserdata(L, &g_tweakTracebackMsg);
        lua_gettable(L, LUA_REGISTRYINDEX);

        lua_pushvalue(L, -2);  // push copy of error message string
        Bassert(lua_type(L, -1)==LUA_TSTRING);

        // Call tweak_traceback_msg(). CAREFUL, it's unprotected!
        lua_call(L, 1, 1);
    }

    return 1;
}

// Registers a function: str = tweak_traceback_msg(str)
static int32_t SetTweakTracebackMsg_CF(lua_State *L)
{
    Bassert(lua_gettop(L)==1);
    L_CheckAndRegisterFunction(L, &g_tweakTracebackMsg);
    g_tweakTracebackMsg = 1;
    return 0;
}

////// Lua C-API interfaces for C game functions that may call events.
// http://www.freelists.org/post/luajit/intermitten-lua-pcall-crash-on-x86-64-linux,1

// Some of these are duplicate declarations:
#ifdef __cplusplus
extern "C" {
#endif
extern void P_AddWeaponMaybeSwitchI(int32_t snum, int32_t weap);
void P_CheckWeaponI(int playerNum);
extern int32_t A_ShootWithZvel(int32_t i, int32_t atwith, int32_t override_zvel);
extern int A_Spawn(int j, int pn);
extern void VM_FallSprite(int32_t i);
extern int32_t VM_ResetPlayer2(int32_t snum, int32_t flags);
extern void A_RadiusDamage(int32_t i, int32_t r, int32_t, int32_t, int32_t, int32_t);
extern void G_OperateSectors(int sectNum, int spriteNum);
extern void G_OperateActivators(int32_t low,int32_t snum);
int32_t A_InsertSprite(int16_t whatsect,int32_t s_x,int32_t s_y,int32_t s_z,int16_t s_pn,int8_t s_s,
                              uint8_t s_xr,uint8_t s_yr,int16_t s_a,int16_t s_ve,int16_t s_zv,int16_t s_ow,int16_t s_ss);
extern void A_AddToDeleteQueue(int32_t i);
extern int32_t A_PlaySound(uint32_t num, int32_t i);
void A_DeleteSprite(int spriteNum);
extern void G_ShowView(vec3_t vec, int32_t a, int32_t horiz, int32_t sect,
                       int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t unbiasedp);
extern void G_GameExit(const char *msg);
#ifdef __cplusplus
}
#endif

static void G_ShowViewXYZ(int32_t x, int32_t y, int32_t z, int32_t a, int32_t horiz, int32_t sect,
                          int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t unbiasedp)
{
    vec3_t vec = { x, y, z };
    G_ShowView(vec, a, horiz, sect, x1, y1, x2, y2, unbiasedp);
}

#define LARG(index) lua_tointeger(L, index)

#define ONE_ARG LARG(1)
#define TWO_ARGS LARG(1), LARG(2)
#define THREE_ARGS LARG(1), LARG(2), LARG(3)

#define CALL_WITH_RET(Name, ...) \
    int32_t ret = Name(__VA_ARGS__); \
    lua_pushinteger(L, ret); \
    return 1

#define CALL_WITHOUT_RET(Name, ...) \
    Name(__VA_ARGS__); \
    return 0

#define DEFINE_RET_CFUNC(Name, ...) \
static int32_t Name##_CF(lua_State *L) \
{ \
    CALL_WITH_RET(Name, __VA_ARGS__); \
}

#define DEFINE_VOID_CFUNC(Name, ...) \
static int32_t Name##_CF(lua_State *L) \
{ \
    CALL_WITHOUT_RET(Name, __VA_ARGS__); \
}

// NOTE: player struct -> player index -> player struct ugliness because
// pointers to FFI cdata apparently can't be reliably passed via lua_getpointer().
// Not to mention that lua_getpointer() returns _const_ void*.
DEFINE_VOID_CFUNC(P_AddWeaponMaybeSwitchI, TWO_ARGS)
DEFINE_VOID_CFUNC(P_CheckWeaponI, ONE_ARG)
DEFINE_RET_CFUNC(A_ShootWithZvel, THREE_ARGS)
DEFINE_RET_CFUNC(A_Spawn, TWO_ARGS)
DEFINE_VOID_CFUNC(VM_FallSprite, ONE_ARG)
DEFINE_RET_CFUNC(VM_ResetPlayer2, TWO_ARGS)
DEFINE_VOID_CFUNC(A_RadiusDamage, LARG(1), LARG(2), LARG(3), LARG(4), LARG(5), LARG(6))
DEFINE_VOID_CFUNC(G_OperateSectors, TWO_ARGS)
DEFINE_VOID_CFUNC(G_OperateActivators, TWO_ARGS)
DEFINE_RET_CFUNC(A_InsertSprite, LARG(1), LARG(2), LARG(3), LARG(4), LARG(5), LARG(6),
                 LARG(7), LARG(8), LARG(9), LARG(10), LARG(11), LARG(12), LARG(13))
DEFINE_VOID_CFUNC(A_AddToDeleteQueue, ONE_ARG)
DEFINE_RET_CFUNC(A_PlaySound, TWO_ARGS)
DEFINE_VOID_CFUNC(A_DeleteSprite, ONE_ARG)
DEFINE_VOID_CFUNC(G_ShowViewXYZ, LARG(1), LARG(2), LARG(3), LARG(4), LARG(5), LARG(6),
                  LARG(7), LARG(8), LARG(9), LARG(10), LARG(11))

#define CFUNC_REG(Name) { #Name, Name##_CF }

static struct { const char *name; lua_CFunction func; } cfuncs[] =
{
    CFUNC_REG(P_AddWeaponMaybeSwitchI),
    CFUNC_REG(P_CheckWeaponI),
    CFUNC_REG(A_ShootWithZvel),
    CFUNC_REG(A_Spawn),
    CFUNC_REG(VM_FallSprite),
    CFUNC_REG(VM_ResetPlayer2),
    CFUNC_REG(A_RadiusDamage),
    CFUNC_REG(G_OperateSectors),
    CFUNC_REG(G_OperateActivators),
    CFUNC_REG(A_InsertSprite),
    CFUNC_REG(A_Spawn),
    CFUNC_REG(A_AddToDeleteQueue),
    CFUNC_REG(A_PlaySound),
    CFUNC_REG(A_DeleteSprite),
    CFUNC_REG(G_ShowViewXYZ),
};

// Creates a global table "CF" containing the functions from cfuncs[].
static void El_PushCFunctions(lua_State *L)
{
    int32_t i;

    lua_newtable(L);

    for (i=0; i<(signed)sizeof(cfuncs)/(signed)sizeof(cfuncs[0]); i++)
    {
        lua_pushstring(L, cfuncs[i].name);
        lua_pushcfunction(L, cfuncs[i].func);
        lua_settable(L, -3);
    }

    lua_setglobal(L, "CF");
}

//////

LUNATIC_CB int32_t (*El_RestoreGamevars)(const char *savecode);

static void El_StateSetup(lua_State *L)
{
    luaopen_lpeg(L);
    lua_pop(L, lua_gettop(L));  // pop off whatever lpeg leaves on the stack

    // create misc. global functions in the Lua state
    lua_pushcfunction(L, SetEvent_CF);
    lua_setglobal(L, "gameevent_internal");
    lua_pushcfunction(L, SetActor_CF);
    lua_setglobal(L, "gameactor_internal");
    lua_pushcfunction(L, SetTweakTracebackMsg_CF);
    lua_setglobal(L, "set_tweak_traceback_internal");

    El_PushCFunctions(L);

    Bassert(lua_gettop(L)==0);

    // This is for engine-side Lua:
    lua_pushcfunction(L, &our_traceback_CF);
}

static void El_OnOutOfMem(void)
{
    G_GameExit("Out of memory in Lunatic.");
}

// 0: success, <0: failure
int32_t El_CreateState(L_State *estate, const char *name)
{
    int32_t i;

    if (!g_timingInited)
    {
        g_timingInited = 1;
        for (i=0; i<MAXTILES; i++)
            g_actorMinMs[i] = 1e308;
    }

    L_ErrorFunc = El_OnError;
    L_OutOfMemFunc = El_OnOutOfMem;

    return L_CreateState(estate, name, &El_StateSetup);
}

void El_DestroyState(L_State *estate)
{
    L_DestroyState(estate);

    g_tweakTracebackMsg = 0;

    // XXX: It would be cleaner to also clear stuff like g_elEvents[], but
    // currently, when the game Lua state is recreated, the array should have
    // the same values as before, so we're skipping that for now.
}

////////// Lua_CFunctions //////////

// gameevent(EVENT_..., lua_function)
static int32_t SetEvent_CF(lua_State *L)
{
    int32_t eventidx;

    Bassert(lua_gettop(L) == 2);
    eventidx = luaL_checkint(L, 1);
    Bassert((unsigned)eventidx < MAXEVENTS);

    L_CheckAndRegisterFunction(L, &g_elEvents[eventidx]);
    g_elEvents[eventidx] = 1;

    return 0;
}

// gameactor(actortile, strength, act, mov, movflags, lua_function)
static int32_t SetActor_CF(lua_State *L)
{
    int32_t actortile;
    el_actor_t *a;

    Bassert(lua_gettop(L) == 6);

    actortile = luaL_checkint(L, 1);
    Bassert((unsigned)actortile < MAXTILES);

    a = &g_elActors[actortile];
    L_CheckAndRegisterFunction(L, a);

    // Set actor properties. They can only be nil if we're chaining actor code.

    if (!lua_isnil(L, 2))
        a->strength = luaL_checkint(L, 2);
    if (!lua_isnil(L, 5))
        a->movflags = luaL_checkint(L, 5);

    if (!lua_isnil(L, 3))
        Bmemcpy(&a->act, lua_topointer(L, 3), sizeof(con_action_t));
    if (!lua_isnil(L, 4))
        Bmemcpy(&a->mov, lua_topointer(L, 4), sizeof(con_move_t));

    a->haveit = 1;

    return 0;
}

//////////////////////////////

static int32_t call_regd_function3(lua_State *L, void *keyaddr,
                                   int32_t spriteNum, int32_t playerNum, int32_t lDist)
{
#if !defined NDEBUG
    const int32_t top = lua_gettop(L);
#endif
    lua_pushcfunction(L, &our_traceback_CF);

    // get the Lua function from the registry
    lua_pushlightuserdata(L, keyaddr);
    lua_gettable(L, LUA_REGISTRYINDEX);

    lua_pushinteger(L, spriteNum);
    lua_pushinteger(L, playerNum);
    lua_pushinteger(L, lDist);

    // -- call it! --
    {
        const int32_t i = lua_pcall(L, 3, 0, -5);
        const int32_t haveerr = (i != 0);

        Bassert(lua_iscfunction(L, -1-haveerr));
        lua_remove(L, -1-haveerr);

        Bassert(lua_gettop(L) == top+haveerr);

        return i;
    }
}

static int32_t g_eventIdx = 0;
static void El_EventErrorPrint(const char *errmsg)
{
    OSD_Printf(OSD_ERROR "event \"%s\" runtime error: %s\n",
               EventNames[g_eventIdx], errmsg);
}

int32_t El_CallEvent(L_State *estate, int32_t eventidx, int32_t spriteNum, int32_t playerNum, int32_t lDist, int32_t *iReturn)
{
    // XXX: estate must be the one where the events were registered...
    //      make a global?

    lua_State *const L = estate->L;
    int32_t i;

    const int32_t o_RETURN = g_RETURN;
    g_RETURN = *iReturn;

    g_elCallDepth++;
    i = call_regd_function3(L, &g_elEvents[eventidx], spriteNum, playerNum, lDist);
    g_elCallDepth--;

    *iReturn = g_RETURN;
    g_RETURN = o_RETURN;

    if (i != 0)
    {
        g_elEventError = 1;
        g_eventIdx = eventidx;
        return L_HandleError(L, i, &El_EventErrorPrint);
    }

    return 0;
}

static int32_t g_actorTile, g_iActor;
static void El_ActorErrorPrint(const char *errmsg)
{
    OSD_Printf(OSD_ERROR "actor %d (sprite %d) runtime error: %s\n",
               g_actorTile, g_iActor, errmsg);
}

int32_t El_CallActor(L_State *estate, int32_t actortile, int32_t spriteNum, int32_t playerNum, int32_t lDist)
{
    lua_State *const L = estate->L;
    int32_t i;

    g_elCallDepth++;
    i = call_regd_function3(L, &g_elActors[actortile], spriteNum, playerNum, lDist);
    g_elCallDepth--;

    if (i != 0)
    {
        g_actorTile = actortile;
        g_iActor = spriteNum;
        return L_HandleError(L, i, &El_ActorErrorPrint);
    }

    return 0;
}


code:
/* The Lunatic Interpreter, part of EDuke32. Game-side stuff. */

#ifndef EDUKE32_LUNATIC_H_
#define EDUKE32_LUNATIC_H_

#include "lunatic.h"
#include "events_defs.h"  // MAXEVENTS
#include "actors.h"  // con_move_t, con_action_t

#ifdef __cplusplus
extern "C" {
#endif

extern L_State g_ElState;

// actor initialization data
typedef struct
{
    con_move_t mov;
    int16_t movflags;
    int16_t strength;
    con_action_t act;
    uint8_t haveit;  // shouldn't be used directly
} el_actor_t;

extern uint8_t g_elEvents[MAXEVENTS];  // shouldn't be used directly
extern el_actor_t g_elActors[MAXTILES];

extern int32_t g_elEventError;

// -- functions --
void El_PrintTimes(void);

void El_ClearErrors(void);
void El_DisplayErrors(void);

int32_t El_CreateState(L_State *estate, const char *name);
void El_DestroyState(L_State *estate);

int32_t El_CallEvent(L_State *estate, int32_t eventidx, int32_t spriteNum, int32_t playerNum, int32_t lDist, int32_t *iReturn);
int32_t El_CallActor(L_State *estate, int32_t actortile, int32_t spriteNum, int32_t playerNum, int32_t lDist);

extern int8_t el_addNewErrors;  // add new errors to display?
void El_OnError(const char *str);

extern int32_t (*El_RestoreGamevars)(const char *savecode);

static inline int32_t El_HaveEvent(int32_t eventidx) { return g_elEvents[eventidx]!=0; }
static inline int32_t El_HaveActor(int32_t actortile) { return g_elActors[actortile].haveit!=0; }

typedef struct {
    uint32_t x, y, z, c;
} rng_jkiss_t;

uint32_t rand_jkiss_u32(rng_jkiss_t *s);
double rand_jkiss_dbl(rng_jkiss_t *s);

#ifdef __cplusplus
}
#endif

#endif


code:
local ffi = require("ffi")
local ffiC = ffi.C

local sector = sector
local inside = inside

local math = require("math")
local xmath = require("xmath")
local stat = require("stat")

local function resetseed()
    math.randomseed(834572183572)
end

local function getmapbounds()
    local inf = 1/0
    local min = { x=inf, y=inf }
    local max = { x=-inf, y=-inf }

    for i=0,ffiC.numsectors-1 do
        for w in wallsofsect(i) do
            local wal = wall[w]

            min.x = math.min(wal.x, min.x)
            max.x = math.max(wal.x, max.x)
            min.y = math.min(wal.y, min.y)
            max.y = math.max(wal.y, max.y)
        end
    end

    return min, max
end

local function getpoints(n, min, max)
    local posns, sects = {}, {}

    resetseed()
    for i=1,n do
        local x = math.random(min.x, max.x)
        local y= math.random(min.y, max.y)
        posns[i] = xmath.vec2(x, y)
        sects[i] = math.random(0, ffiC.numsectors-1)
    end

    return posns, sects
end

-- Compare 'inside' implementations
--
-- N: number of calls
function compinside(n)
    if (type(n) ~= "number") then
        error("N must be a number")
    end

    local sti = stat.new()
    local sts = stat.new()

    local min, max = getmapbounds()
    local posns, sects = getpoints(n, min, max)

    local isi, iss = {}, {}

    for i=1,n do
        local t = ffiC.gethiticks()
        isi[i] = inside(posns[i], sects[i])
        t = ffiC.gethiticks()-t
        sti:add(t)

        local t = ffiC.gethiticks()
        iss[i] = sector[sects[i]]:contains(posns[i])
        t = ffiC.gethiticks()-t
        sts:add(t)

--        if (isi[i]~=iss[i]) then
--            print("unequal: "..i.." "..sects[i].." "..posns[i].x.." "..posns[i].y.." ("..tostring(isi[i])..","..tostring(iss[i])..")")
--        end
        assert(isi[i]==iss[i])
    end

    print("====================")
    print("inside(): " .. sti:getstatstr())
    print("contains(): " .. sts:getstatstr())
end


code:
-- Random walker as updatesector() timing test, for Lunatic-m32
-- Load with: lua "require 'randwalk'"

local ffi = require "ffi"
local ffiC = ffi.C

local xmath = require "xmath"
local stat = require "stat"

-- set to nil to disable saving positions
g_posns = {}

local USF_BREADTH = sector.UPDATE_FLAGS.BREADTH

local function updatesectorbreadth(pos, sectnum)
    return updatesector(pos, sectnum, USF_BREADTH)
end

-- [STATS, TIMES] = RANDWALK(N, SPRITENUM, MINLEN, MAXLEN [, RANDOFS [, FUNCI [, LOGFN]]])
function randwalk(N, spritenum, minlen, maxlen, randofs, funci, logfn)
    -- Set the random seed to an arbitrary but fixed value so that the "random" walk
    -- is deterministically reproducible for particular arguments.
    math.randomseed(834572183572)

    randofs = randofs or 0

    for i=1,randofs do
        math.random()
    end

    local funcnames = { "updatesector", "updatesectorbreadth", "updatesectorz" }
    local funcs = { updatesector, updatesectorbreadth, updatesectorz }
    local updatesectorfunc = funcs[funci]

    -- stats
    local st = { all=stat.new(), succ=stat.new(), fail=stat.new() }

    local times = {}
    local successp = {}

    local pos = xmath.vec3(sprite[spritenum])
    local sectnum = sprite[spritenum].sectnum

    for i=1,N do
--[[
        -- get random integers for the displacement
        local ax,ay,az = math.random(minlen,maxlen), math.random(minlen,maxlen), math.random(minlen,maxlen)

        -- random signs
        local sx,sy,sz = math.random(0,1), math.random(0,1), math.random(0,1)
        sx, sy, sz = 2*sx-1, 2*sy-1, 2*sz-1  -- {0,1} -> {-1,1}
        ax, ay, az = sx*ax, sy*ay, sz*az
--]]
---[[
        local ang = 2*math.pi*math.random()
        local len = math.random(minlen, maxlen)
        local ax, ay, az = len*math.cos(ang), len*math.sin(ang), 0
--]]
        local newpos = pos + xmath.ivec3(ax,ay,az)

        local t = ffiC.gethiticks()
        local newsect = updatesectorfunc(newpos, sectnum)
        t = ffiC.gethiticks()-t

        st.all:add(t)
        times[i] = t

        successp[i] = (newsect >= 0)

        if (newsect >= 0) then
            st.succ:add(t)

            pos = newpos
            sectnum = newsect
        else
            st.fail:add(t)
        end

        if (g_posns ~= nil) then
            g_posns[i] = pos
        end
    end

    local stallres = st.all:getstats()

    print("====================")
    printf("Random %s walk starting with sprite %d",
           funcnames[funci], spritenum)
    printf("Hop length: [%d .. %d],  RNG offset: %d,  %s",
           minlen, maxlen, randofs, logfn)
    printf("-- Times [ms] (total=%f)", N*stallres.mean)
    print("All: ".. tostring(stallres))
    print("Succeeded: ".. st.succ:getstatstr())
    print("Failed: ".. st.fail:getstatstr())

    if (logfn ~= nil) then
        local logfile, errmsg = io.open(logfn, "w")
        if (logfile == nil) then
            printf("Couldn't open log file \"%s\" for writing: %s",
                   logfn, errmsg)
        else
            logfile:write(" \t \t  \t\t\n")
            for i=1,N do
                if (g_posns ~= nil) then
                    logfile:write(string.format("%s %d  %d %d %d\n",
                                                tostring(times[i]), successp[i] and 1 or 0,
                                                g_posns[i].x, g_posns[i].y, g_posns[i].z))
                else
                    logfile:write(string.format("%s %d\n", tostring(times[i]), successp[i] and 1 or 0))
                end
            end

            logfile:close()
        end
    end

    return st, ts
end


code:
#!/usr/bin/env luajit

local stat = require "stat"

if (#arg ~= 2) then
    print("Usage: profdemo.lua <eduke32> <demo_arg>")
    print("Example: ./profdemo.lua ../../eduke32 -d3:1,8")
    print("         (for 8 repetitions of demo 3 at 1 frame per gametic)")
    print("")
    os.exit(1)
end

local eduke32 = arg[1]
local demoarg = arg[2]

local numrepstr = demoarg:match(",[0-9]+$")
local numreps = tonumber(numrepstr and numrepstr:sub(2)) or 1
local st = { game=stat.new(), drawrooms=stat.new(), drawrest=stat.new() }
local stperx = { game=stat.new(), drawrooms=stat.new(), drawrest=stat.new() }

local unit, unitperx = {}, {}

for i=1,numreps do
    local fh = io.popen(eduke32.." "..demoarg)

    while (true) do
        local str = fh:read("*l")
        if (str == nil) then
            break
        end

        local NUMRE = "([0-9]+%.[0-9]+)"
        local UNITRE = "[mu]?s"

        local RE = "(== demo [0-9]+ ([a-z]+) times: "..NUMRE.." ("..UNITRE..") %("..NUMRE.." ("..UNITRE.."/[a-z]+)%))"
        local wholematch, whatstr, time1, unit1, time2, unit2 = str:match(RE)

        if (wholematch ~= nil) then
            if (numreps==1) then
                print(wholematch)
            else
                st[whatstr]:add(tonumber(time1))
                stperx[whatstr]:add(tonumber(time2))

                unit[whatstr] = unit1
                unitperx[whatstr] = unit2
            end
        end
    end
end

if (numreps > 1) then
    local keys = { "game", "drawrooms", "drawrest" }

    for i=1,#keys do
        local key = keys[i]
        if (unit[key] ~= nil) then
            print("== "..key.." times:")
            print("   "..st[key]:getstatstr().."  ["..unit[key].."]")
            print("   "..stperx[key]:getstatstr().."  ["..unitperx[key].."]")
        end
    end
end


code:
-- Pseudo random number generation module for Lunatic

local ffi = require("ffi")
local ffiC = ffi.C

local rawset = rawset

local type = type
local decl = decl  -- comes from above (_defs_game.lua)

local print = print  -- for commented out debug block in new() below

module(...)

-- PRNG state struct
ffi.cdef[[
typedef struct {
    uint32_t x, y, z, c;
} rng_jkiss_t;

typedef union { unsigned char u[16]; double d[2]; } uchar_double_u_t;
]]

-- PRNG functions
decl[[
uint32_t rand_jkiss_u32(rng_jkiss_t *s);
double rand_jkiss_dbl(rng_jkiss_t *s);

uint32_t Bcrc32(const void* data, size_t length, uint32_t crc);
]]

local function get_rand_u32(tin)
    tin.d[0] = ffiC.gethiticks() % 1
    tin.d[1] = ffiC.gethiticks() % 1
    return ffiC.Bcrc32(tin.u, 16, 0)
end

local mt = {
    __tostring = function(s)
        return "rand.new("..s.x..","..s.y..","..s.z..","..s.c..")"
    end,

    __index = {
        getu32 = ffiC.rand_jkiss_u32,
        getdbl = ffiC.rand_jkiss_dbl,

        -- Initialize the JKISS PRNG using the CRC32 of the result of several
        -- profiling timer calls interpreted as byte sequence.
        init_time_md4 = function(s)
            local tin = ffi.new("uchar_double_u_t")
            local tout = ffi.new("uint32_t [4]")

            repeat
                s.y = get_rand_u32(tin)
            until (s.y ~= 0)  -- y must not be zero!

            s.x = get_rand_u32(tin)
            s.z = get_rand_u32(tin)
            s.c = get_rand_u32(tin) % 698769068 + 1  -- Should be less than 698769069
        end,
    },
}
local jkiss = ffi.metatype("rng_jkiss_t", mt)

function new(x,y,z,c)
    local s
    if (x == nil or type(x)=="boolean") then
        -- initialize with arbitrary but fixed state
        s = jkiss(123456789, 987654321, 43219876, 6543217)
        if (x) then
            s:init_time_md4()
        end
    else
        s = jkiss(x,y,z,c)
    end
--[[
    local r=ffi.new("rng_jkiss_t")
    r.x = 123456789; r.y = 987654321; r.z = 43219876; r.c = 6543217;

    local N = 1e7
    local t=ffiC.gethiticks()
    for i=1,N do
        ffiC.rand_jkiss_dbl(r)
    end
    print("RANDGEN TIME: "..ffiC.gethiticks()-t)  -- x86_64: approx. 100 ms
--]]

    return s
end


code:
-- Statistics module for Lunatic.

local ffi = require("ffi")

local math = require("math")
local string = require("string")

local tostring = tostring

module(...)

ffi.cdef[[
typedef struct {
    double n;
    // vvv internal vvv
    double m, s;
    double min, max;
} runningstat_t;

typedef struct {
    const double n;
    const double mean, var, std;
    const double min, max;
} runningstat_res_t;
]]

local NaN = 0/0

local res_mt = {
    __tostring = function(s)
        return
            string.format("N=%d; mean=%.5g, std=%.5g; min=%.5g, max=%.5g",
                         s.n, s.mean, s.std, s.min, s.max)
    end
}
local rstatres = ffi.metatype("runningstat_res_t", res_mt)

local mt = {
    __tostring = function(s)
        -- XXX: with this and the other serializing of ffi types, take care of
        -- NaN and Infs when reading back (e.g. by "nan=0/0" in that context)
        return "stat.new("..s.n..","..s.m..","..s.s..","..s.min..","..s.max..")"
    end,

    -- See: Accurately computing running variance, by John D. Cook
    -- http://www.johndcook.com/standard_deviation.html
    __index = {
        add = function(s, num)
            if (s.n > 0) then
                -- N>0, recurrence
                s.n = s.n+1

                local lastm = s.m
                s.m = lastm + (num-lastm)/s.n
                s.s = s.s + (num-lastm)*(num-s.m)

                if (num < s.min) then s.min = num end
                if (num > s.max) then s.max = num end
            else
                -- N==0, initialization
                s.n = 1

                s.m = num
                s.s = 0

                s.min = num
                s.max = num
            end
        end,

        getstats = function(s)
            local var = s.n > 1 and s.s/(s.n-1) or NaN
            return rstatres(s.n, s.m, var, math.sqrt(var), s.min, s.max)
        end,

        getstatstr = function(s)
            return tostring(s:getstats())
        end,

        reset = function(s)
            s.n = 0
            s.m, s.s, s.min, s.max = NaN, NaN, NaN, NaN
        end,
    },
}
local rstat = ffi.metatype("runningstat_t", mt)

function new(n,m,s, min,max)
    if (n == nil) then
        -- initialization containing no elements
        return rstat(0, NaN, NaN, NaN, NaN)
    elseif (m == nil) then
        -- same as initialization with N==0 above (one element)
        return rstat(1, n, 0, n, n)
    else
        -- generic initialization (internal use only)
        return rstat(n,m,s, min,max)
    end
end


code:
--
-- strict.lua
-- checks uses of undeclared global variables
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
--

local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget

local mt = getmetatable(_G)
if mt == nil then
  mt = {}
  setmetatable(_G, mt)
end

mt.__declared = {}

local function what ()
  local d = getinfo(3, "S")
  return d and d.what or "C"
end

mt.__newindex = function (t, n, v)
  if not mt.__declared[n] then
    local w = what()
    if w ~= "main" and w ~= "C" then
      error("assign to undeclared variable '"..n.."'", 2)
    end
    mt.__declared[n] = true
  end
  rawset(t, n, v)
end

-- PK: change from original (undeclared-refs in main allowed):
mt.__index = function (t, n)
  if not mt.__declared[n] then
      local w = what()
      if w ~= "main" and w ~= "C" then
          error("variable '"..n.."' is not declared", 2)
      end
  end
  return rawget(t, n)
end


code:
-- test script for ELua/Lunatic Interpreter

-- error=nil -- must not affect "require"
local require = require
local string = require("string")
local bit = require("bit")
local math = require("math")

local pcall = pcall
local DBG_ = require("_LUNATIC_DBG")

local gv, sector, wall, sprite, spriteext = gv, sector, wall, sprite, spriteext
local actor, player, projectile = actor, player, projectile
local gameevent, gameactor = gameevent, gameactor

local spritesofsect = spritesofsect
local hitscan = hitscan

local assert, error, print, tostring = assert, error, print, tostring

local D = require("CON.DEFS")

print('---=== ELua Test script ===---')

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

local function checkfail(funcstr, expectedmsg)
    if (DBG_ == nil) then
        return
    end

    local status, errmsg = pcall(DBG_.loadstring(funcstr))
    if (status) then
        print('^21ERROR:^O '..funcstr.." DIDN'T fail")
    else
        if (expectedmsg==nil or string.find(errmsg, expectedmsg, 1, true)) then
            print("^11SUCCESS:^O "..funcstr.." failed: "..errmsg)
        else
            -- XXX: beginning with "^10" is counted as error in OSD_Printf()
            print("^10ERROR*:^O "..funcstr.." failed: "..errmsg..
                  ", but expected error message was: "..expectedmsg)
        end
    end
end

gameevent
{
    "ENTERLEVEL",

    function()
        local vol, lev = gv.currentEpisode(), gv.currentLevel()
        printf('volume=%d, level=%d', vol, lev)

        if (vol ~= 4) then
            -- Tweak some sector pals.
            print('tweaking sector pals')
            print('numsectors: ' .. gv.numsectors .. ' of ' .. gv.MAXSECTORS)

            local SF = sector.STAT
            for i = 0, gv.numsectors/2 do
                local sec = sector[i]
                sec.floorpal = 1;
                sector[i].floor.shade = sec.floor.shade + 4
                sector[i].ceilingpal = 2;
                local ceil = sec.ceiling
                ceil.shade = sector[i].ceiling.shade + 8
                ceil.statbits:flip(SF.SMOOSH)
                sec.floorstatbits:flip(SF.SWAPXY)
            end
        end

        if (vol==1 and lev==1) then  -- E1L1
            print('tweaking some sprites 2')
            local i = 562
            spriteext[i].alpha = 0.5;
            sprite[i].cstat = bit.bor(sprite[i].cstat, 2+512);
            spriteext[i].pitch = 128;
            spriteext[i].roll = 256;

            i = 107  -- pistol ammo at rooftop
            spriteext[i].pitch = 128;
            spriteext[i].roll = 256;

            for spr in spritesofsect(307) do  -- some fence sprites in E1L1
                printf('spr %d', spr)
                sprite[spr].pal = 6
            end

            actor[562].flags = bit.bor(actor[562].flags, 2);   -- pal 6 with goggles on front SEENINE
        end
---[[
        if (vol==1 and lev==8) then
            local havebunch = false
            for i=0,gv.numsectors-1 do
                havebunch = havebunch or (sector[i].ceilingbunch >= 0)
            end
            if (havebunch) then
                print('tweaking bunch 1');
                -- trueror1.map
                for i, what in sectorsofbunch(1, gv.BOTH_CF) do
                    sector[i][what].z = sector[i][what].z - 3*1024;
                end
            end
        end
--]]
    end
}

gameevent
{
    "JUMP",

    function()
        print("tweaking forcefield with lotag 34 (E2L1)")

        for w=0,gv.numwalls-1 do
            local wal = wall[w]
            if (wal.overpicnum == D.W_FORCEFIELD or wal.overpicnum == D.W_FORCEFIELD+1) then
                if (wal.lotag==34) then
                    wal.cstat = wal.cstatbits:test(85) and 0 or 85
                end
            end
        end
    end
}

local unsafe = pcall(function() string.UNSAFE=true; end)

checkfail("tostring = nil", "attempt to write into the global environment")
--DBG_.printkv('_G in test.elua', _G)

-- direct gv array access forbidden
checkfail('gv.sprite[0].yrepeat = 100', "access forbidden")

checkfail('print(sprite[100000].ceilingpal)', "out-of-bounds sprite[] read access")

checkfail('print(gv.sprite[0])', "access forbidden")

-- set metatable forbidden
checkfail('setmetatable(sprite, {})', "attempt to read undeclared variable 'setmetatable'")

gameevent
{
    "ENTERLEVEL",

    function()
        -- OOB write access.
        -- Note that indexing ("reading") sector fails, even if the user wants to
        -- assign to a sector member. Potentially confusing error message.
        checkfail('sector[-1].ceilingpal = 4', "out-of-bounds sector[] read access")

        -- wallnum member is read-only
        checkfail('sector[0].wallnum = 0', "attempt to write to constant location")  -- this comes from LJ/FFI

        -- direct sector write access forbidden
        checkfail('sector[4] = sector[6]', "cannot write directly to sector[]")

        -- creating new keys forbidden... handled by LuaJIT
        checkfail('wall[4].QWE = 123', "has no member named 'QWE'")

        -- no pointer arithmetic!
        checkfail('local spr = sprite[0]; local x=spr+1', "attempt to perform arithmetic on")

        -- actor[].t_data[] is not accessible for now
        checkfail('local i = actor[0].t_data[15]', "has no member named 't_data'")

        -- sprite.picnum may happen as a thinko/typo kind of error (spr.picnum was meant)
        checkfail("local pic = sprite.picnum", "invalid access to static data")

        checkfail("require('engine').setshadetab(200, nil)",
                  "setshadetab() may be run only while LUNATIC_FIRST_TIME is true")

        checkfail("sprite[0]:set_picnum(-10)", "invalid tile number")
    end
}

-- gv.numsectors is read-only
checkfail('gv.numsectors = 4', "attempt to write to constant location")

-- cannot create new fields in 'gv'
checkfail('gv.QWE = 4', "write access forbidden")

-- that would be horrible...
checkfail('sprite._nextspritesect[4] = -666', "cannot write directly to nextspritesect[]")

-- we're indexing a plain array!
checkfail('print(sprite._nextspritesect[4].whatfield)', "attempt to index a number value")

-- our 'require' has only safe stuff
--checkfail("require('os')")

-- gamevars are created using a special different mechanism
checkfail("new_global = 345", "attempt to write into the global environment")
-- Can't reassign to existing vars either
assert(actor ~= nil)
checkfail("actor = 345", "attempt to write into the global environment")

-- can't redefine constants in 'gv'
checkfail('gv.CEILING = 3', "attempt to write to constant location")

-- string.dump is unavailable
checkfail('local s=require[[string]]; local tmp=s.dump(gameevent)',
          "attempt to call field 'dump' (a nil value)")

if (not unsafe) then
    -- changing base module tables is disallowed
    checkfail('local s=require[[string]]; s.format=nil', "modifying base module table forbidden")
else
    print('WARNING: RUNNING WITH UNPROTECTED BASE MODULES')
end

print('')
-- This is problematic, even though pretty much every access will yield a
-- "missing declaration" error.
-- See http://luajit.org/ext_ffi_api.html#ffi_C about what stuff ffi.C contains.
checkfail('gv.luaJIT_setmode(nil, 0, 0)', "missing declaration for symbol 'luaJIT_setmode'")

checkfail('gv.luaJIT_BC_con_lang', "attempt to call a nil value")
checkfail('gv.gethiticks = nil', "attempt to write to constant location")

checkfail('gameactor{1680, 0}', "must provide a function with last numeric arg or .func")

checkfail("do local bt=require'test.test_bitar'; bt.QWE=1; end", "modifying module table forbidden")
-- the cdata returned by player[] can't be made into a pointer!
checkfail("do local pl=player[0]; i=pl[1]; end")
checkfail("do local ud=gv.ud.camerasprite; end", "access forbidden")  -- test for proper decl()
checkfail("gv.g_sizes_of=nil; print(gv.g_sizes_of[0])", "write access forbidden")
checkfail("gv.cam.sect=-1", "invalid sector number")
checkfail("local flag=gv.SFLAG_NULL", "missing declaration")

-- NOTE: player[0] is forbidden at file scope, this is just for testing purposes.
player[0].wackedbyactor = -1  -- should succeed
checkfail("player[0].curr_weapon = -1", "Invalid weapon ID")
player[0].curr_weapon = 1
checkfail("local w = player[0].weapon[-1]", "out-of-bounds weapon read access")

-- XXX: This gives a very strange error message: "attempt to write to constant location". Why?
-- (note how I forgot to index weapon with a weapon index or name)
--player[0].weapon.firesound = 1e5
checkfail("player[0].weapon.SHOTGUN.firesound = 1e5", "invalid sound number")
checkfail("player[0].weapon.SHOTGUN.firesound = 0/0", "must be a non-NaN number")
checkfail("player[0].weapon.SHOTGUN.firesound = 1/0", "invalid sound number")
checkfail("gameactor{1680, action=require('con').action{numframes=-10}, function() end}",
          "action has negative number of frames")
-- NOTE: It should only be relied on that setting e.g. .firesound to -1 sets it
-- to 0, not other negative values.
player[0].weapon.SHOTGUN.firesound = -1/0
assert(player[0].weapon.SHOTGUN.firesound == 0)

gameevent{gv.EVENT_JUMP,
          function(actori, playeri, dist)
              printf("jump i=%d p=%d d=%d", actori, playeri, dist)
              error("greetings from EVENT_JUMP")
          end
         }

--[[
gameevent
{
    "PROCESSINPUT",

    -- Input test.
    -- NOTE: I don't think that exposing g_player[].sync (aka "input") is a good idea...
    func = function(actori, playeri, dist)
        local IB = player._INPUT_BITS
        local input = player[playeri]._input
        if (bit.band(input.bits, IB.JUMP) ~= 0) then
            print("JUMPED")
            -- ... because for example this doesn't work
            -- (P_HandleSharedKeys, where the JETPACK bit is tested, runs
            -- before P_ProcessInput):
            input.bits = bit.bor(input.bits, IB.JETPACK)
        end
    end
}
--]]

local WEAPON = gv.WEAPON

-- Bad hack to test out pitch/roll: EVENT_GAME is discouraged from Lunatic.
local PITCH_PICNUM = { [D.SEENINE]=true, }
gameevent{ "GAME",
    function(aci)
        local spr = sprite[aci]
        if (PITCH_PICNUM[spr.picnum]) then
            local height = spr:getheightofs()

            local sexy = spriteext[aci]
            sexy.pitch = gv.totalclock
            sexy.mdoff.x = -height/16  -- test xoff + pitch
            sexy.mdoff.z = -height  -- test zoff + pitch

            -- Test roll + yoff
            sexy.roll = gv.totalclock
            sexy.mdoff.y = -height/16
        end
    end
}

-- test event chaining
gameevent
{
    "JUMP",

    flags = actor.FLAGS.chain_beg,

    function(actori, playeri, dist)
        local ps = player[playeri]
        print("\n--- I'm first!")
--        DBG_.oom()
        local pistol = ps.weapon.PISTOL
        if (pistol.shoots ~= D.RPG) then
            pistol.shoots = D.RPG
        else
            pistol.shoots = D.SHOTSPARK1
        end
        ps.weapon[WEAPON.PISTOL].firesound = D.LIGHTNING_SLAP

        -- This succeeds, because sound2time is a time, not a sound.
        ps.weapon.SHOTGUN.sound2time = 5000
        printf("set shotgun's sound2time to %d", ps.weapon.SHOTGUN.sound2time)

        -- Set pipebomb and tripbomb to timed mode.
        -- XXX: Provide either named constants or methods?
        -- XXX: These are probably reset to default on new game.
        ps.pipebombControl = 2
        ps.tripbombControl = 2

        -- Test of INTERNAL member _pals.
        -- NOTE: setting colors partially is bad! E.g. after an item is
        -- picked up, col[0] and col[1] remain and tint everything greenish.
        if (DBG_ ~= nil) then
            ps._pals[2] = 20
            ps._pals.f = 30
        end
    end
}

local xmath = require "xmath"

gameevent
{
    gv.EVENT_ENTERLEVEL,

    function()
        if (DBG_ ~= nil) then
            DBG_.testmembread()
        end

        -- NOTE: times are for helixhorned (Core2Duo 3GHz)
        local i
        local N = 1e6
        local t = gv.gethiticks()

        for i=3,N do
            gv.gethiticks()
        end

        t = gv.gethiticks()-t

        -- x86_64: 35ns/call, x86: 280 ns/call
        -- Windows 32-bit: about 1 us/call?
        printf("%d gethiticks() calls took %.03f ms (%.03f us/call)",
               N, t, (t*1000)/N)

        local sum=0
        t = gv.gethiticks()
        for i=1,N do sum = sum+gv.ksqrt(i) end
        t = gv.gethiticks()-t
        -- x86_64: 14ns/call
        printf("%d ksqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
               N, t, (t*1000)/N, sum)

        sum=0
        t = gv.gethiticks()
        for i=1,N do sum = sum+math.sqrt(i) end
        t = gv.gethiticks()-t
        -- x86_64: 7ns/call
        printf("%d math.sqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
               N, t, (t*1000)/N, sum)

        printf("sqrt(0xffffffff) = %f(ksqrt) %f(math.sqrt)",
               gv.ksqrt(0xffffffff), math.sqrt(0xffffffff))

        local pl = player[0]
        -- MAX < current is "allowed"
        pl.max_ammo_amount[WEAPON.RPG] = 17
        pl:give_weapon(WEAPON.RPG)
        pl.ammo_amount[WEAPON.RPG] = 54

        pl:give_weapon(WEAPON.SHRINKER)
        -- This looks much prettier:
        pl.ammo_amount.SHRINKER = 2

        -- MORTER2 from test/weaponvars.con
        local PNUM = 1653
        local proj = projectile[PNUM]
        if (proj ~= nil) then
            printf('Have projectile %d', PNUM)
            player[0].weapon.SHOTGUN.shoots = PNUM
            proj.drop = 0
            proj:set_trail(D.SMALLSMOKE)
        else
            printf('^10Do NOT have projectile %d, test/weaponvars.con not loaded?', PNUM)
        end

        if (gv._LUNATIC_STRICT == 0) then
            t = gv.gethiticks()
            local N=1
            for n=1,N do
                for i=0,gv.MAXSPRITES-1 do
                    sprite[i].blend = 1
                end
                for i=gv.MAXSPRITES-1,0,-1 do
                    sprite[i].shade = 1
                end
                for i=0,gv.MAXSPRITES-1 do
                    sprite[i].xoffset = 0
                end
                for i=gv.MAXSPRITES-1,0,-1 do
                    sprite[i].yoffset = 1
                end
            end
            t = gv.gethiticks()-t
            printf("%d x four 0..MAXSPRITES-1 iterations took %.03f us per outer iteration", N, (1000*t)/N)
            -- Results on x86:
            -- N=1: 480-1000 us (too large variance)
            -- N=10: 190-210 us * 10 = 1.9-2.1 ms
            -- N=100: about 160 us * 100 = about 16 ms
        end

        -- Make the DUKECAR in E1L1 into a zombie actor (temporarily)
        -- NOTE: Use static value (not the one from 'D').
        if (sprite[24].picnum==2491) then
            sprite.changestat(24, actor.STAT.ZOMBIEACTOR)
        end

        checkfail("gameevent('GAME', function() print('qwe') end)",
                  "must be called from top level")

        -- Test vec3 + wall. Pseudo wall member 'z' will be accessed.
        local mpos = xmath.vec3()
        for i=0,gv.numwalls-1 do
            mpos = mpos + wall[i]
        end
        mpos = mpos/gv.numwalls
        local impos = xmath.ivec3(mpos)^20  -- test ivec3 with dvec3 arg, test '^' op
        assert(impos.z == -20)
        printf("Map center point: (%d,%f)", mpos.x, impos.y)
    end
}

gameevent{"LOADACTOR", function(i)
    local spr = sprite[i]
    if (i==614 and spr.picnum==930) then
        -- "police line" ribbon in E1L1
        -- clear the hitag so that it doesn't spawn as FALLER from premap
        -- Rather a HACK: relies on an implementation detail (A_Spawn()
        -- "hard-wired" code).
        spr.hitag = 0
    end
end}

gameactor
{
    -- "police line" ribbon
    930, nil, 1,

    func = function(i)
        local spr = sprite[i]
        local r = math.random
        local d = 20
        -- NOTE: __add metamethod is called because of the RHS:
        local v = spr + xmath.vec3(r(-d,d), r(-d,d))
        spr:setpos(v):updatesect()

        -- Test vec3 constructor with cdata.
        local tempvec = xmath.vec3(player[0].pos)
    end
}

local stat = require("stat")
local hs = stat.new()

local con = require("con")
local AC, MV = con.AC, con.MV

local CAC, CMV, CAI = require("CON.ACTION"), require("CON.MOVE"), require("CON.AI")
assert(CAC); assert(CMV); assert(CAI)

local AC, MV = {}, {}

AC.TROOPSTAND = assert(CAC.ATROOPSTAND) -- or con.action{0,1,5,1,1}
AC.TROOPFLINTCH = con.action{50, 1, 1, 1, 6}
MV.SHRUNKVELS = con.move{hvel=32}
con.ai(AC.TROOPFLINTCH, MV.SHRUNKVELS, 0)  -- unused; TODO: test

local TROOPSTRENGTH = 30

local AF = actor.FLAGS
local CS = sprite.CSTAT

-- Crosshair sprite.
-- NOTE: This ought to be a gamevar -- if a savegame is restored, a new one
-- will be spawned.
local chair

gameactor{ D.APLAYER, AF.chain_end,
    function(aci, pli)
        if (chair == nil) then
            chair = con.spawn(555, aci)
            printf("Spawned our crosshair: sprite %d", chair)
            local spr = sprite[chair]
            -- Set to STAT_MISC because otherwise interpolation goes crazy (old
            -- value never updated; dunno why...)
            sprite.changestat(chair, actor.STAT.MISC)
            spr.xrepeat, spr.yrepeat = 96, 96
            spr.cstatbits:set(CS.CENTER)
        end

        local ps = player[pli]
        local ray = xmath.kangvec(ps.ang, -(ps.horiz-100)*2048)

        local hit = hitscan(ps.pos, ps.cursectnum, ray, 0)
        if (hit.sector >= 0) then
            sprite[chair]:setpos(hit.pos)
            sprite.changesect(chair, hit.sector)
        end
    end
}

-- Add NODAMAGEPUSH flag to NEWBEAST.
gameactor { D.NEWBEAST, AF.chain_end + AF.NODAMAGEPUSH, function() end }

-- Also test actor code chaining: strength is doubled.
gameactor
{
    D.LIZTROOP, AF.chain_end+AF.enemy, 2*TROOPSTRENGTH,

    action = AC.TROOPSTAND,

    func = function(i, playeri, dist)
        sprite[i].pal = math.random(32)
--        sprite[i].ang = bit.band(sprite[i].ang-20, 2047)

        local spr = sprite[i]

        local t = gv.gethiticks()
        local hit = hitscan(spr, spr.sectnum, {x=10, y=10, z=0}, gv.CLIPMASK0)

        hs:add(1000*(gv.gethiticks()-t))

        if (hs.n == 300) then
            printf("hitscan: %s", hs:getstatstr())
            hs:reset()
            error("greetings from LIZTROOP actor")
        end

        local actr = actor[i]
        if (actr:get_count() % 30 == 0) then
            spr.cstatbits:flip(CS.YFLIP)
        end

        -- Test of bitint's ":test()" for actor[].flags.
        actr.flagsbits:test(AF.NVG)

        if (dist < 4096) then
            -- Duke Vader / Anakin Nukewalker?
            actor[i]:set_action(AC.TROOPFLINTCH)
            actor[i]:set_move(MV.SHRUNKVELS)

            if (dist < 1024) then
                con.killit()
            end
        end

        if (actr:has_action(CAC.ATROOPWALKING)) then
            if (actr:get_count() % 50 == 0) then
                actr.movflagsbits:flip(actor.MOVFLAGS.spin)
            end
        end
    end,

    -- NOTE: the animate callback is not yet documented and thus not official API!
    animate = function(tspr)
        local tspr2 = tspr:dup()
        if (tspr2) then
            tspr2.x = tspr2.x + 512*math.cos(gv.totalclock/60)
            tspr2.y = tspr2.y + 512*math.sin(gv.totalclock/60)
            tspr2.cstatbits:set(CS.TRANS_BITMASK)
        end

        -- XXX: inserted tsprites have floor shadow in classic! (r_shadow)
        -- G_DoSpriteAnimations() is passed as callback to the engine on occasion,
        -- in other words, created tsprites may be fed back to G_DoSpriteAnimations()!
        -- classic: shows shadow for both "ghost" liztroop and aim "reticle"
        -- Polymost: only for "ghost"
        -- Polymer: none
        local aimv = 256*xmath.bangvec(tspr.ang)
        local hit = hitscan(tspr^(16*256), tspr.sectnum, aimv, gv.CLIPMASK1)

        if (hit.wall >= 0) then
            local aimtspr = tspr:dup()
            if (aimtspr) then
                aimtspr.pal = 2
                aimtspr:set_picnum(555)
                aimtspr:setpos(hit.pos, hit.sector)
            end
        end
    end,
}

gameactor
{
    1275,  -- newspaper, E4L6 sprite #513

    action = con.action{0, 4, delay=20},  -- Same as {0, 4, 1, 1, 20}
    move = 1,  -- so that the ID is nonzero and it will move

    func = function(aci)
        local a = actor[aci]
        local delay = math.sin(0.1 * 2*math.pi*gv.totalclock/120)
        a:set_action_delay(20 + 10*delay)
        if (sprite[aci].pal ~= 0) then
            a:set_hvel(1024/30)
            a:set_vvel(-1024/30)
        else
            a:set_hvel(50*delay)
        end
        a.movflags = actor.MOVFLAGS.geth + actor.MOVFLAGS.getv
    end,
}

gameevent
{
    "DISPLAYROOMS",

    function(aci, pli)
        local ps = player[pli]
        local cam = gv.cam

        if (ps.newowner < 0) then
            cam.pos.z = cam.pos.z + 64*16*math.sin(gv.totalclock/30)
        end

        if (ps.cursectnum >= 0) then
            local hit = sector[ps.cursectnum]:zrangeat(cam.pos)
            if (gv.totalclock%200==0) then
                printf("hit %s %d at z=%d, %s %d at z=%d",
                       hit.c.spritep and "sprite" or "sector", hit.c.num, hit.c.z,
                       hit.f.spritep and "sprite" or "sector", hit.f.num, hit.f.z)
            end
        end
    end
}

gameevent
{
    "DISPLAYREST",

    function()
        for i=0,10 do
            for j=1,100 do
                -- XXX: This is slower in the Polymodes than with classic!
--                con._gametext(2822, j, i, 12, 0, 0, 16, 0,0,gv.xdim,gv.ydim,8192)
            end
        end

        -- Clear showing every sector with the pavement floor tile. (Unless we're
        -- in such a sector or an adjoining one.)
        -- XXX: We need a better place to do this, maybe an event in
        -- G_DisplayRest() where show2dsector[] is tweaked?
        -- NOT YET OFFICIAL API.
        local show2dsector = sector.showbitmap
        for i=0,gv.numsectors-1 do
            if (sector[i].floorpicnum==815) then
                show2dsector:set0(i)
            end
        end
    end
}

gameactor
{
    D.APLAYER, actor.FLAGS.chain_beg,

    function(aci, pli)
        if (con._squished(aci, pli)) then
            printf("Squished LunaCON test")
        end
    end
}

gameactor
{
    -- Innocent sign, similar to test/weaponvars.con actor 909 (tree trunk)
    1211, actor.FLAGS.replace,

    function(aci, pli)
        local a = actor[aci]

        if (a:get_count() >= 120) then
            local i = con.spawn(D.TRANSPORTERSTAR, aci)
            sprite[i].z = sprite[i].z - 1024*16
            con.shoot(D.MORTER, aci, -4096)
            a:set_count(0)
        end
    end
}

local function testbit(num, b)
    return bit.band(num,b)~=0 and 1 or 0
end

local FADE_SPEED = {
    [WEAPON.KNEE] = 1/2.5,

    1/128,
    1/5,
    1/3,
    1/2,
    1,  -- pipebomb: ~1 sec
    2,
    3,
    5,
    127,  -- freezer; such a fast fade is not visible, but it clears any
          -- existing one (if of higher priority)
    [WEAPON.GROW] = 9.9,  -- test banker's rounding -- should be like 10
}

-- Test player[]:fadecol(), a better palfrom.
gameevent
{
    "CHANGEWEAPON",

    function (aci, pli)
        local ps = player[pli]
        local neww, curw = gv.RETURN, ps.curr_weapon

        local r, g, b = testbit(neww, 1), testbit(neww, 2), testbit(neww, 4)
        local speed = FADE_SPEED[neww] or 1
        player[pli]:fadecol(0.5, r, g, b, speed, neww-5)
    end
}

-- Time the above p:fadecol() test for verification of the <speed> argument.
local last_f, last_t = 0, 0
local last_secs = nil
gameevent
{
    "DISPLAYREST",

    function(aci, pli)
        local ps = player[pli]
        -- WARNING: _pals in INTERNAL and off-limits to users!
        local curf = ps._pals.f
        if (curf > last_f) then
            -- Starting a tint
            last_secs = nil
            last_f = curf
            last_t = gv.getticks()
        elseif (last_t > 0 and curf==0) then
            -- Fade has ended
            last_secs = (gv.getticks()-last_t)/1000
            last_f, last_t = 0, 0
        end

        if (last_secs ~= nil) then
            con.minitext(10, 10, string.format("Last fade time: %.03f s (%.03f gametics)",
                                               last_secs, last_secs*30))
        end
    end,
}

printf("EVENT_INIT = %d", gv.EVENT_INIT)  -- tests default defines

local bittest = require "test.test_bitar"
bittest.sieve()

require("test.test_geom", 123123)
require("test.test_rotspr")

do
    -- Test ksin vs. sinb
    local xmath = require "xmath"
    local sum = 0

    local N = 1000
    local t = gv.gethiticks()
    for i=0,N*2048-1 do
        sum = sum+xmath.ksin(i)
    end
    t = gv.gethiticks()-t
    sum = sum*1e12
    printf("ksin: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)

    sum = 0
    local t = gv.gethiticks()
    for i=0,N*2048-1 do
        sum = sum+xmath.sinb(i)
    end
    t = gv.gethiticks()-t
    sum = sum*1e12
    printf("sinb: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)

    -- Results (helixhorned x86):
    -- ksin: sum*1e12=0.000, 3.801us per 0-2047 cycle
    -- sinb: sum*1e12=0.052, 2.886us per 0-2047 cycle
end

do
    -- Test getflorzofslopeptr()/sector[]:floorzat()
    local N = 100
    local t = gv.gethiticks()

    for n=1,N do
        for i=0,gv.numsectors-1 do
            local sec = sector[i]
            local w1 = sec.wallptr
            sec:floorzat(wall[w1])
        end
    end

    printf("%d x %d floorzat: %.03f us", N, gv.numsectors, (gv.gethiticks()-t)*1000)

    -- Results for 100 x 325 floorzat (helixhorned x86):
    -- cdecl getflorzofslope(): 572.165 us
    -- __fastcall getflorzofslope(): 830.147 us (sic, but tested only once!)
end

print('---=== END TEST SCRIPT ===---')

--[[
function check_sector_idx()
    error("bla")
end
spritesofsect(0)
--]]

--DBG_.oom()

-- This will complain about wrong usage of 'error'. In particular,
-- the nil must not propagate to C!
checkfail('error(nil)', "error using 'error': error message must be a string")

local D = require("CON.DEFS")
checkfail('require("CON.DEFS").APLAYER=123', "modifying base module table forbidden")
-- Test with lunatic/test/rotfixed_actor.con.
print("DUKECAR="..tostring(D.DUKECAR))

do
    print('---------- getangle test')

    local function CreateGetAngFunc(roundfunc)
        return function(x, y)
            local ang = (1024/math.pi)*math.atan2(y, x)  -- note the swapped args
            return bit.band(roundfunc(ang), 2047)
        end
    end

    local ourgetang = CreateGetAngFunc(math.ceil)
    local ourgetangf = CreateGetAngFunc(math.floor)

    local function printang(x, y)
        printf('%4d,%4d: %13d, %16d, %16d', x, y,
               gv.getangle(x, y), ourgetang(x, y), ourgetangf(x, y))
    end

    print "   x,   y: getangle(x, y) | math.atan2/ceil | math.atan2/floor"
    printang(10, 100)
    printang(10, -100)
    printang(-10, -100)
    printang(-10, 100)

    printang(0, 0)

    printang(1, 0)
    printang(0, 1)
    printang(-1, 0)
    printang(0, -1)

    local N=1e5
    local r = 0

    local t = gv.gethiticks()
    for i=1,N do
        r = r + gv.getangle(10, i)
    end
    local t1 = gv.gethiticks()-t

    r = 0
    t = gv.gethiticks()
    for i=1,N do
        r = r + ourgetang(10, i)
    end
    local t2 = gv.gethiticks()-t

    printf('Time for %d runs: getangle: %.03f ms, math.atan2: %.03f ms', N, t1, t2)
    print('----------')
end

if (LUNATIC_FIRST_TIME) then
    require("test.shadexfog").test_create0()
end


code:
gamevar tmp 0 0
/*
actor LIZTROOP
    getactor[THISACTOR].mdflags tmp
    orvar tmp 16
    setactor[THISACTOR].mdflags tmp
enda
*/

gamevar ceilz 0 0
gamevar ceilhit 0 0
gamevar florz 0 0
gamevar florhit 0 0
gamevar pi 0 0

gamevar doxp 0 0
gamevar xp 0 0

define Q 500

gamevar pdist 0 2

onevent EVENT_GAME
    ifactor LIZTROOP
        findplayer pdist

    // Test both view/wall/floor sprites for
    // spriteext[].xpanning (Polymost only)
    setvar doxp 0
    ifactor 937  // --- vvv --- E2L1
        setvar doxp 1
    else ifactor FEMMAG1
        setvar doxp 1
    else ifactor PIGCOP
        setvar doxp 1
    else ifactor 849  // 64x24: in classic, better than nonpow2-x tile 937
        setvar doxp 1

    ifvarn doxp 0
    {
        getactor[THISACTOR].xpanning xp
        addvar xp 1
        setactor[THISACTOR].xpanning xp
    }

    getwall[0].blend xp  // test wall[].blend
endevent

onevent EVENT_ANIMATESPRITES
    ifactor LIZTROOP
    {
        setvarvar tmp totalclock
        andvar tmp 255
        settspr[THISACTOR].tsprshade tmp
    }
endevent

gamevar lizi 0 0

onevent EVENT_DISPLAYREST
    findnearactor LIZTROOP 4096 lizi
    ifvarn lizi -1
    {
        redefinequote Q nearest LIZTROOP is %d units away (Manhattan dist.)
        qsprintf Q Q /**/ actorvar[lizi].pdist
        gametextz STARTALPHANUM 10 100 Q 2 0 16 /*bounds:*/ 0 0 xdim ydim 32768
    }

    getplayer[THISACTOR].i pi
    // This blows up on C-CON:
    getzrange sprite[pi].x sprite[pi].y sprite[pi].z player[THISACTOR].cursectnum
     /*out:*/ ceilz ceilhit florz florhit
      /*in:*/ 128 CLIPMASK0

    redefinequote Q hit %d at %d, %d at %d
    qsprintf Q Q /**/ ceilhit ceilz florhit florz
    minitext 100 8 Q 0 0
    gametext STARTALPHANUM 320 16 Q 0 0 16 /*bounds:*/ 0 0 xdim ydim
    gametextz STARTALPHANUM 320 24 Q 0 0 16 /*bounds:*/ 0 0 xdim ydim 32768
endevent

// Following code by Fox, from
// http://forums.duke4.net/topic/955-eduke32-scripting/page__view__findpost__p__153176

gamevar SIN 0 0
gamevar COS 0 0
gamevar HORIZ 0 0
gamevar HITSECT 0 0
gamevar HITWALL 0 0
gamevar HITSPRITE 0 0
gamevar HITX 0 0
gamevar HITY 0 0
gamevar HITZ 0 0

onevent EVENT_DISPLAYREST
    sin SIN player[THISACTOR].ang
    cos COS player[THISACTOR].ang
    setvar HORIZ 100
    subvarvar HORIZ player[THISACTOR].horiz
    subvarvar HORIZ player[THISACTOR].horizoff
    mulvar HORIZ 32
    hitscan player[THISACTOR].posx player[THISACTOR].posy player[THISACTOR].posz player[THISACTOR].cursectnum
            COS SIN HORIZ HITSECT HITWALL HITSPRITE HITX HITY HITZ CLIPMASK1

    redefinequote Q hitscan: hit sector %d, wall %d, sprite %d
    qsprintf Q Q /**/ HITSECT HITWALL HITSPRITE
    minitext 100 80 Q 0 0
endevent


code:
// Inputs to "state checknearwall".
gamevar wd 128 0  // wall distance
gamevar cfd 1024 0  // ceiling/floor distance

// Outputs of "state checknearwall".
gamevar hitwall -1 0

// Temporary variables.
gamevar tmp 0 0
gamevar ret 0 0
gamevar x 0 0
gamevar y 0 0
gamevar z 0 0
gamevar sectnum 0 0

define CM_BLK_WAL 1

state getcoords
    setvarvar x sprite[THISACTOR].x
    setvarvar y sprite[THISACTOR].y
    setvarvar z sprite[THISACTOR].z
    setvarvar sectnum sprite[THISACTOR].sectnum
ends

// Inputs: (wd, cfd)
// Returns: hitwall
//  -1: hit no wall
// >=0: hit that wall
state checknearwall
    setvar hitwall -1

    setvarvar tmp wd, shiftvarl tmp 14

    state getcoords
    clipmovenoslide ret (x, y, z, sectnum) (tmp, 0) (wd, cfd, cfd) CM_BLK_WAL
    ifvarn ret 0 { setvarvar hitwall ret, subvar hitwall 32768, break }

    state getcoords
    clipmovenoslide ret (x, y, z, sectnum) (0, tmp) (wd, cfd, cfd) CM_BLK_WAL
    ifvarn ret 0 { setvarvar hitwall ret, subvar hitwall 32768, break }

    mulvar tmp -1

    state getcoords
    clipmovenoslide ret (x, y, z, sectnum) (tmp, 0) (wd, cfd, cfd) CM_BLK_WAL
    ifvarn ret 0 { setvarvar hitwall ret, subvar hitwall 32768, break }

    state getcoords
    clipmovenoslide ret (x, y, z, sectnum) (0, tmp) (wd, cfd, cfd) CM_BLK_WAL
    ifvarn ret 0 { setvarvar hitwall ret, subvar hitwall 32768, break }
ends

////////// Example usage //////////

onevent EVENT_PROCESSINPUT
    state checknearwall
endevent

define Q_tmp 500
definequote Q_tmp <write on me>

define Q_no 501
definequote Q_no Hit no wall

define Q_yes 502
definequote Q_yes Hit wall %d

define Q_info 503
definequote Q_info pos: %d %d %d sect: %d

onevent EVENT_DISPLAYREST
    qsprintf Q_tmp Q_info, x y z sectnum
    minitext (100, 8) Q_tmp (0, 0)

    ifvarn hitwall -1
    {
        qsprintf Q_tmp Q_yes hitwall
        minitext (100, 16) Q_tmp (0, 0)
    }
    else
    {
        minitext (100, 16) Q_no (0, 0)
    }
endevent


code:
local bit = require("bit")
local band = bit.band

local tostring = tostring

local gv = gv
local actor = actor
local sector, wall, sprite = sector, wall, sprite

local printf = printf
local sectorsofbunch = sectorsofbunch

local con = require("con")

local D = require("CON.DEFS")

----------

local TROR_GLASSBREAKER = 2959  -- red 'T'
-- Actor controlling the timing of a TROR hplane breaking.
gameactor
{
    TROR_GLASSBREAKER,

    function(aci, pli, dist)
        local spr = sprite[aci]
        if (not (spr.lotag == 712 and spr.hitag == 119)) then  -- check BREAKER_MAGIC
            sprite.changestat(aci, actor.STAT.DEFAULT)
            return
        end

        local cnt = actor[aci]:get_count()
        local finish = (cnt >= 6)

        if (cnt == 0) then
            -- NOTE: INTERNAL interface, DON'T USE!
            con._sound(aci, D.GLASS_BREAKING)
        end

        local bunchnum = spr.extra
        for sectnum, what in sectorsofbunch(bunchnum, gv.BOTH_CF) do
            local cf = sector[sectnum][what]
            cf:set_picnum(D.GLASS2 + cnt)
            cf.statbits:clear(sector.STAT.BLOCK + sector.STAT.HITSCAN)
            if (finish) then
                cf.statbits:clear(sector.STAT.TRANS_BITMASK)
            end
        end

        if (finish) then
            con.killit()
        end
    end
}

local DHP = sector.DAMAGEHPLANE

gameevent
{
    "DAMAGEHPLANE",

    function(aci, pli, RETURN)
        local what, sectnum = sector.damagehplane_whatsect(RETURN)
        local sec = sector[sectnum]

        -- Part I: make various screens breakable when it's a ceiling picnum.
        if (what == "ceiling") then
            -- hit ceiling
            if (sec.ceilingpicnum >= 263 and sec.ceilingpicnum <= 275) then
                sec:set_ceilingpicnum(D.W_SCREENBREAK + gv.krand()%3)
                gv.RETURN = DHP.GLASSBREAK
                return con.longjmp()
            end
        end

        gv.RETURN = DHP.DEFAULT
    end
}

gameevent
{
    "DAMAGEHPLANE",

    function(aci, pli, RETURN)
        local what, sectnum = sector.damagehplane_whatsect(RETURN)
        local sec = sector[sectnum]

        -- Part II: breakable TROR hplanes
        local cf = sec[what]
--        printf("damage %s of sector %d (pic %d, bunch %d, hittable: %s)", what, sectnum,
--               cf.picnum, cf.bunch, tostring(cf.statbits:test(sector.STAT.HITSCAN)))

        if (cf.bunch >= 0 and (cf.picnum==198 or cf.picnum==D.GLASS2) and
                cf.statbits:test(sector.STAT.HITSCAN)) then
            local bi = con.insertsprite(TROR_GLASSBREAKER, wall[sec.wallptr], sectnum, actor.STAT.ACTOR)
            local breaker = sprite[bi]

            breaker.cstat = sprite.CSTAT.INVISIBLE
            breaker.lotag, breaker.hitag = 712, 119  -- BREAKER_MAGIC
            breaker.extra = cf.bunch

            gv.RETURN = DHP.SUPPRESS
            return con.longjmp()
        end

        gv.RETURN = DHP.DEFAULT
    end
}


code:
// In CON, the dangling else ambiguity is resolved in a very peculiar way:
// A cascade "ifP1 ifP2 ifP3 Stmt1 ... else Stmt2" is considered as if the
// predicates P1, P2, P3, ... are combined with a logical AND.
// So effectively, there is no ambiguity at all, and one of Stmt1 and Stmt2
// is guaranteed to be taken.

define Q 114
define GLOBAL 0

gamevar x 0 GLOBAL
// divisible by 2, 3, 5?
gamevar xm2 0 GLOBAL
gamevar xm3 0 GLOBAL
gamevar xm5 0 GLOBAL

state testelse
    setvarvar xm2 x, modvar xm2 2
    setvarvar xm3 x, modvar xm3 3
    setvarvar xm5 x, modvar xm5 5

    redefinequote Q ERROR: one path of if/else must be taken

    ifvarn xm2 0
    ifvarn xm3 0
    ifvarn xm5 0
        redefinequote Q x=%d is not divisible by either 2, 3 or 5
    else
        redefinequote Q x=%d is divisible by either 2, 3 or 5

    qsprintf Q Q x
    userquote Q
ends

onevent EVENT_ENTERLEVEL
    setvar x 0
    whilevarn x 31
    {
        state testelse
        addvar x 1
    }
endevent

// Test deferred code. This CON code doesn't make much sense, but check out
// the generated Lua code...
onevent EVENT_JUMP
    ifpdistl 100 ifpdistg 0 ifcanseetarget
    {
        palfrom 32 32 32
        ifcanseetarget
            quote 29
        ifcanseetarget
            quote 30
        setvar x 0
        state testelse
    }
    else ifcanseetarget
        quote 31
    else ifvare 1 1
    {
        // Test empty brace-enclosed statement under if condition.
    }
    else
    {
        quote Q
    }
endevent


code:
local require = require

local string = require("string")
local con = require("con")
local xmath = require("xmath")

local gv = gv
local sprite = sprite
local actor = actor
local player = player

local assert = assert

local printf = printf
local gameevent = gameevent
local spritesofstat = spritesofstat

local Inf = 0/1

module(...)

-- Insert MUSICANDSFX? (Delete it otherwise.)
insp = false

-- Hitag and lotag of last deleted MUSICANDSFX sprite.
tag = {}
tag.hi, tag.lo = 0, 0

-- Preliminary dummy of a local gamevar.
local ournumjumps = 0

local lastv = xmath.vec3()
ilastv = xmath.ivec3()

require "end_gamevars"

-- We may cache globals defined in the gamevar section afterwards, but not
-- refer to locals defined prior to the gamevar section in it.
local tag = tag

local D = require("CON.DEFS")

gameevent{"JUMP", actor.FLAGS.chain_beg,
function(aci, pli)
    local ps = player[pli]

    ournumjumps = ournumjumps+1

    if (insp) then
        -- Insert MUSICANDSFX sprite with same lo-/hitag as last deleted one.
        printf("delmusicsfx: jump count=%d, inserting", ournumjumps)

        local spr = sprite[con.spawn(D.MUSICANDSFX, aci)]
        spr.lotag, spr.hitag = tag.lo, tag.hi
    else
        -- Delete nearest MUSICANDSFX sprite.

        local nearestdst = Inf
        local nearesti = -1

        for i in spritesofstat(actor.STAT.FX) do
            local dst = (sprite[i]-ps.pos):len2()
            if (nearesti == -1 or (dst < nearestdst and dst < sprite[i].hitag)) then
                printf("MSFX %d dist %d", i, dst)
                nearesti = i
                nearestdst = dst
            end
        end

        if (nearesti >= 0) then
            local spr = sprite[nearesti]
            tag.lo, tag.hi = spr.lotag, spr.hitag
            lastv.x, lastv.y, lastv.z = spr.x, spr.y, spr.z+0.5
            ilastv.x, ilastv.y, ilastv.z = spr.x, spr.y, spr.z
            actor.delete(nearesti)
        end

        assert(nearesti < 0 or sprite[nearesti].picnum==D.MUSICANDSFX)
        printf("delmusicsfx: jump count=%d, deleting sprite %d", ournumjumps, nearesti)
    end

    insp = not insp
end}

-- Display the number of times we jumped on the screen.
gameevent
{
    "DISPLAYREST",

    function()
        con.minitext(240, 10, string.format("jumped %d times", ournumjumps))
    end
}


code:
actor 1890 1
    // The following "else { if" must not be translated to an "elseif".
    ifcount 1 nullop else
    {
        ifvare 0 0
            spawn 1
        else
            spawn 2

        ifvare 1 1
            spawn 3
        else ifvare 1 2
            spawn 4
    }
enda


code:
definequote 125 FIRST
definequote 126 SECOND
definequote 127 THIRD

onevent EVENT_ENTERLEVEL
    echo 125
endevent

onevent EVENT_ENTERLEVEL
    echo 126
    break
    echo 126
endevent

onevent EVENT_ENTERLEVEL
    echo 127
endevent

eventloadactor 930  // "police line" ribbon
    // make non-destroyable
    setactor[THISACTOR].hitag 0
enda

eventloadactor 2491  // DUKECAR
    killit
enda

/*
Outputs:
========

C-CON:
------
(Since r5097:)
THIRD
SECOND
FIRST
(Before r5097:)
THIRD
SECOND

LunaCON:
--------
THIRD
SECOND
FIRST

*/


code:
// Test for gamearrays / their persistence.

define QWESZ 10
define ASDSZ 12

define MAXTILES 30720  // XXX

gamearray qwe QWESZ
gamearray asd ASDSZ

define NEWQWESZ 9
define NEWASDSZ 14

gamearray qwe1 1  // should auto-resize when reading in
gamearray asd1 NEWASDSZ

gamevar qwesz 0 0
gamevar asdsz 0 0

// Test gamevars that are named like Lua keywords.
gamevar local 0 0

gamevar i 0 0
gamevar tmp 0 0

define Q_qwe_fn 400
define Q_asd_fn 401
definequote Q_qwe_fn LTEST_QWE_ARRAY.bin
definequote Q_asd_fn LTEST_ASD_ARRAY.bin

// Wouldn't run in C-CON otherwise; redefinequote/qsprintf errors out when
// passed a non-allocated quote as destination.
definequote 500 TEMP
definequote 501 TEMP

definequote 505 tile y sizes: 0:%d 1:%d 2:%d 3:%d 4:%d 5:%d 6:%d 7:%d 8:%d
definequote 506 1: 0x0a0a0d0d==168430861 ? %d
definequote 507 2: 0x0a0a0d0d==168430861 ? %d

gamevar MINUSONE -1 0

state error
    setactor[MINUSONE].cstat 0

    // This doesn't work in C-CON:
//    setactor[-1].cstat 0
    // error: symbol `1' is not a game variable.
ends

onevent EVENT_ENTERLEVEL
    setvar i 0
    whilevarn i QWESZ
    {
        setarray qwe[i] i
        addvar i 1
    }

    // Test nested array syntax:
    setarray qwe[qwe[5]] 0

    // qwe[] now:
    // 0 1 2 3 4 0 6 7 8 9

    setvar i 0
    whilevarn i ASDSZ
    {
        setvarvar tmp i
        addvar tmp 100
        setarray asd[i] tmp
        setvarvar local tmp
        addvar i 1
    }

    copy qwe[4] /*->*/ asd[3] 3

    setarray asd[4] 0x0a0a0d0d

    // asd[] now:
    // 100 101 102 4 0x0a0a0d0d 6 106 107 108 109 110 111

    qsprintf 506 506 asd[4]
    userquote 506

    resizearray qwe NEWQWESZ  // shrink
    resizearray asd NEWASDSZ  // grow

    ifvarn qwe[4] 4 state error
    ifvarn asd[4] 0x0a0a0d0d state error

    writearraytofile qwe Q_qwe_fn
    writearraytofile asd Q_asd_fn

    readarrayfromfile qwe1 Q_qwe_fn
    readarrayfromfile asd1 Q_asd_fn

    qsprintf 507 507 asd[4]
    userquote 507

    ifvarn asd[4] 0x0a0a0d0d state error

    // test array sizes
    getarraysize qwe qwesz
    getarraysize asd asdsz

    ifvarn qwesz NEWQWESZ state error
    ifvarn asdsz NEWASDSZ state error

    // test array sizes with *1 arrays
    getarraysize qwe1 qwesz
    getarraysize asd1 asdsz

    ifvarn qwesz NEWQWESZ state error
    ifvarn asdsz NEWASDSZ state error

    redefinequote 500 [%d]: qwe=%d, qwe1=%d, asd=%d, asd1=%d
    redefinequote 501 Because C-CON can't qsprintf into an uninitialized one

    setvar i 0
    whilevarvarn i -1
    {
        ifvarl i NEWQWESZ
            qsprintf 501 500 i qwe[i] qwe1[i] asd[i] asd1[i]
        else
            qsprintf 501 500 i -1 -1 asd[i] asd1[i]
        userquote 501
        addvar i 1

        ifvare i NEWASDSZ
            setvar i -1
    }

    redefinequote 500 MUST REACH
    userquote 500

    setvar i 120
    modvar i 25
    addlogvar i

    //// Test system gamearrays

    getarraysize tilesizx qwesz
    ifvarn qwesz MAXTILES state error

    copy tilesizy[0] /*->*/ qwe[0] 9
    qsprintf 505 505 qwe[0] qwe[1] qwe[2] qwe[3] qwe[4] qwe[5] qwe[6] qwe[7] qwe[8]
    userquote 505

    // Must give a translation error:
//    resizearray tilesizy 100
endevent


code:
// From http://wiki.eduke32.com/wiki/How_to_make_a_clock (author: The Commander)
// with modifications

gamevar sec 0 0
gamevar min 0 0
gamevar hour 0 0
gamevar mday 0 0
gamevar mon 0 0
gamevar year 0 0
gamevar wday 0 0
gamevar yday 0 0

onevent EVENT_DISPLAYREST
    gettimedate sec min hour mday mon year wday yday

    ifvarl hour 20 ifvarg hour 9
        digitalnumberz 2992 285 16 hour 0 0 0 0 0 xdim ydim 65536

    ifvarg hour 19
        digitalnumberz 2992 282 16 hour 0 0 0 0 0 xdim ydim 65536

    ifvarl hour 10
    {
        digitalnumberz 2992 288 16 hour 0 0 0 0 0 xdim ydim 65536
        rotatesprite 269 16 65536 0 2992 0 0 0 0 0 xdim ydim
    }

    ifvarg min 9
        digitalnumberz 2992 308 16 min 0 0 0 0 0 xdim ydim 65536

    ifvarl min 10
    {
        digitalnumberz 2992 315 16 min 0 0 0 0 0 xdim ydim 65536
        rotatesprite 296 16 65536 0 2992 0 0 0 0 0 xdim ydim
    }

    gametextz STARTALPHANUM 20 20  1  0 0 0  0 0 xdim ydim 32768
endevent

// Following code by Hendricks266, from
// http://forums.duke4.net/topic/1093-what-are-you-working-on-for-duke-right-now/page__view__findpost__p__152626

gamevar zoom 0 0

definequote 10000 abcdefghijklm
definequote 10001 nopqrstuvwxyz

onevent EVENT_DISPLAYREST
    setvarvar zoom totalclock
    modvar zoom 240
    mulvar zoom 8192
    divvar zoom 15

    digitalnumberz DIGITALNUM 160 5 1234567890 0 0 16 windowx1 windowy1 windowx2 windowy2 zoom

    gametextz STARTALPHANUM 320 30 10000 0 0 16 windowx1 windowy1 windowx2 windowy2 zoom

    rotatesprite 160 70 zoom 0 SPINNINGNUKEICON 0 0 0 windowx1 windowy1 windowx2 windowy2
endevent

// Following code from the wiki 'showview' entry, with modifications
gamevar x 0 0
gamevar y 0 0
gamevar z 0 0
gamevar ang 0 0
gamevar horiz 0 0
gamevar temp 0 0
gamevar sect 0 0

onevent EVENT_DISPLAYREST
    getplayer[THISACTOR].posx x
    getplayer[THISACTOR].posy y
    getplayer[THISACTOR].posz z
    getplayer[THISACTOR].ang ang
    getplayer[THISACTOR].horiz horiz
    getplayer[THISACTOR].horizoff temp
    addvarvar horiz temp

    // Head cam.
    subvar z 16384
    updatesectorz x y z sect
    // This upper view should be a bit wider.
    ifvarg sect -1
        showviewunbiased x y z ang horiz sect 16 8 156 39

    // Groin cam.
    addvar z 16384
    addvar z 4096
    updatesectorz x y z sect
    ifvarg sect -1
        showview x y z ang horiz sect 16 39 156 70
endevent


code:
definegametype 0 46051 Cooperative, not Dukematch

// NOTE: we need to define the rest anew too (or at least #4), because
//  'definegametype' does "g_numGametypes = idx+1", as opposed to
//  "g_numGametypes = max(g_numGametypes, idx+1)".
definegametype 1 46051 Cooperative
definegametype 2 16410 Dukematch (no spawn)
definegametype 3 216088 Team Dukematch
definegametype 4 213018 Team Dukematch (no spawn)

gamevar temp 0 0

// Original version by Fox, from
// http://forums.duke4.net/topic/775-eduke32-20-and-polymer/page__view__findpost__p__183890
onevent EVENT_DISPLAYREST
    // Notes:
    //  - index (THISACTOR) is never used.
    //  - incorrect if screenpeek != myconnectindex, e.g. by pressing [K]:
    //  - all three are equivalent:
    getuserdef[THISACTOR].statusbarscale temp
    getuserdef .statusbarscale temp
    getuserdef[].statusbarscale temp

    redefinequote 0 %ld
    qsprintf 0 0 temp
    gametext STARTALPHANUM 10 10 0 0 0 0 0 0 xdim ydim
endevent


code:
-- A spawner (NUKEBUTTON+3) of colored TRANSPORTERSTAR+4 sprites in a helical
-- arrangement.

local require = require
local math = require("math")

local con = require("con")
local xmath = require("xmath")

local gv = gv
local actor = actor
local player = player
local sprite = sprite

local gameactor = gameactor

module(...)

-- Dual-typed per-actor array: false if a broken nuke switch is not enabled,
-- start game tic of when it was enabled otherwise.
-- NOTE: for objects that are not supposed to be deleted such as this one, it
-- would also be OK to use a plain table.
local nukeswStart = con.actorvar(false)

-- This one should be a per-actor variable because it holds info about
-- "volatile" actors.
local starPal = con.actorvar(0)

-- Color per decasecond, can be changed from outside.
COLOR = { 1, 2, 6, 7, 8 }

require("end_gamevars")

local bangvec = xmath.bangvec
local angvec = xmath.angvec

local D = require("CON.DEFS")
local GTICSPERSEC = gv.GTICSPERSEC

gameactor
{
    D.TRANSPORTERSTAR+4,

    flags = actor.FLAGS.NOCLIP,

    move = con.move{100},
    movflags = actor.MOVFLAGS.geth,

    func = function(aci)
        local spr = sprite[aci]
        -- NOTE: this is prettier than calling it 'a', even if 'act' is used to
        -- denote an action in other places:
        local act = actor[aci]

        if (act:has_action(0)) then
            act:set_action(1)  -- TODO: actor constructors, i.e. 'init' callbacks

            local decasec = math.floor((gv.gametic - nukeswStart[spr.owner])/(GTICSPERSEC*10)) % 12

            local pal = COLOR[decasec+1]
            if (pal ~= nil) then
                starPal[aci] = pal
            end

            -- Every 2nd minute, we stop coloring the spawned stars. This tests
            -- per-actor variable resetting to the default value.
            spr.pal = starPal[aci]
        end

        if (act:checkbump()) then
            con.killit()
        end

        -- Test spr:changesect() vs. sprite.changesect()
        local sectnum = spr.sectnum

        for i=0,gv.numsectors-1 do
            if (spr.pal ~= 2 and spr.pal ~= 7) then
                sprite.changesect(aci, i)  -- noticeably faster...
            else
                spr:changesect(i)  -- ...than this
            end
        end

        sprite.changesect(aci, sectnum)
    end
}

local CS = sprite.CSTAT
local SPAWNSPERTIC = 10  --> 300/second --> 18000 per minute
local TWOPI = 2*math.pi

gameactor
{
    D.NUKEBUTTON+3,  -- destroyed end-of-level nuke switch

    function(aci)
        local spr = sprite[aci]

        for pi in player.all() do
            -- XXX: how to emulate "use switch" distance checking code, but in
            -- an actor-centric fashion?
            if (not nukeswStart[aci] and player.holdskey(pi, "OPEN")
                    and (player[pi].pos - spr):len2sq() < 256^2) then
                -- Enable us.
                nukeswStart[aci] = gv.gametic
                spr.cstatbits:clear(CS.TRANS_BITMASK)
                spr.cstatbits:set(CS.TRANS1)
                break
            end
        end

        local startgtic = nukeswStart[aci]
        if (not startgtic) then
            return
        end

        local hei, zofs = spr:getheightofs()
        local radius = hei/2

        for i=0,SPAWNSPERTIC-1 do
            -- Make one second go once round the circle, spawning
            -- SPAWNSPERTIC*GTICSPERSEC stars.
            local ii = ((gv.gametic*SPAWNSPERTIC)%(GTICSPERSEC*SPAWNSPERTIC)) + i
            local v = (radius/16)*angvec(ii*TWOPI/(GTICSPERSEC*SPAWNSPERTIC))
            local circvec = xmath.vec3(0, v.x, 16*v.y):rotate(spr.ang)
            local pos = spr^(zofs + radius) + 256*bangvec(spr.ang) + circvec

            con.insertsprite{D.TRANSPORTERSTAR+4, pos, spr.sectnum, actor.STAT.ACTOR, aci,
                             xrepeat=3, yrepeat=3, ang=spr.ang}
        end
    end
}


code:
// Map state test.
// To warp to a particular map, press the steroids key.
// Volume: player angle / 512, displayed on screen.
// Level: current weapon.
// Clear mapstate: press [QuickKick]

define Q_tmp 400
definequote Q_tmp ====== TEMP =======

definequote 401 WILL WARP TO VOLUME %d

define SQ 402
definequote SQ <source>
definequote 403 <dest>
definequote 404 <dest>
definequote 405 <dest>
definequote 406 <dest>
definequote 407 <dest>
definequote 408 <dest>
definequote 409 <dest>
definequote 410 <dest>
definequote 411 <dest>
definequote 412 <dest>
definequote 413 <dest>
definequote 414 <dest>

define Q_cms 500
definequote Q_cms Cleared mapstate for volume %d level %d

// Test gamevar flags GAMEVAR_NODEFAULT and GAMEVAR_NORESET.
// Global.
define GV_NODEFAULT 1024
define GV_NORESET 131072
define GV_NODEFAULT_NORESET 132096
// Per-player.
define PV_NODEFAULT 1025
define PV_NORESET 131073
define PV_NODEFAULT_NORESET 132097
// Per-actor (not very meaningful, as the APLAYER actor will in general have a
// different sprite for different levels).
define AV_NODEFAULT 1026
define AV_NORESET 131074
define AV_NODEFAULT_NORESET 132098

// global
gamevar numwarps 0 0
gamevar numwarpsD 0 GV_NODEFAULT
gamevar numwarpsR 0 GV_NORESET
gamevar numwarpsDR 0 GV_NODEFAULT_NORESET
// per-player
gamevar Pnumwarps 0 1
gamevar PnumwarpsD 0 PV_NODEFAULT
gamevar PnumwarpsR 0 PV_NORESET
gamevar PnumwarpsDR 0 PV_NODEFAULT_NORESET
// per-actor
gamevar Anumwarps 0 2
gamevar AnumwarpsD 0 AV_NODEFAULT
gamevar AnumwarpsR 0 AV_NORESET
gamevar AnumwarpsDR 0 AV_NODEFAULT_NORESET

gamevar pal 0 0
gamevar dq 0 0
gamevar y 0 0

state calcvolume
    getplayer[THISACTOR].ang gs
    andvar gs 2047
    divvar gs 512
ends

state dispnumwarps
    gametextz STARTALPHANUM 20 y  dq  0 pal 0  0 0 xdim ydim 32768
    addvar dq 1
    addvar y 8
ends

onevent EVENT_DISPLAYREST
    state calcvolume
    qsprintf Q_tmp /*<-*/ 401 /**/ gs
    gametext STARTALPHANUM 20 20  Q_tmp  0 0 0  0 0 xdim ydim

    setvar y 40
    setvar dq 403
    setvar pal 0

    // global
    redefinequote SQ numwarps (no flags): %d
    qsprintf dq SQ numwarps
    state dispnumwarps
    redefinequote SQ numwarps (NODEFAULT): %d
    qsprintf dq SQ numwarpsD
    state dispnumwarps
    redefinequote SQ numwarps (NORESET): %d
    qsprintf dq SQ numwarpsR
    state dispnumwarps
    redefinequote SQ numwarps (NODEFAULT, NORESET): %d
    qsprintf dq SQ numwarpsDR
    state dispnumwarps

    // per-player
    setvar pal 14
    redefinequote SQ Pnumwarps (no flags): %d
    qsprintf dq SQ Pnumwarps
    state dispnumwarps
    redefinequote SQ Pnumwarps (NODEFAULT): %d
    qsprintf dq SQ PnumwarpsD
    state dispnumwarps
    redefinequote SQ Pnumwarps (NORESET): %d
    qsprintf dq SQ PnumwarpsR
    state dispnumwarps
    redefinequote SQ Pnumwarps (NODEFAULT, NORESET): %d
    qsprintf dq SQ PnumwarpsDR
    state dispnumwarps

    // per-actor
    setvar pal 10
    redefinequote SQ Anumwarps (no flags): %d
    qsprintf dq SQ Anumwarps
    state dispnumwarps
    redefinequote SQ Anumwarps (NODEFAULT): %d
    qsprintf dq SQ AnumwarpsD
    state dispnumwarps
    redefinequote SQ Anumwarps (NORESET): %d
    qsprintf dq SQ AnumwarpsR
    state dispnumwarps
    redefinequote SQ Anumwarps (NODEFAULT, NORESET): %d
    qsprintf dq SQ AnumwarpsDR
    state dispnumwarps
endevent

onevent EVENT_USESTEROIDS
    // global
    addvar numwarps 1
    addvar numwarpsD 1
    addvar numwarpsR 1
    addvar numwarpsDR 1
    // per-player
    addvar Pnumwarps 1
    addvar PnumwarpsD 1
    addvar PnumwarpsR 1
    addvar PnumwarpsDR 1
    // per-actor
    addvar Anumwarps 1
    addvar AnumwarpsD 1
    addvar AnumwarpsR 1
    addvar AnumwarpsDR 1

    savemapstate

    state calcvolume
    startlevel gs currentweapon
endevent

onevent EVENT_QUICKKICK
    state calcvolume

    qsprintf Q_tmp Q_cms gs currentweapon
    userquote Q_tmp

    // Calc linear map index.
    mulvar gs 64  // MAXLEVELS (XXX: should be exposed via CON)
    addvarvar gs currentweapon

    clearmapstate gs
endevent

onevent EVENT_ENTERLEVEL
    loadmapstate
endevent

onevent EVENT_USEJETPACK
    savegamevar numwarpsD
endevent

onevent EVENT_USENIGHTVISION
    readgamevar numwarpsD
endevent


code:
local xmath = require("xmath")

local sprite = sprite

--

local maputil = {}

-- csfunc = maputil.CreateCrossSpriteFunc(use_sprite_i_func, use_sprite_j_func)
--
-- use_sprite_i_func: function(spri, ud), where
--   <spri> is a reference to sprite i
-- use_sprite_j_func: function(sprj, spri, ud), where
--   <spri> is a reference to sprite i
--   <sprj> is a reference to sprite j
--
-- csfunc: function(userdata, process_sprite_j_func), where
--   <userdata> is passed as <ud> to the functions described above
--   <process_sprite_j_func>: function(sprj, spri, ud), the user-provided
--     function that processes sprite j in some way
function maputil.CreateCrossSpriteFunc(use_sprite_i_func, use_sprite_j_func)
    return function(userdata, process_sprite_j_func)
        for i in sprite.all() do
            if (use_sprite_i_func(sprite[i], userdata)) then
                for j in sprite.all() do
                    if (use_sprite_j_func(sprite[j], sprite[i], userdata)) then
                        process_sprite_j_func(sprite[j], sprite[i], userdata)
                    end
                end
            end
        end
    end
end

-- Functions for "for all sprites Y of tile X that are closer than D, ..."
-- Passed userdefs: { picnumi, picnumj, maxldist }
local function snearpic_usei(spr, ud)
    return spr.picnum == ud[1]
end

local function snearpic_usej(sprj, spri, ud)
    return sprj.picnum == ud[2] and xmath.ldist(spri, sprj) <= ud[3]
end

local snearpic_func = maputil.CreateCrossSpriteFunc(snearpic_usei, snearpic_usej)

-- maputil.for_sprites_near_picnum(picnumi, picnumj, dist, process_sprite_func)
--
-- Runs the following loop:
--
-- for all sprites i with tile <picnumi>,
--   for all sprites j of tile <picnumj> that are closer to sprite i than <dist> [*],
--     call process_sprite_func(sprite[j], sprite[i])
--
-- [*] using xmath.ldist()
function maputil.for_sprites_near_picnum(picnumi, picnumj, dist, process_sprite_func)
    snearpic_func({picnumi, picnumj, dist}, process_sprite_func)
end

return maputil


code:
// NOTE: This file is misnamed, "break" is local control flow, of course.

state teststate_break
    ifvare 1 1
    {
        redefinequote 114 BEFORE
        echo 114

        break

        redefinequote 114 AFTER
        echo 114
    }

    redefinequote 114 STILL LIVE INNER
    echo 114
ends

definequote 499 ===
definequote 500 MAXSPRITES: %d
definequote 501 MAXSTATUS: %d
definequote 502 MAX_WEAPONS: %d

// XXX: EVENT_INIT not reached with LunaCON
onevent EVENT_ENTERLEVEL
    state teststate_break

    redefinequote 114 STILL LIVE OUTER
    echo 114

    qsprintf 499 /*<-*/ 500 MAXSPRITES
    echo 499
    qsprintf 499 /*<-*/ 501 MAXSTATUS
    echo 499
    qsprintf 499 /*<-*/ 502 MAX_WEAPONS
    echo 499
endevent

// Test number parsing
gamevar MINUS_ONE  -1 0
gamevar INT32_MIN   0x80000000 0
gamevar INT32_MIN_ -0x80000000 0
gamevar INT32_MINh   0x80000000h 0
gamevar INT32_MIN_h -0x80000000h 0

gamevar INT32_MIN_P1 0x80000001 0
gamevar INT32_MIN_P1h 0x80000001h 0
//gamevar INT32_MAX_  -0x80000001 0  // error

gamevar INT32_MAX   0x7fffffff 0
gamevar INT32_MAXh   0x7fffffffh 0

// 9 digits? truncate it, but only because there's an F there.
gamevar SOMEHEX 0xFFEFC0001 0

// This one throws an error:
//gamevar BADHEX 0xBFEFC0001 0


code:
definequote 125 SPAWNED HEAVYHBOMB
definequote 126 RAN EVENT_EGS
definequote 127 RAN TEST STATE

state teststate1
    return
    quote 127
ends

onevent EVENT_EGS
    ifactor HEAVYHBOMB
    {
        state teststate1  // after teststate1's return, return from EVENT_EGS!
        quote 126
    }
endevent

onevent EVENT_FIRE
    // the concrete actor is irrelevant, only placeholder
    spawn HEAVYHBOMB  // --> EVENT_EGS
    quote 125
endevent