-- 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, 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; ]] 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); ]] -- 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]; ]] -- 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("con") -- All-zero action and move local nullac, nullmv = ffi.new("const struct action"), ffi.new("const struct move") local function check_literal_am(am) if (type(am) ~= "number") then error("bad argument: expected number", 3) end if (not (am >= 0 and am <= 32767)) then error("bad argument: expected number in [0 .. 32767]", 3) end end local actor_mt = { __index = { -- action set_action = function(a, act) a = ffi.cast("actor_u_t *", a) if (ffi.istype("con_action_t", act)) then a.t_data[4] = act.id a.ac = act.ac else check_literal_am(act) 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_u_t *", a) if (ffi.istype("con_action_t", act)) then return (a.t_data[4]==act.id) else check_literal_am(act) return (a.t_data[4]==act) end end, -- count set_count = function(a) ffi.cast("actor_u_t *", a).t_data[0] = 0 end, get_count = function(a) return ffi.cast("actor_u_t *", a).t_data[0] end, -- action count reset_acount = function(a) ffi.cast("actor_u_t *", a).t_data[2] = 0 end, get_acount = function(a) return ffi.cast("actor_u_t *", a).t_data[2] end, -- move set_move = function(a, mov, movflags) a = ffi.cast("actor_u_t *", a) if (ffi.istype("con_move_t", mov)) then a.t_data[1] = mov.id a.mv = mov.mv else check_literal_am(mov) a.t_data[1] = mov a.mv = nullmv end a.t_data[0] = 0 local i = a-ffi.cast("actor_u_t *", ffiC.actor[0]) ffiC.sprite[i].hitag = movflags or 0 -- TODO: random angle moveflag end, has_move = function(a, mov) a = ffi.cast("actor_u_t *", a) if (ffi.istype("con_move_t", mov)) then return (a.t_data[1]==mov.id) else check_literal_am(mov) return (a.t_data[1]==mov) end end, -- ai set_ai = function(a, ai) local oa = a a = ffi.cast("actor_u_t *", a) -- TODO: literal number AIs? assert(ffi.istype("con_ai_t", ai)) -- 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_u_t *", a) if (ffi.istype("con_ai_t", ai)) then return (a.t_data[5]==ai.id) else check_literal_am(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) -- 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)