-- INTERNAL
-- definitions of BUILD and game types for the Lunatic Interpreter

_EDUKE32_LUNATIC = true

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

---=== Duke3D engine and game definitions ===---
ffi.cdef[[
#pragma pack(push,1)
typedef struct
{
    const int16_t wallptr, wallnum;
    int32_t ceilingz, floorz;
    int16_t ceilingstat, floorstat;
    int16_t ceilingpicnum, ceilingheinum;
    int8_t ceilingshade;
    uint8_t ceilingpal, ceilingxpanning, ceilingypanning;
    int16_t floorpicnum, floorheinum;
    int8_t floorshade;
    uint8_t floorpal, floorxpanning, floorypanning;
    uint8_t visibility, filler;
    int16_t lotag, hitag, extra;
} sectortype;

typedef struct
{
    int32_t x, y;
    const int16_t point2, nextwall, nextsector;
    int16_t cstat;
    int16_t picnum, overpicnum;
    int8_t shade;
    uint8_t pal, xrepeat, yrepeat, xpanning, ypanning;
    int16_t lotag, hitag, extra;
} walltype;

typedef struct
{
    int32_t x, y, z;
    int16_t cstat, picnum;
    int8_t shade;
    uint8_t pal, clipdist, filler;
    uint8_t xrepeat, yrepeat;
    int8_t xoffset, yoffset;
    const int16_t sectnum, statnum;
    int16_t ang, owner, xvel, yvel, zvel;
    int16_t lotag, hitag, extra;
} spritetype;

typedef struct {
    const uint32_t mdanimtims;
    const int16_t mdanimcur;
    int16_t angoff, pitch, roll;
    int32_t xoff, yoff, zoff;
    uint8_t flags;
    uint8_t xpanning, ypanning;
    const uint8_t filler;
    float alpha;
    const int32_t _do_not_use1;
    const int32_t _do_not_use2;
} spriteext_t;

typedef struct {
    int32_t x, y, z;
} vec3_t;

typedef struct {
    vec3_t pos;
    int16_t sprite, wall, sect;
} hitdata_t;

typedef struct {
    char r,g,b,f;
} palette_t;
#pragma pack(pop)
]]

local vec3_ct = ffi.typeof("vec3_t")
local hitdata_ct = ffi.typeof("hitdata_t")

---- engine data and functions ----

-- GLOBAL gv: provides access to C global *scalars* and safe functions
-- XXX: still exposes C library functions etc. contained in ffi.C.
--  Is this a problem?
local gv_ = {
    -- all non-scalars need to be explicitly listed here and access to them is
    -- redirected to the dummy empty table...
}

local dummy_empty_table = {}

-- This is for declarations of arrays or pointers which should not be
-- accessible through the gv global.
-- NOTE: don't declare multiple pointers on one line (like int32_t *a, *b;)!
local function decl(str)
    for varname in string.gmatch(str, "([%a_][%w_]*)[[(;]") do
--        print("protect "..varname)
        gv_[varname] = dummy_empty_table
    end

    ffi.cdef(str)
end

ffi.cdef[[int32_t engine_main_arrays_are_static, engine_v8;]]

-- NOTE TO SELF: This is not C, never EVER write
--    if (x)
-- when checking a C variable x for 'thuthiness'
if (ffiC.engine_main_arrays_are_static ~= 0) then
    decl[[
    sectortype sector[];
    walltype wall[];
    spritetype sprite[];
    spriteext_t spriteext[];
    ]]
else
    decl[[
    sectortype *sector;
    walltype *wall;
    spritetype *sprite;
    spriteext_t *spriteext;
    ]]
end

if (ffiC.engine_v8 == 0) then
    -- V7
    ffi.cdef[[
enum
{
    MAXSECTORS = 1024,
    MAXWALLS = 8192,
    MAXSPRITES = 4096,
}
]]
else
    -- V8
    ffi.cdef[[
enum
{
    MAXSECTORS = 4096,
    MAXWALLS = 16384,
    MAXSPRITES = 16384,
}
]]
end

ffi.cdef[[
enum {
    MAXSTATUS = 1024,
    MAXTILES = 30720,

    MAXBUNCHES = 256,
    CEILING = 0,
    FLOOR = 1,

    CLIPMASK0 = (1<<16)+1,  // blocking
    CLIPMASK1 = (256<<16)+64,  // hittable
};
]]

ffi.cdef[[
const int16_t numsectors, numwalls;
const int32_t numyaxbunches;
const int32_t totalclock;
const int32_t xdim, ydim;
]]

