-- INTERNAL -- definitions of BUILD and game types for the Lunatic Interpreter local require = require local ffi = require("ffi") local ffiC = ffi.C local bit = bit local string = string local table = table local math = math local assert = assert local error = error local getfenv = getfenv local getmetatable = getmetatable local ipairs = ipairs local loadstring = loadstring local pairs = pairs local rawget = rawget local rawset = rawset local select = select local setmetatable = setmetatable local setfenv = setfenv local tonumber = tonumber local tostring = tostring local type = type require("jit.opt").start("maxmcode=10240") -- in KiB -- The "gv" global will provide access to C global *scalars* and safe functions. -- NOTE: This exposes C library functions from e.g. the global C namespace, but -- without their declarations, they will be sitting there like a stone. local gv_ = {} -- [key]= forbids, [key]= overrides local gv_access = {} -- This is for declarations of arrays or pointers which should not be -- accessible through the "gv" global. The "defs_common" module will -- use this function. -- -- Notes: do not declare multiple scalars on one line (this is bad: -- "int32_t a, b"). Do not name array arguments (or add a space -- between the identifier and the '[' instead). function decl(str, ...) -- NOTE that the regexp also catches non-array/non-function identifiers -- like "user_defs ud;" for varname in string.gmatch(str, "([%a_][%w_]*)[[(;]") do if (ffiC._DEBUG_LUNATIC ~= 0) then print("FORBID "..varname) end gv_access[varname] = true end ffi.cdef(str, ...) end -- Load the definitions common to the game's and editor's Lua interface. local defs_c = require("defs_common") local cansee = defs_c.cansee local strip_const = defs_c.strip_const local setmtonce = defs_c.setmtonce -- Must be after loading "defs_common" which redefines "print" to use -- OSD_Printf() local print, printf = print, defs_c.printf ---=== EDuke32 game definitions ===--- local INV_NAMES = { "STEROIDS", "SHIELD", "SCUBA", "HOLODUKE", "JETPACK", "DUMMY1", "ACCESS", "HEATS", "DUMMY2", "FIRSTAID", "BOOTS", } local WEAPON_NAMES = { "KNEE", "PISTOL", "SHOTGUN", "CHAINGUN", "RPG", "HANDBOMB", "SHRINKER", "DEVISTATOR", "TRIPBOMB", "FREEZE", "HANDREMOTE", "GROW", } ---- game structs ---- ffi.cdef([[ enum dukeinv_t { ]].. "GET_"..table.concat(INV_NAMES, ",GET_") ..[[, GET_MAX }; enum dukeweapon_t { ]].. table.concat(WEAPON_NAMES, "_WEAPON,").."_WEAPON," ..[[ MAX_WEAPONS }; enum { MAXPLAYERS = 16, }; ]]) ffi.cdef[[ struct action { int16_t startframe, numframes; int16_t viewtype, incval, delay; }; struct move { int16_t hvel, vvel; }; #pragma pack(push,1) typedef struct { int32_t id; struct move mv; } con_move_t; typedef struct { int32_t id; struct action ac; } con_action_t; #pragma pack(pop) typedef struct { int32_t id; con_action_t act; con_move_t mov; int32_t movflags; } con_ai_t; ]] defs_c.bitint_new_struct_type("int32_t", "SBit32") -- Struct template for actor_t. It already has 'const' fields (TODO: might need -- to make more 'const'), but still has array members exposed, so is unsuited -- for external exposure. local ACTOR_STRUCT = [[ __attribute__((packed)) struct { 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[1]; ]]..defs_c.bitint_member("SBit32", "flags")..[[ vec3_t bpos; //12b int32_t floorz,ceilingz,lastvx,lastvy; //16b int32_t lasttransport; //4b const int16_t picnum; int16_t ang, extra; const int16_t owner; int16_t movflag,tempang,timetosleep; //6b int16_t actorstayput; const int16_t dispicnum; int16_t shootzvel, cgg; const int16_t lightId, lightcount, lightmaxrange; const union { intptr_t ptr; uint64_t dummy; } _light; } ]] local bcarray = require("bcarray") local bcheck = require("bcheck") local check_sector_idx, check_tile_idx = bcheck.sector_idx, bcheck.tile_idx local check_sprite_idx = bcheck.sprite_idx local check_weapon_idx, check_inventory_idx = bcheck.weapon_idx, bcheck.inventory_idx local check_sound_idx = bcheck.sound_idx bcarray.new("int16_t", 64, "loogie", "int16_x_64") -- TODO: randomize member names bcarray.new("int16_t", ffiC.MAX_WEAPONS, "weapon", "int16_x_MAX_WEAPONS", WEAPON_NAMES) bcarray.new("int16_t", ffiC.GET_MAX, "inventory", "int16_x_GET_MAX", INV_NAMES) -- NOTE: writing e.g. "ps.jetpack_on" in Lua when "ps.jetpack_on~=0" was meant -- is probably one of the most commonly committed errors, so we make it a bool -- type instead of uint8_t. The only issue is that if CON coders used these -- fields to store more than just one bit, we're in trouble. -- This will need to be documented and frozen for release. local DUKEPLAYER_STRUCT = [[ __attribute__((packed)) 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; int32_t autostep, autostep_sbw; uint32_t interface_toggle_flag; int32_t pipebombControl, pipebombLifetime, pipebombLifetimeVar; int32_t tripbombControl, tripbombLifetime, tripbombLifetimeVar; int32_t zrange; int16_t angrange, autoaimang; uint16_t max_actors_killed, actors_killed; uint16_t gotweapon, zoom; int16_x_64 loogiex; int16_x_64 loogiey; int16_t sbs, sound_pitch; int16_t ang, oang, angvel; const int16_t cursectnum; int16_t look_ang, last_extra, subweapon; int16_x_MAX_WEAPONS max_ammo_amount; int16_x_MAX_WEAPONS ammo_amount; int16_x_GET_MAX inv_amount; int16_t wackedbyactor, pyoff, opyoff; int16_t horiz, horizoff, ohoriz, ohorizoff; const int16_t newowner; int16_t jumping_counter, airleft; int16_t fta; const int16_t ftq, access_wallnum, access_spritenum; int16_t got_access, weapon_ang, visibility; int16_t somethingonplayer, on_crane; const int16_t i; const int16_t one_parallax_sectnum; int16_t random_club_frame, one_eighty_count; const int16_t dummyplayersprite; int16_t extra_extra8; int16_t actorsqu, timebeforeexit; const int16_t customexitsound; int16_t last_pissed_time; int16_x_MAX_WEAPONS weaprecs; 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; bool reloading; const uint8_t weapreccnt; uint8_t aim_mode, auto_aim, weaponswitch, movement_lock, team; uint8_t tipincs, hbomb_hold_delay, frag_ps, kickback_pic; uint8_t gm; bool on_warping_sector; uint8_t footprintcount, hurt_delay; bool hbomb_on, jumping_toggle, rapid_fire_hold, on_ground; // NOTE: there's array indexing with inven_icon, but always after a // bound check: uint8_t inven_icon, buttonpalette; bool over_shoulder_on; uint8_t show_empty_weapon; bool jetpack_on, spritebridge; uint8_t lastrandomspot; // unused bool scuba_on; uint8_t footprintpal; bool heat_on; uint8_t invdisptime; bool holster_weapon; uint8_t falling_counter, footprintshade; uint8_t refresh_inventory, last_full_weapon; bool toggle_key_flag; uint8_t 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; const int8_t curr_weapon; uint8_t palette; palette_t _pals; // NOTE: In C, the struct type has no name. We only have it here to define // a metatype later. const weaponaccess_t weapon; const int8_t _padding; } ]] local PROJECTILE_STRUCT = [[ struct { int32_t workslike, cstat; int32_t hitradius, range, flashcolor; const int16_t spawns; int16_t sound, isound, vel; const int16_t decal, trail; int16_t tnum, drop; int16_t offset, bounces, bsound; int16_t toffset; int16_t extra, extra_rand; int8_t sxrepeat, syrepeat, txrepeat, tyrepeat; int8_t shade, xrepeat, yrepeat, pal; int8_t movecnt; uint8_t clipdist; int8_t filler[6]; } ]] -- KEEPINSYNC weapondata_mt below. local WEAPONDATA_STRUCT = "struct {"..table.concat(con_lang.wdata_members, ';').."; }" local randgen = require("randgen") local geom = require("geom") local ma_rand = randgen.new(true) -- initialize to "random" (time-based) seed local ma_count = nil local function ma_replace_array(typestr, neltstr) local nelts = tonumber(neltstr) if (nelts==nil) then nelts = ffiC[neltstr] assert(type(nelts)=="number") end local strtab = { "const ", typestr.." " } for i=1,nelts do local ch1 = 97 + (ma_rand:getu32() % 25) -- 'a'..'z' strtab[i+2] = string.format("_%c%x%s", ch1, ma_count, (i= 0 and am <= 32767)) then error("bad argument: expected number in [0 .. 32767]", 3) end end -- An unrestricted actor_t pointer, for internal use: local actor_ptr_ct = ffi.typeof("$ *", ffi.typeof(strip_const(ACTOR_STRUCT))) local player_ptr_ct = ffi.typeof("$ *", ffi.typeof(strip_const(DUKEPLAYER_STRUCT))) local projectile_ptr_ct = ffi.typeof("$ *", ffi.typeof(strip_const(PROJECTILE_STRUCT))) local weapondata_ptr_ct = ffi.typeof("$ *", ffi.typeof((strip_const(WEAPONDATA_STRUCT):gsub(" _"," ")))) 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 function get_actor_idx(a) local i = ffi.cast(actor_ptr_ct, a)-ffi.cast(actor_ptr_ct, ffiC.actor) assert(not (i >= ffiC.MAXSPRITES+0ULL)) return i end local actor_mt = { __index = { -- action set_action = function(a, act) a = ffi.cast(actor_ptr_ct, a) if (type(act)=="string") then act = AC[act]; end -- TODO: disallow passing the FFI types altogether, in favor of -- strings (also move, ai)? 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 (type(act)=="string") then act = AC[act]; end 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 (type(mov)=="string") then mov = MV[mov]; end 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 = get_actor_idx(a) 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 (type(mov)=="string") then mov = MV[mov]; end 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) if (type(ai)=="string") then ai = AI[ai]; end -- 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 (type(ai)=="string") then ai = AI[ai]; end 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, -- Getters/setters. -- TODO: make a bcarray instead. get_t_data = function(a, idx) if (idx >= 10ULL) then error("Invalid t_data index "..idx, 2) end return ffi.cast(actor_ptr_ct, a).t_data[idx] end, _set_t_data = function(a, idx, val) if (idx >= 10ULL) then error("Invalid t_data index "..idx, 2) end ffi.cast(actor_ptr_ct, a).t_data[idx] = val end, set_picnum = function(a, picnum) if (picnum >= 0) then check_tile_idx(picnum) end ffi.cast(actor_ptr_ct, a).picnum = picnum end, set_owner = function(a, owner) -- XXX: is it permissible to set to -1? check_sprite_idx(owner) ffi.cast(actor_ptr_ct, a).owner = owner end, }, } local function actor_index_index(a, key) -- actor[].proj gets an actor's projectile ([gs]etthisprojectile in CON) if (key=="proj") then return ffiC.SpriteProjectile[get_actor_idx(a)] end end setmtonce(actor_mt.__index, { __index = actor_index_index }) ffi.metatype("actor_t", actor_mt) --- PER-PLAYER WEAPON SETTINGS local weapondata_mt = { __index = function(wd, member) -- Handle protected members that are renamed (e.g. shoots/_shoots). return ffi.cast(weapondata_ptr_ct, wd)[member] end, __newindex = function(wd, member, val) if (string.match(member, "sound")) then if (val < 0) then val = 0 -- Set to 0 if oob (e.g. CrackDown). else check_sound_idx(val) end elseif (member=="workslike") then check_weapon_idx(val) elseif (member=="spawn" or member=="shoots") then if (val < 0) then -- Set to 0 if oob (e.g. CrackDown). This is a bit problematic -- for .shoots because it's used unconditionally except in one -- case (see player.c). val = 0 else check_tile_idx(val) end end ffi.cast(weapondata_ptr_ct, wd)[member] = val end, } ffi.metatype("weapondata_t", weapondata_mt) local weaponaccess_mt = { -- Make syntax like "player[0].weapon.KNEE.shoots" possible __index = function(wa, key) if (type(key)=="number") then check_weapon_idx(key) return ffiC.g_playerWeapon[wa._p][key] end -- TODO: use bcarray? return ffiC.g_playerWeapon[wa._p][ffiC[key.."_WEAPON"]] end, } ffi.metatype("weaponaccess_t", weaponaccess_mt) local player_mt = { __index = { --- Getters/setters get_ammo_amount = function(p, weap) return p.ammo_amount[weap] end, set_ammo_amount = function(p, weap, amount) p.ammo_amount[weap] = amount end, get_max_ammo_amount = function(p, weap) return p.max_ammo_amount[weap] end, set_max_ammo_amount = function(p, weap, amount) p.max_ammo_amount[weap] = amount end, set_curr_weapon = function(p, weap) check_weapon_idx(weap) ffi.cast(player_ptr_ct, p).curr_weapon = weap end, get_inv_amount = function(p, inv) return p.inv_amount[inv] end, set_inv_amount = function(p, inv, amount) p.inv_amount[inv] = amount end, set_ftq = function(p, ftq) bcheck.quote_idx(ftq) ffi.cast(player_ptr_ct, p).ftq = ftq end, set_cursectnum = function(p, sectnum) check_sector_idx(sectnum) ffi.cast(player_ptr_ct, p).cursectnum = sectnum end, set_customexitsound = function(p, soundnum) check_sound_idx(soundnum) ffi.cast(player_ptr_ct, p).customexitsound = soundnum end, -- CON-like addammo/addweapon, but without the non-local control flow -- (returns true if weapon's ammo was at the max. instead). addammo = con._addammo, addweapon = con._addweapon, stomp = con._pstomp, have_weapon = function(p, weap) return (bit.band(p.gotweapon, bit.lshift(1, weap)) ~= 0) end, give_weapon = function(p, weap) p.gotweapon = bit.bor(p.gotweapon, bit.lshift(1, weap)) end, take_weapon = function(p, weap) p.gotweapon = bit.band(p.gotweapon, bit.bnot(bit.lshift(1, weap))) end, -- Give or take weapon, for implementation of CON's .gotweapon access. _gt_weapon = function(p, weap, give_p) if (give_p ~= 0) then p:give_weapon(weap) else p:take_weapon(weap) end end, -- XXX: is the correct spelling "whack"? wack = function(p, no_return_to_center) p.horiz = p.horiz + 64 if (not no_return_to_center) then p.return_to_center = 9 end local n = bit.arshift(128-bit.band(ffiC.krand(),255), 1) p.rotscrnang = n p.look_ang = n end, --- Not fully specified, off-limits to users. Might disappear, change --- signature, etc... -- TODO: need to think about external API: 0-255 based? 0-1 based? _palfrom = function(p, f, r,g,b) local pals = p._pals pals.f = f pals.r, pals.g, pals.b = r, g, b end, }, } local function player_index_index(p, key) if (key=="_input") then return ffiC.g_player[p.weapon._p].sync[0] end end setmtonce(player_mt.__index, { __index = player_index_index }) ffi.metatype("DukePlayer_t", player_mt) local function GenProjectileSetFunc(Member) return function(self, picnum) if (picnum >= 0) then check_tile_idx(picnum) end ffi.cast(projectile_ptr_ct, self)[Member] = picnum end end local projectile_mt = { __index = { set_spawns = GenProjectileSetFunc "spawns", set_decal = GenProjectileSetFunc "decal", set_trail = GenProjectileSetFunc "trail", }, } ffi.metatype("projectile_t", projectile_mt) local user_defs_mt = { __index = { set_screen_size = function(ud, screen_size) -- TODO: modify .statusbarmode accordingly ud.screen_size = screen_size end, set_volume_number = function(ud, volume_number) -- XXX: should volume_number==MAXVOLUMES be allowed too? if (volume_number >= con_lang.MAXVOLUMES+0ULL) then error("invalid volume number "..volume_number, 2) end ud.volume_number = volume_number end, }, } ffi.metatype("user_defs", user_defs_mt) --- CUSTOM "gv" VARIABLES local camera_mt = { -- TODO: "set position" method, which also updates the sectnum __index = ffiC.g_camera, __newindex = function(_, key, val) if (key=="sect") then check_sector_idx(val) end ffiC.g_camera[key] = val end, } gv_access.cam = setmtonce({}, camera_mt) gv_access._ud = ffiC.ud -- Support for some CON global system gamevars. RETURN handled separately. gv_access._csv = ffi.new "struct { int32_t LOTAG, HITAG, TEXTURE; }" function gv_access._get_yxaspect() return ffiC.yxaspect end function gv_access._get_viewingrange() return ffiC.viewingrange end function gv_access._currentFramerate() return ffiC.g_currentFrameRate end function gv_access._currentMenu() return ffiC.g_currentMenu end function gv_access._set_guniqhudid(id) local MAXUNIQHUDID = 256 -- KEEPINSYNC build.h if (id >= MAXUNIQHUDID+0ULL) then error("invalid unique HUD ID "..id) end ffiC.guniqhudid = id end -- TODO: make return 1-based index function gv_access.currentEpisode() return ffiC.ud.volume_number end function gv_access.currentLevel() return ffiC.ud.level_number end function gv_access.currentRenderMode() -- TODO: USE_OPENGL=0 build return ffiC.rendmode end function gv_access.doQuake(gametics, snd) ffiC.g_earthquakeTime = gametics if (snd ~= nil) then con._globalsound(ffiC.screenpeek, snd) end end -- Declare all con_lang.labels constants in the global FFI namespace. for i=1,#con_lang.labels do if (getmetatable(con_lang.labels[i]) ~= "noffiC") then 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 end ---=== Set up restricted global environment ===--- local allowed_modules = { coroutine=coroutine, bit=bit, table=table, math=math, string=string, os = { clock = function() return gv_.gethitickms()*0.001 end, }, randgen = randgen, geom = geom, stat = require("stat"), bitar = require("bitar"), xmath = require("xmath"), con = con, } -- Protect base modules. do local function basemodule_newindex() error("modifying base module table forbidden", 2) end for modname, themodule in pairs(allowed_modules) do local mt = { __index = themodule, __newindex = basemodule_newindex, } allowed_modules[modname] = setmtonce({}, mt) end end local package_loaded = {} -- false/true/table local modname_stack = {} -- []=string local module_gamevars = {} -- [] = { , , ... } local function getcurmodname(thisfuncname) if (#modname_stack == 0) then error("'"..thisfuncname.."' must be called at the top level of a require'd file", 3) -- ... as opposed to "at runtime". end return modname_stack[#modname_stack] end local function errorf(level, fmt, ...) local errmsg = string.format(fmt, ...) error(errmsg, level+1) end local ERRLEV = 5 local function readintostr(fn) -- XXX: this is pretty much the same as the code in L_RunOnce() local fd = ffiC.kopen4loadfrommod(fn, 0) -- TODO: g_loadFromGroupOnly if (fd < 0) then return nil end local sz = ffiC.kfilelength(fd) if (sz == 0) then ffiC.kclose(fd) return "" end if (sz < 0) then ffi.kclose(fd) error("INTERNAL ERROR: kfilelength() returned negative length", ERRLEV) end local str = ffi.new("char [?]", sz) 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 local required_module_mt = { __newindex = function() -- TODO: allow gamevars to be nil? error("modifying module table forbidden", 2) end, __metatable = true, } -- 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. -- * allows passing varargs beyond the name to the module. local function our_require(modname, ...) -- Check module name is valid first. -- TODO: restrict valid names? if (type(modname) ~= "string") then error("module name must be a string", 2) end -- Handle the section between module() and require("end_gamevars"). if (modname == "end_gamevars") then local thismodname = getcurmodname("require") if (module_gamevars[thismodname] ~= nil) then error("\"require 'end_gamevars'\" must be called at most once per require'd file", 2) end local gvnames = {} for name in pairs(getfenv(2)) do gvnames[#gvnames] = name if (ffiC._DEBUG_LUNATIC ~= 0) then printf("MODULE %s GAMEVAR %s", thismodname, name) end end module_gamevars[thismodname] = gvnames return end -- See whether it's a base module name. if (allowed_modules[modname] ~= nil) then return allowed_modules[modname] end --- Search user modules... local omod = package_loaded[modname] if (omod ~= nil) then if (omod==false) then error("Loop while loading modules", ERRLEV-1) end -- already loaded assert(omod==true or type(omod)=="table") return omod end local modfn = modname .. ".lua" local str = readintostr(modfn) if (str == nil) then errorf(ERRLEV-1, "Couldn't open file \"%s\"", modfn) end local modfunc, errmsg = loadstring(str) if (modfunc == nil) then errorf(ERRLEV-1, "Couldn't load \"%s\": %s", modname, errmsg) end package_loaded[modname] = false -- 'not yet loaded' 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... setmetatable(modtab, required_module_mt) else package_loaded[modname] = true end return modtab end 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. -- Also, our 'module' takes no varargs ("option functions" in Lua). -- TODO: make transactional? local function our_module() if (#modname_stack == 0) then error("'module' must be called at the top level of a require'd file", 2) -- ... as opposed to "at runtime". end local modname = getcurmodname("module") if (type(package_loaded[modname])=="table") then error("'module' must be called at most once per 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 tweaks -- pull in only 'safe' stuff local G_ = {} -- our soon-to-be global environment G_.assert = assert G_.error = our_error G_.ipairs = ipairs G_.pairs = pairs G_.pcall = pcall G_.print = print 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_ -- Chain together two functions taking 3 input args. local function chain_func3(func1, func2) if (func1==nil or func2==nil) then return assert(func1 or func2) end -- Return a function that runs first and then tail-calls . return function(aci, pli, dist) func1(aci, pli, dist) return func2(aci, pli, dist) end end -- Actor functions, saved for actor chaining local actor_funcs = {} local gameactor_internal = gameactor_internal -- included in lunatic.c -- gameactor(tilenum [, flags [, strength [, act [, mov [, movflags]]]]], actor_func) local function our_gameactor(tilenum, ...) bcheck.top_level("gameactor") if (type(tilenum) ~= "number") then error("invalid argument #1 to gameactor: must be a number", 2) end if (tilenum >= ffiC.MAXTILES+0ULL) then error("invalid argument #1 to gameactor: must be a tile number [0..gv.MAXTILES-1]", 2) end local args = {...} -- Number of varargs; args may have nil's in the middle! local numargs = select('#', ...) if (numargs == 0) then error("invalid call to gameactor: must have at least two arguments (tilenum, func)", 2) end if (numargs >= 7) then -- sic, because tilenum isn't counted! error("invalid call to gameactor: must have at most 7 arguments", 2) end if (type(args[numargs]) ~= "function") then error("invalid last argument to gameactor: must be a function", 2) end local flags = (numargs > 1) and args[1] or 0 if (type(flags) ~= "number") then error("invalid 'flags' argument to gameactor: must be a number", 2) end local AF = actor.FLAGS local chainflags = bit.band(flags, AF.replace) flags = bit.band(flags, BNOT_CHAIN_MASK) -- Default chaining behavior: don't, replace the old actor instead (like CON). if (chainflags == 0) then chainflags = AF.replace end local replacep = (chainflags==AF.replace) if (not replacep and not actor_funcs[tilenum]) then error("attempt to chain code to nonexistent actor tile "..tilenum, 2) end local flags_rbits = bit.band(flags, BNOT_SFLAG_USER_MASK) if (flags_rbits ~= 0) then error("invalid 'flags' argument to gameactor: must not set reserved bits (0x" ..(bit.tohex(flags_rbits))..")", 2) end local strength = (numargs > 2) and args[2] or (replacep and 0 or nil) if (replacep or strength~=nil) then if (type(strength) ~= "number") then error("invalid 'strength' argument to gameactor: must be a number", 2) end end -- TODO: literal number action other than 0? local act = (numargs > 3) and args[3] or (replacep and "NO" or nil) if (replacep or act ~= nil) then if (type(act)=="string") then act = AC[act] end if (not ffi.istype(con_action_ct, act)) then error("invalid 'act' argument to gameactor: must be a string or action", 2) end end -- TODO: literal number move other than 0? local mov = (numargs > 4) and args[4] or (replacep and "NO" or nil) if (replacep or mov ~= nil) then if (type(mov)=="string") then mov = MV[mov] end if (not ffi.istype(con_move_ct, mov)) then error("invalid 'mov' argument to gameactor: must be a string or move", 2) end end local movflags = (numargs > 5) and args[5] or (replacep and 0 or nil) if (replacep or movflags ~= nil) then if (type(movflags) ~= "number") then error("invalid 'movflags' argument to gameactor: must be a number", 2) end end -- All good, set the tile bits and register the actor! local tile = ffiC.g_tile[tilenum] local func = args[numargs] -- NOTE: when chaining, this allows passing different flags which are -- silently ORed. This may or may not be what the user intends, but it -- allows e.g. adding a stayput bit to an already defined enemy. -- Modifying existing behavior is the whole point of chaining after all. tile.flags = replacep and flags or bit.bor(tile.flags, flags) local newfunc = replacep and func or (chainflags==AF.chain_beg) and chain_func3(func, actor_funcs[tilenum]) or (chainflags==AF.chain_end) and chain_func3(actor_funcs[tilenum], func) or assert(false) gameactor_internal(tilenum, strength, act, mov, movflags, newfunc) actor_funcs[tilenum] = newfunc end -- Event functions, saved for event chaining local event_funcs = {} local gameevent_internal = gameevent_internal -- included in lunatic.c -- gameevent( [, flags], ) local function our_gameevent(event, flags, func) if (ffiC.g_elCallDepth > 0) then error("Invalid use of gameevent: must be called from top level", 2) end if (type(event) == "string") then if (event:sub(1,6) ~= "EVENT_") then event = "EVENT_"..event end local eventidx = con_lang.EVENT[event] if (eventidx == nil) then errorf(2, "gameevent: invalid event label %q", event) end event = eventidx end if (type(event) ~= "number") then error("invalid argument #1 to gameevent: must be a number or event label", 2) end if (event >= con_lang.MAXEVENTS+0ULL) then error("invalid argument #1 to gameevent: must be an event number (0 .. MAXEVENTS-1)", 2) end local AF = actor.FLAGS -- Event chaining: in Lunatic, chaining at the *end* is the default. if (func==nil) then func = flags flags = AF.chain_end else if (type(flags) ~= "number") then error("invalid 'flags' argument to gameevent: must be a number", 2) end if (bit.band(flags, BNOT_CHAIN_MASK) ~= 0) then error("invalid 'flags' argument to gameevent: must not set reserved bits", 2) end if (flags == 0) then flags = AF.chain_end end end if (type(func) ~= "function") then error("invalid last argument to gameevent: must be a function", 2) end local newfunc = (flags==AF.replace) and func or (flags==AF.chain_beg) and chain_func3(func, event_funcs[event]) or (flags==AF.chain_end) and chain_func3(event_funcs[event], func) or assert(false) gameevent_internal(event, newfunc) event_funcs[event] = newfunc end --- non-default data and functions G_.gameevent = our_gameevent G_.gameactor = our_gameactor -- These come from above: G_.player = player G_.actor = actor G_.projectile = projectile G_.g_tile = g_tile ---=== Lunatic interpreter setup ===--- local concode --- Compile CONs do read_into_string = readintostr -- for lunacon local lunacon = require("lunacon") local confn = { ffi.string(ffiC.G_ConFile()) } local nummods = ffiC.g_scriptModulesNum if (nummods > 0) then assert(ffiC.g_scriptModules ~= nil) for i=1,nummods do confn[i+1] = ffi.string(ffiC.g_scriptModules[i-1]) end end local lineinfo concode, lineinfo = lunacon.compile(confn) if (concode == nil) then error("Failure compiling CON code, exiting.", 0) end assert(lineinfo) -- Translate one Lua line number to a CON file name + line number local function transline(lnum) return string.format("%s:%d", lineinfo:getfline(tonumber(lnum))) end -- Register the function that tweaks an error message, looking out for -- errors from CON and translating the line numbers. local function tweak_traceback_msg(errmsg) return errmsg:gsub('%[string "CON"%]:([0-9]+)', transline) end set_tweak_traceback_internal(tweak_traceback_msg) end -- 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. local function printkv(label, table) print("========== Keys and values of "..label.." ("..tostring(table)..")") 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 local tmpmt = { __index = function(_, key) if (gv_access[key] == nil) then return ffiC[key] end if (type(gv_access[key])~="boolean") then -- overridden... return gv_access[key] end error("access forbidden", 2) end, __newindex = function(_, key, val) if (gv_access[key] == nil) then -- Read-only vars handled by LuaJIT. ffiC[key] = val else error("write access forbidden", 2) end end, } gv = setmtonce(gv_, tmpmt) -- This will create 'sprite', 'wall', etc. HERE, i.e. in the environment of this chunk defs_c.create_globals(_G) ---=== 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_.debug = require("debug") DBG_.printkv = printkv DBG_.loadstring = loadstring DBG_.serializeGamevars = serializeGamevars DBG_.loadGamevarsString = loadGamevarsString DBG_.oom = function() local s = "1" for i=1,math.huge do s = s..s end end -- Test reading from all struct members function DBG_.testmembread() for _1, sac in pairs { con_lang.StructAccessCode, con_lang.StructAccessCode2 } do for what, labels in pairs(sac) do if (what~="tspr") then for _3, membaccode in pairs(labels) do if (type(membaccode)=="table") then membaccode = membaccode[1] end if (membaccode) then local codestr = "do local _bit,_math=require'bit',require'math'; local tmp=".. membaccode:gsub("%%s","0").." end" local code = assert(loadstring(codestr)) code() end end end end end end ---=== 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, __metatable = true, }) -- 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 globals defined after setting this chunk's -- environment earlier. setfenv(0, _G) -- Finally, run the CON code translated into Lua. if (concode) then local confunc, conerrmsg = loadstring(concode, "CON") if (confunc == nil) then error("Failure loading translated CON code: "..conerrmsg, 0) end confunc() end