decl[[
const int16_t headspritesect[MAXSECTORS+1], headspritestat[MAXSTATUS+1];
const int16_t prevspritesect[MAXSPRITES], prevspritestat[MAXSPRITES];
const int16_t nextspritesect[MAXSPRITES], nextspritestat[MAXSPRITES];

const int16_t headsectbunch[2][MAXBUNCHES], nextsectbunch[2][MAXSECTORS];

int16_t yax_getbunch(int16_t i, int16_t cf);

int32_t hitscan(const vec3_t *sv, int16_t sectnum, int32_t vx, int32_t vy, int32_t vz,
                hitdata_t *hitinfo, uint32_t cliptype);

void rotatesprite(int32_t sx, int32_t sy, int32_t z, int16_t a, int16_t picnum,
                  int8_t dashade, char dapalnum, int32_t dastat,
                  int32_t cx1, int32_t cy1, int32_t cx2, int32_t cy2);
]]

-- array -> element flattening inside structs,
-- e.g. int32_t a[4] --> int32_t _a0, _a1, _a2, _a3;
local function repeat_n_elts(type, namebase, num)
    local strbuf = { "const "..type.." " }

    for i=1,num do
        local str = namebase..tostring(i-1)
        if (i < num) then
            str = str..","
        end
        strbuf[#strbuf+1] = str
    end
    strbuf[#strbuf+1] = ";"

    return table.concat(strbuf)
end

---- game structs ----
ffi.cdef[[
enum dukeinv_t {
    GET_STEROIDS,
    GET_SHIELD,
    GET_SCUBA,
    GET_HOLODUKE,
    GET_JETPACK,
    GET_DUMMY1,
    GET_ACCESS,
    GET_HEATS,
    GET_DUMMY2,
    GET_FIRSTAID,
    GET_BOOTS,
    GET_MAX
};

enum dukeweapon_t {
    KNEE_WEAPON,
    PISTOL_WEAPON,
    SHOTGUN_WEAPON,
    CHAINGUN_WEAPON,
    RPG_WEAPON,
    HANDBOMB_WEAPON,
    SHRINKER_WEAPON,
    DEVISTATOR_WEAPON,
    TRIPBOMB_WEAPON,
    FREEZE_WEAPON,
    HANDREMOTE_WEAPON,
    GROW_WEAPON,
    MAX_WEAPONS
};

enum {
    MAXPLAYERS = 16,
};
]]

ffi.cdef([[
#pragma pack(push,1)

struct action {
    int16_t startframe, numframes;
    int16_t viewtype, incval, delay;
};

struct move {
    int16_t hvel, vvel;
};

typedef struct { int32_t id; struct move mv; } con_move_t;
typedef struct { int32_t id; struct action ac; } con_action_t;

typedef struct {
    int32_t id;
    con_action_t act;
    con_move_t mov;
    int32_t movflags;
} con_ai_t;

// TODO: still need to make some fields read-only
// NOTE: must not expose arrays in structs!!!
typedef struct
{
]]
..repeat_n_elts("int32_t", "_t", 10)..
[[
//    const int32_t t_data[10];  // 56b sometimes used to hold offsets to con code
    const struct move mv;
    const struct action ac;
    const int16_t _padding;

    int16_t picnum,ang,extra,owner; //8b
    int16_t movflag,tempang,timetosleep; //6b

    int32_t flags, bposx,bposy,bposz; //16b
    int32_t floorz,ceilingz,lastvx,lastvy; //16b
    int32_t lasttransport; //4b

    const int16_t lightId, lightcount, lightmaxrange, cgg; //8b
    int16_t actorstayput, dispicnum, shootzvel; // 6b
]]
..repeat_n_elts("int8_t", "_d", 8)..
[[
//    const int8_t _do_not_use[8];
} actor_t;

// The _u_t versions are unrestricted variants for internal use.
typedef struct
{
    int32_t t_data[10];
    struct move mv;
    struct action ac;
    const int16_t padding_;

    int16_t picnum,ang,extra,owner;
    int16_t movflag,tempang,timetosleep;

    int32_t flags, bposx,bposy,bposz;
    int32_t floorz,ceilingz,lastvx,lastvy;
    int32_t lasttransport;

    int16_t lightId, lightcount, lightmaxrange, cgg;
    int16_t actorstayput, dispicnum, shootzvel;
    const int8_t _do_not_use[8];
} actor_u_t;

typedef struct {
    vec3_t pos, opos, vel, npos;
    int32_t bobposx, bobposy;
    int32_t truefz, truecz, player_par;
    int32_t randomflamex, exitx, exity;
    int32_t runspeed, max_player_health, max_shield_amount;

    uint32_t interface_toggle_flag;

    uint8_t palette;

    uint16_t max_actors_killed, actors_killed;
    uint16_t gotweapon, zoom;
]]
..repeat_n_elts("int16_t", "_lx", 64)..repeat_n_elts("int16_t", "_ly", 64)..
[[
//    int16_t loogiex[64], loogiey[64];
    int16_t sbs, sound_pitch;

    int16_t ang, oang, angvel, cursectnum, look_ang, last_extra, subweapon;
]]
..repeat_n_elts("int16_t", "_ma", ffiC.MAX_WEAPONS)
..repeat_n_elts("int16_t", "_aa", ffiC.MAX_WEAPONS)
..repeat_n_elts("int16_t", "_ia", ffiC.GET_MAX)..
[[
//    int16_t max_ammo_amount[MAX_WEAPONS], ammo_amount[MAX_WEAPONS], inv_amount[GET_MAX];
    int16_t wackedbyactor, pyoff, opyoff;

    int16_t horiz, horizoff, ohoriz, ohorizoff;
    int16_t newowner, jumping_counter, airleft;
    int16_t fta, ftq, access_wallnum, access_spritenum;
    int16_t got_access, weapon_ang, visibility;
    int16_t somethingonplayer, on_crane, i, one_parallax_sectnum;
    int16_t random_club_frame, one_eighty_count;
    int16_t dummyplayersprite, extra_extra8;
    int16_t actorsqu, timebeforeexit, customexitsound, last_pissed_time;
]]
..repeat_n_elts("int16_t", "_w", ffiC.MAX_WEAPONS)..
[[
//    int16_t weaprecs[MAX_WEAPONS];
    int16_t weapon_sway, crack_time, bobcounter;

    int16_t orotscrnang, rotscrnang, dead_flag;   // JBF 20031220: added orotscrnang
    int16_t holoduke_on, pycount;
    int16_t transporter_hold;

    uint8_t max_secret_rooms, secret_rooms;
    uint8_t frag, fraggedself, quick_kick, last_quick_kick;
    uint8_t return_to_center, reloading, weapreccnt;
    uint8_t aim_mode, auto_aim, weaponswitch, movement_lock, team;
    uint8_t tipincs, hbomb_hold_delay, frag_ps, kickback_pic;

    uint8_t gm, on_warping_sector, footprintcount, hurt_delay;
    uint8_t hbomb_on, jumping_toggle, rapid_fire_hold, on_ground;
    uint8_t inven_icon, buttonpalette, over_shoulder_on, show_empty_weapon;

    uint8_t jetpack_on, spritebridge, lastrandomspot;
    uint8_t scuba_on, footprintpal, heat_on, invdisptime;

    uint8_t holster_weapon, falling_counter, footprintshade;
    uint8_t refresh_inventory, last_full_weapon;

    uint8_t toggle_key_flag, knuckle_incs, knee_incs, access_incs;
    uint8_t walking_snd_toggle, palookup, hard_landing, fist_incs;

    int8_t numloogs, loogcnt, scream_voice;
    int8_t last_weapon, cheat_phase, weapon_pos, wantweaponfire, curr_weapon;

    palette_t pals;
]]
..repeat_n_elts("char", "_n", 32)..
[[
//    char name[32];

    int8_t padding_;
} DukePlayer_t;

typedef struct {
    uint32_t bits; // 4b
    int16_t fvel, svel; // 4b
    int8_t avel, horz; // 2b
    int8_t extbits, filler; // 2b
} input_t;

typedef struct {
    DukePlayer_t *ps;
    input_t *sync;

    int32_t netsynctime;
    int16_t ping, filler;
    int32_t pcolor, pteam;
    uint8_t frags[MAXPLAYERS], wchoice[MAX_WEAPONS];

    char vote, gotvote, pingcnt, playerquitflag;
    char user_name[32];
    uint32_t revision;
} playerdata_t;
#pragma pack(pop)

enum
{
    MAXMOUSEBUTTONS = 10,
    MAXMOUSEAXES = 2,
    MAXJOYBUTTONS = (32+4),
    MAXJOYAXES = 8,
    NUMGAMEFUNCTIONS = 56,
};

typedef struct {
    vec3_t camera;
    int32_t const_visibility,uw_framerate;
    int32_t camera_time,folfvel,folavel,folx,foly,fola;
    int32_t reccnt,crosshairscale;

    int32_t runkey_mode,statusbarscale,mouseaiming,weaponswitch,drawweapon;   // JBF 20031125
    int32_t democams,color,msgdisptime,statusbarmode;
    int32_t m_noexits,noexits,autovote,automsg,idplayers;
    int32_t team, viewbob, weaponsway, althud, weaponscale, textscale;

    int32_t entered_name,screen_tilting,shadows,fta_on,executions,auto_run;
    int32_t coords,tickrate,levelstats,m_coop,coop,screen_size,lockout,crosshair;
    int32_t playerai,angleinterpolation,obituaries;

    int32_t respawn_monsters,respawn_items,respawn_inventory,recstat,monsters_off,brightness;
    int32_t m_respawn_items,m_respawn_monsters,m_respawn_inventory,m_recstat,m_monsters_off,detail;
    int32_t m_ffire,ffire,m_player_skill,m_level_number,m_volume_number,multimode;
    int32_t player_skill,level_number,volume_number,m_marker,marker,mouseflip;

    int32_t configversion;

    int16_t cameraang, camerasect, camerahoriz;
    int16_t pause_on,from_bonus;
    int16_t camerasprite,last_camsprite;
    int16_t last_level,secretlevel, bgstretch;

    struct {
        int32_t UseJoystick;
        int32_t UseMouse;
        int32_t AutoAim;
        int32_t ShowOpponentWeapons;
        int32_t MouseDeadZone,MouseBias;
        int32_t SmoothInput;

        // JBF 20031211: Store the input settings because
        // (currently) jmact can't regurgitate them
        int32_t MouseFunctions[MAXMOUSEBUTTONS][2];
        int32_t MouseDigitalFunctions[MAXMOUSEAXES][2];
        int32_t MouseAnalogueAxes[MAXMOUSEAXES];
        int32_t MouseAnalogueScale[MAXMOUSEAXES];
        int32_t JoystickFunctions[MAXJOYBUTTONS][2];
        int32_t JoystickDigitalFunctions[MAXJOYAXES][2];
        int32_t JoystickAnalogueAxes[MAXJOYAXES];
        int32_t JoystickAnalogueScale[MAXJOYAXES];
        int32_t JoystickAnalogueDead[MAXJOYAXES];
        int32_t JoystickAnalogueSaturate[MAXJOYAXES];
        uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];

        //
        // Sound variables
        //
        int32_t FXDevice;
        int32_t MusicDevice;
        int32_t FXVolume;
        int32_t MusicVolume;
        int32_t SoundToggle;
        int32_t MusicToggle;
        int32_t VoiceToggle;
        int32_t AmbienceToggle;

        int32_t NumVoices;
        int32_t NumChannels;
        int32_t NumBits;
        int32_t MixRate;

        int32_t ReverseStereo;

        //
        // Screen variables
        //

        int32_t ScreenMode;

        int32_t ScreenWidth;
        int32_t ScreenHeight;
        int32_t ScreenBPP;

        int32_t ForceSetup;
        int32_t NoAutoLoad;

        int32_t scripthandle;
        int32_t setupread;

        int32_t CheckForUpdates;
        int32_t LastUpdateCheck;
        int32_t useprecache;
    } config;

    char overhead_on,last_overhead,showweapons;
    char god,warp_on,cashman,eog,showallmap;
    char show_help,scrollmode,noclip;
    char ridecule[10][40];
    char savegame[10][22];
    char pwlockout[128],rtsname[128];
    char display_bonus_screen;
    char show_level_text;
} user_defs;
]])

decl[[
const char *g_sizes_of_what[];
int32_t g_sizes_of[];
actor_t actor[MAXSPRITES];
user_defs ud;
playerdata_t g_player[MAXPLAYERS];

const int32_t playerswhenstarted;
]]

-- functions
decl[[
int32_t kopen4loadfrommod(const char *filename, char searchfirst);
int32_t kfilelength(int32_t handle);
void kclose(int32_t handle);
int32_t kread(int32_t handle, void *buffer, int32_t leng);
]]

-- JKISS PRNG state struct and functions, see randgen module
ffi.cdef[[
typedef struct {
    uint32_t x, y, z, c;
} rng_jkiss_t;
]]
decl[[
uint32_t rand_jkiss_u32(rng_jkiss_t *s);
double rand_jkiss_dbl(rng_jkiss_t *s);

void md4once(const unsigned char *block, unsigned int len, unsigned char digest[16]);
]]

ffi.cdef[[
int32_t ksqrt(uint32_t num);
]]

ffi.cdef "double gethitickms(void);"



local assert = assert
local error = error
local ipairs = ipairs
local loadstring = loadstring
local pairs = pairs
local print = print
local rawget = rawget
local rawset = rawset
local setmetatable = setmetatable
local setfenv = setfenv
local type = type

local string = string
local table = table

-- http://lua-users.org/wiki/SandBoxes says "potentially unsafe"
-- as it allows to see implementations of functions.
--local string_dump = string.dump
string.dump = nil


-- sanity-check struct type sizes
for i=0,6 do
    assert(ffi.sizeof(ffi.string(ffiC.g_sizes_of_what[i]))
           == ffiC.g_sizes_of[i])
end


--== Custom operations for BUILD data structures ==--
-- declares struct action and struct move, and their ID-wrapped types
-- con_action_t and con_move_t
local con = require("control")

-- All-zero action and move
local nullac, nullmv = ffi.new("const struct action"), ffi.new("const struct move")

local function check_literal_am(am, typename)
    if (type(am) ~= "number") then
        error("bad argument: expected number or "..typename, 3)
    end

    if (not (am >= 0 and am <= 32767)) then
        error("bad argument: expected number in [0 .. 32767]", 3)
    end
end

local actor_ptr_ct = ffi.typeof("actor_u_t *")  -- an unrestricted actor_t pointer
local con_action_ct = ffi.typeof("con_action_t")
local con_move_ct = ffi.typeof("con_move_t")
local con_ai_ct = ffi.typeof("con_ai_t")

local actor_mt = {
    __index = {
        -- action
        set_action = function(a, act)
            a = ffi.cast(actor_ptr_ct, a)
            if (ffi.istype(con_action_ct, act)) then
                a.t_data[4] = act.id
                a.ac = act.ac
            else
                check_literal_am(act, "action")
                a.t_data[4] = act
                a.ac = nullac
            end

            a.t_data[2] = 0
            a.t_data[3] = 0
        end,

        has_action = function(a, act)
            a = ffi.cast(actor_ptr_ct, a)
            if (ffi.istype(con_action_ct, act)) then
                return (a.t_data[4]==act.id)
            else
                check_literal_am(act, "action")
                return (a.t_data[4]==act)
            end
        end,

        -- count
        set_count = function(a)
            ffi.cast(actor_ptr_ct, a).t_data[0] = 0
        end,

        get_count = function(a)
            return ffi.cast(actor_ptr_ct, a).t_data[0]
        end,

        -- action count
        reset_acount = function(a)
            ffi.cast(actor_ptr_ct, a).t_data[2] = 0
        end,

        get_acount = function(a)
            return ffi.cast(actor_ptr_ct, a).t_data[2]
        end,

        -- move
        set_move = function(a, mov, movflags)
            a = ffi.cast(actor_ptr_ct, a)
            if (ffi.istype(con_move_ct, mov)) then
                a.t_data[1] = mov.id
                a.mv = mov.mv
            else
                check_literal_am(mov, "move")
                a.t_data[1] = mov
                a.mv = nullmv
            end

            a.t_data[0] = 0
            local i = a-ffi.cast(actor_ptr_ct, ffiC.actor[0])
            ffiC.sprite[i].hitag = movflags or 0

            -- TODO: random angle moveflag
        end,

        has_move = function(a, mov)
            a = ffi.cast(actor_ptr_ct, a)
            if (ffi.istype(con_move_ct, mov)) then
                return (a.t_data[1]==mov.id)
            else
                check_literal_am(mov, "move")
                return (a.t_data[1]==mov)
            end
        end,

        -- ai
        set_ai = function(a, ai)
            local oa = a
            a = ffi.cast(actor_ptr_ct, a)

            -- TODO: literal number AIs?
            if (not ffi.istype(con_ai_ct, ai)) then
                error("bad argument: expected ai", 2)
            end

            -- NOTE: compare with gameexec.c
            a.t_data[5] = ai.id

            oa:set_action(ai.act)
            oa:set_move(ai.mov, ai.movflags)

            a.t_data[0] = 0
        end,

        has_ai = function(a, ai)
            a = ffi.cast(actor_ptr_ct, a)

            if (ffi.istype(con_ai_ct, ai)) then
                return (a.t_data[5]==ai.id)
            else
                check_literal_am(ai, "ai")
                return (a.t_data[5]==ai)
            end
        end,
    },
}
ffi.metatype("actor_t", actor_mt)


--- default defines
local con_lang = require("con_lang")

for i=1,#con_lang.labels do
    local strbuf = {"enum {"}

    for label, val in pairs(con_lang.labels[i]) do
        strbuf[#strbuf+1] = string.format("%s = %d,", label, val)
    end
    strbuf[#strbuf+1] = "};"

    ffi.cdef(table.concat(strbuf))
end


---=== Set up restricted global environment ===---

gv_tmp = gv_  -- required by randgen

local allowed_modules = {
    coroutine=coroutine, bit=bit, table=table, math=math, string=string,

    os = {
        clock = function() return gv_.gethitickms()*0.001 end,
    },

    randgen = require("randgen"),
    geom = require("geom"),
    stat = require("stat"),
    bitar = require("bitar"),

    con = con,
}

for modname, themodule in pairs(allowed_modules) do
    local mt = {
        __index = themodule,
        __newindex = function(tab,idx,val)
            error("modifying base module table forbidden", 2)
        end,
        __metatable = true,
    }

    -- Comment out to make base modules not protected:
    allowed_modules[modname] = setmetatable({}, mt)
end

local function check_valid_modname(modname, errlev)
    if (type(modname) ~= "string") then
        error("module name must be a string", errlev+1)
    end

    -- TODO: restrict valid names?
end

local function errorf(level, fmt, ...)
    local errmsg = string.format(fmt, ...)
    error(errmsg, level+1)
end


local package_loaded = {}
local modname_stack = {}
local ERRLEV = 5

local function readintostr(fn)
    -- XXX: this is pretty much the same as the code in El_RunOnce()

    local fd = ffiC.kopen4loadfrommod(fn, 0)  -- TODO: g_loadFromGroupOnly
    if (fd < 0) then
        errorf(ERRLEV, "Couldn't open file \"%s\"", fn)
    end

    local sz = ffiC.kfilelength(fd)
    if (sz == 0) then
        ffiC.kclose(fd)
        errorf(ERRLEV, "Didn't load module \"%s\": zero-length file", fn)
    end

    if (sz < 0) then
        ffi.kclose(fd)
        error("INTERNAL ERROR: kfilelength() returned negative length", 5)
    end

    local str = ffi.new("char [?]", sz)  -- XXX: what does it do on out of mem?
    local readlen = ffiC.kread(fd, str, sz)

    ffiC.kclose(fd); fd=-1

    if (readlen ~= sz) then
        errorf(ERRLEV, "INTERNAL ERROR: couldn't read \"%s\" wholly", fn)
    end

    return ffi.string(str, sz)
end

-- The "require" function accessible to Lunatic code.
-- Base modules in allowed_modules are wrapped so that they cannot be
-- modified, user modules are searched in the EDuke32 search
-- path.  Also, our require never messes with the global environment,
-- it only returns the module.
local function our_require(modname)
    check_valid_modname(modname, 2)

    -- see whether it's a base module name first
    if (allowed_modules[modname] ~= nil) then
        return allowed_modules[modname]
    end

    --- search user modules

    if (package_loaded[modname] ~= nil) then
        -- already loaded
        return package_loaded[modname]
    end

    -- TODO: better pattern-matching (permit "", ".lua", ".elua" ?)
    local str = readintostr(modname .. ".lua")

    local modfunc, errmsg = loadstring(str)
    if (modfunc == nil) then
        errorf(ERRLEV-1, "Couldn't load \"%s\": %s", modname, errmsg)
    end

    package_loaded[modname] = true
    table.insert(modname_stack, modname)

    -- Run the module code!
    modfunc(modname)  -- TODO: call protected and report errors here later

    table.remove(modname_stack)

    local modtab = package_loaded[modname]

    if (type(modtab) == "table") then
        -- Protect module table if there is one...
        local mt = {
            __index = modtab,
            __newindex = function(tab,idx,val)
                error("modifying module table forbidden", 2)
            end,
        }

        setmetatable(modtab, mt)
    end

    return modtab
end


-- _G tweaks -- pull in only 'safe' stuff
local G_ = {}  -- our soon-to-be global environment

local module_mt = {
    __index = function (_, n)
        error("attempt to read undeclared variable '"..n.."'", 2)
    end,
}

-- Our 'module' replacement doesn't get the module name from the function args
-- since a malicious user could remove other loaded modules this way.
-- TODO: make transactional?
local function our_module()
    local modname = modname_stack[#modname_stack]
    if (type(modname) ~= "string") then
        error("'module' must be called at the top level of a require'd file", 2)
    end

    local M = setmetatable({}, module_mt)
    package_loaded[modname] = M
    -- change the environment of the function which called us:
    setfenv(2, M)
end

-- overridden 'error' that always passes a string to the base 'error'
local function our_error(errmsg, level)
    if (type(errmsg) ~= "string") then
        error("error using 'error': error message must be a string", 2)
    end

    if (level) then
        if (type(level) ~= "number") then
            error("error using 'error': error level must be a number", 2)
        end

        error(errmsg, level==0 and 0 or level+1)
    end

    error(errmsg, 2)
end

G_.assert = assert
G_.error = our_error
G_.ipairs = ipairs
G_.pairs = pairs
G_.pcall = pcall
G_.print = print  -- TODO: --> initprintf or OSD_Printf; why not needed on linux?
G_.module = our_module
G_.next = next
G_.require = our_require
G_.select = select
G_.tostring = tostring
G_.tonumber = tonumber
G_.type = type
G_.unpack = unpack
G_.xpcall = xpcall
G_._VERSION = _VERSION

-- Available through our 'require':
-- bit, coroutine, math, string, table

-- Not available:
-- collectgarbage, debug, dofile, gcinfo (DEPRECATED), getfenv, getmetatable,
-- jit, load, loadfile, loadstring, newproxy (NOT STD?), package, rawequal,
-- rawget, rawset, setfenv, setmetatable

G_._G = G_

--- non-default data and functions
G_._EDUKE32_LUNATIC = _EDUKE32_LUNATIC
G_.gameevent = gameevent  -- included in lunatic.c
G_.gameactor = gameactor  -- included in lunatic.c


---=== Lunatic interpreter setup ===---

local lunacon = require("lunacon")


-- change the environment of this chunk to the table G_
-- NOTE: all references to global variables from this point on
-- (also in functions created after this point) refer to G_ !
setfenv(1, G_)

-- Print keys and values of a table.
-- REMEMBER special position of 'tostring' (it's looked up and used as a global
-- from 'print')
local function printkv(label, table)
    print('========== Keys and values of '..label)
    for k,v in pairs(table) do
        print(k .. ': ' .. tostring(v))
    end
    print('----------')
end

--printkv('_G AFTER SETFENV', _G)


---=== Restricted access to C variables from Lunatic ===---

-- error(..., 2) is to blame the caller and get its line numbers

-- set metatable and forbid setting it further
local function setmtonce(tab, mt)
    mt.__metatable = true
    return setmetatable(tab, mt)
end

local tmpmt = {
    __index = function() error('dummy variable: read access forbidden', 2) end,
    __newindex = function() error('dummy variable: write access forbidden', 2) end,
}
setmtonce(dummy_empty_table, tmpmt)

gv = gv_
local tmpmt = {
    __index = ffiC,
    __newindex = function() error("cannot create new or write into existing fields of 'gv'", 2) end,
}
setmtonce(gv, tmpmt)

---- indirect C array access ----
local tmpmt = {
    __index = function(tab, key)
                  if (key >= 0 and key < ffiC.numsectors) then return ffiC.sector[key] end
                  error('out-of-bounds sector[] read access', 2)
              end,

    __newindex = function(tab, key, val) error('cannot write directly to sector[] struct', 2) end,
}
sector = setmtonce({}, tmpmt)

local tmpmt = {
    __index = function(tab, key)
                  if (key >= 0 and key < ffiC.numwalls) then return ffiC.wall[key] end
                  error('out-of-bounds wall[] read access', 2)
              end,

    __newindex = function(tab, key, val) error('cannot write directly to wall[] struct', 2) end,
}
wall = setmtonce({}, tmpmt)

local tmpmt = {
    __index = function(tab, key)
                  if (key >= 0 and key < ffiC.playerswhenstarted) then
                      return ffiC.g_player[key].ps[0]
                  end
                  error('out-of-bounds player[] read access', 2)
              end,

    __newindex = function(tab, key, val) error('cannot write directly to player[] struct', 2) end,
}
player = setmtonce({}, tmpmt)

-- create a safe indirection for an ffi.C array
local function creategtab(ctab, maxidx, name)
    local tab = {}
    local tmpmt = {
        __index = function(tab, key)
                      if (key>=0 and key < maxidx) then
                          return ctab[key]
                      end
                      error('out-of-bounds '..name..' read access', 2)
                  end,
        __newindex = function(tab, key, val)
                         error('cannot write directly to '..name, 2)
                     end,
    }

    return setmtonce(tab, tmpmt)
end

sprite = creategtab(ffiC.sprite, ffiC.MAXSPRITES, 'sprite[] struct')
spriteext = creategtab(ffiC.spriteext, ffiC.MAXSPRITES, 'spriteext[] struct')
headspritesect = creategtab(ffiC.headspritesect, ffiC.MAXSECTORS, 'headspritesect[]')
-- TODO: allow sprite freelist access via the status list for CON compatibility?
headspritestat = creategtab(ffiC.headspritestat, ffiC.MAXSTATUS, 'headspritestat[]')
nextspritesect = creategtab(ffiC.nextspritesect, ffiC.MAXSPRITES, 'nextspritesect[]')
nextspritestat = creategtab(ffiC.nextspritestat, ffiC.MAXSPRITES, 'nextspritestat[]')
prevspritesect = creategtab(ffiC.prevspritesect, ffiC.MAXSPRITES, 'prevspritesect[]')
prevspritestat = creategtab(ffiC.prevspritestat, ffiC.MAXSPRITES, 'prevspritestat[]')

actor = creategtab(ffiC.actor, ffiC.MAXSPRITES, 'actor[]')

function TEMP_getvollev()  -- REMOVE
    return ffiC.ud.volume_number+1, ffiC.ud.level_number+1
end

---- per-sector/per-statnum sprite iterators ----
local function iter_spritesofsect(sect, i)
    if (i < 0) then
        i = ffiC.headspritesect[sect]
    else
        i = ffiC.nextspritesect[i]
    end

    if (i >= 0) then return i end
end

function spritesofsect(sect)
    if (sect < 0 or sect >= ffiC.numsectors) then
        error("passed invalid sectnum to spritesofsect iterator", 2)
    end

    return iter_spritesofsect, sect, -1
end

local function iter_spritesofstat(stat, i)
    if (i < 0) then
        i = ffiC.headspritestat[stat]
    else
        i = ffiC.nextspritestat[i]
    end

    if (i >= 0) then return i end
end

function spritesofstat(stat)
    if (stat < 0 or stat >= ffiC.MAXSTATUS) then
        error("passed invalid statnum to spritesofstat iterator", 2)
    end

    return iter_spritesofstat, stat, -1
end

-- TROR iterators
local function iter_sectorsofbunch(cf, i)
    if (i < 0) then
        i = ffiC.headsectbunch[cf][-i-1];
    else
        i = ffiC.nextsectbunch[cf][i];
    end

    if (i >= 0) then return i end
end

function sectorsofbunch(bunchnum, cf)
    if (bunchnum < 0 or bunchnum >= ffiC.numyaxbunches) then
        error("passed invalid bunchnum to sectorsofbunch iterator", 2)
    end
    if (cf ~= 0 and cf ~= 1) then
        error("passed invalid 'cf' to sectorsofbunch iterator, must be 0 or 1", 2)
    end

    return iter_sectorsofbunch, cf, -bunchnum-1
end

function getbunch(sectnum, cf)
    if (sectnum < 0 or sectnum >= ffiC.numsectors) then
        error('passed out-of-bounds sector number'..sectnum, 2)
    end
    if (cf ~= 0 and cf ~= 1) then
        error("passed invalid 'cf' to getbunch, must be 0 or 1", 2)
    end

    return ffiC.yax_getbunch(sectnum, cf)
end


---=== Engine functions, wrapped for Lua convenience ===---
-- returns a hitdata_ct
function hitscan(x,y,z, sectnum, vx,vy,vz, cliptype)
    if (sectnum < 0 or sectnum >= ffiC.numsectors) then
        error('passed out-of-bounds sector number'..sectnum, 2)
    end

    local vec = vec3_ct(x,y,z)
    local hitdata = hitdata_ct()

    ffiC.hitscan(vec, sectnum, vx,vy,vz, hitdata, cliptype)
    return hitdata
end

---=== Game variables ===---

-- gamevarNames[name] is true if that gamevar was declared
local gamevarNames = {}

-- code for prohibiting initial assignments to create new variables,
-- based on 14.2 of PiL
function gamevar(name, initval)  -- aka 'declare'
    if (type(name) ~= "string") then
        error("First argument to 'gamevar' must be a string", 2)
    end

    if (not string.match(name, "^[%a_][%w_]*$")) then
        error("First argument to 'gamevar' must be a valid identifier", 2)
    end

    if (string.match(name, "^_")) then
        error("Identifier names starting with an underscore are reserved", 2)
    end

    if (gamevarNames[name]) then
        error(string.format("Duplicate declaration of identifier '%s'", name), 2)
    end

    if (rawget(G_, name) ~= nil) then
        error(string.format("Identifier name '%s' is already used in the global environment", name), 2)
    end

    gamevarNames[name] = true
    rawset(G_, name, initval or false)
end


---=== Serialization, from PiL with modifications ===---
local function basicSerialize(o)
    if (type(o) == "number") then
        if (o ~= o) then return "0/0" end  -- nan
        if (o == 1/0) then return "1/0" end  -- inf
        if (o == -1/0) then return "-1/0" end  -- -inf

        return tostring(o)
    elseif (type(o) == "boolean") then
        return tostring(o)
    elseif (type(o) == "string") then
        return string.format("%q", o)
    end
end

-- will contain all gamevar tables as keys (and true as value)
local tmpgvtabs = {}

local function save(name, value, saved, strings)
    saved = saved or {}       -- initial value

    strings[#strings+1] = name .. "="

    local str = basicSerialize(value)

    if (str ~= nil) then
        strings[#strings+1] = str .. "\n"
    elseif (type(value) == "table") then
        if (saved[value]) then    -- value already saved?
            strings[#strings+1] = saved[value] .. "\n"  -- use its previous name
        else
            saved[value] = name   -- save name for next time
            strings[#strings+1] = "{}\n"  -- create a new table

            for k,v in pairs(value) do      -- save its fields
                local keystr = basicSerialize(k)
                if (keystr == nil) then
                    error("cannot save a " .. type(k) .. " as key of a table", 2);
                end
                local fieldname = string.format("%s[%s]", name, keystr)
                if (type(v)=="table" and not tmpgvtabs[v]) then
                    error("cannot save \""..name..
                          "\": gamevar tables may only contain tables that are also gamevars", 2)
                end
                save(fieldname, v, saved, strings)
            end
        end
    else
        error("cannot save \""..name.."\", a " .. type(value), 2)
    end
end

local function serializeGamevars()
    local saved = {}
    local strings = {}

    for gvname,_ in pairs(gamevarNames) do
        if (type(G_[gvname])=="table") then
            tmpgvtabs[G_[gvname]] = true
        end
    end

    -- TODO: catch errors in save()
    for gvname,_ in pairs(gamevarNames) do
        save(gvname, G_[gvname], saved, strings)
    end
    strings[#strings+1] = "\n"

    tmpgvtabs = {}

    return table.concat(strings)
end

local function loadGamevarsString(string)
--[=[
    for gvname,_ in pairs(gamevarNames) do
        G_[gvname] = nil;
    end
    gamevarNames = {};  -- clear gamevars
--]=]

    assert(loadstring(string))()
end


-- REMOVE this for release
DBG_ = {}
DBG_.printkv = printkv
DBG_.loadstring = loadstring
DBG_.serializeGamevars = serializeGamevars
DBG_.loadGamevarsString = loadGamevarsString


---=== Finishing environment setup ===---

--printkv('_G AFTER DECLS', _G)


-- PiL 14.2 continued
-- We need this at the end because we were previously doing just that!
setmetatable(
    G_, {
        __newindex = function (_1, n, _2)
            error("attempt to write to undeclared variable '"..n.."'", 2)
        end,
        __index = function (_, n)
            error("attempt to read undeclared variable '"..n.."'", 2)
        end,
        })

-- Change the environment of the running Lua thread so that everything
-- what we've set up will be available when this chunk is left.
-- In particular, we need the functions defined after setting this chunk's
-- environment earlier.
setfenv(0, _G)