-- 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 ipairs = ipairs local loadstring = loadstring local pairs = pairs local rawget = rawget local rawset = rawset local setmetatable = setmetatable local setfenv = setfenv local tonumber = tonumber local type = type local print = print local tostring = tostring --== First, load the definitions common to the game's and editor's Lua interface. -- The "gv" global will provide access to C global *scalars* and safe functions. -- XXX: still exposes C library functions etc. contained in ffi.C, 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. The "defs_common" module will -- use this function. -- -- Notes: do not declare multiple pointers 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 -- print("DUMMY "..varname) gv_[varname] = dummy_empty_table end ffi.cdef(str) end -- load the common definitions! local defs_c = require("defs_common") local cansee = defs_c.cansee local strip_const = defs_c.strip_const local setmtonce = defs_c.setmtonce ---=== EDuke32 game definitions ===--- ---- 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; #pragma pack(pop) ]] -- 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 = [[ { 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]; int32_t flags; //4b 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 const int16_t lightId, lightcount, lightmaxrange, cgg; //8b int16_t actorstayput; const int16_t dispicnum; int16_t shootzvel; const int8_t _do_not_use[8]; } ]] local DUKEPLAYER_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; uint8_t palette; uint16_t max_actors_killed, actors_killed; uint16_t gotweapon, zoom; const int16_t loogiex[64]; const int16_t loogiey[64]; int16_t sbs, sound_pitch; int16_t ang, oang, angvel; const int16_t cursectnum; int16_t look_ang, last_extra, subweapon; const int16_t max_ammo_amount[MAX_WEAPONS]; const int16_t ammo_amount[MAX_WEAPONS]; const int16_t inv_amount[GET_MAX]; int16_t wackedbyactor, pyoff, opyoff; int16_t horiz, horizoff, ohoriz, ohorizoff; const int16_t newowner; int16_t jumping_counter, airleft; int16_t fta, 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; const 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; int8_t const curr_weapon; palette_t pals; const char name[32]; const int8_t padding_[1]; } ]] 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 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) --== 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") local MV, AC, AI = con.MV, con.AC, con.AI -- 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 player_ptr_ct = ffi.typeof("DukePlayer_u_t *") 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 (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 = 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 (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. 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, }, } ffi.metatype("actor_t", actor_mt) --- default defines etc. local con_lang = require("con_lang") local function check_weapon_idx(weap) if (weap >= ffiC.MAX_WEAPONS+0ULL) then error("Invalid weapon ID "..weap, 3) end end local function check_inventory_idx(inv) if (inv >= ffiC.GET_MAX+0ULL) then error("Invalid inventory ID "..inv, 3) end end local player_mt = { __index = { --- Getters/setters get_ammo_amount = function(p, weap) check_weapon_idx(weap) return ffi.cast(player_ptr_ct, p).ammo_amount[weap] end, set_ammo_amount = function(p, weap, amount) check_weapon_idx(weap) ffi.cast(player_ptr_ct, p).ammo_amount[weap] = amount end, get_max_ammo_amount = function(p, weap) check_weapon_idx(weap) return ffi.cast(player_ptr_ct, p).max_ammo_amount[weap] end, set_max_ammo_amount = function(p, weap, amount) check_weapon_idx(weap) ffi.cast(player_ptr_ct, 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) check_inventory_idx(inv) return ffi.cast(player_ptr_ct, p).inv_amount[inv] end, set_inv_amount = function(p, inv, amount) check_inventory_idx(inv) ffi.cast(player_ptr_ct, p).inv_amount[inv] = amount end, set_customexitsound = function(p, soundnum) if (soundnum >= con_lang.MAXSOUNDS+0ULL) then error("Invalid sound number "..soundnum, 2) end 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, addinventory = con._addinventory, pstomp = con._pstomp, wack = function(p) p.horiz = p.horiz + 64 p.return_to_center = 9 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... _palfrom = function(p, f, r,g,b) local pals = p.pals pals.f = f pals.r, pals.g, pals.b = r, g, b end, -- XXX: is this so useful without z offset? _cansee = function(p, otherpos, othersect) return cansee(p.pos, sprite[p.i].sectnum, otherpos, othersect) end, }, } ffi.metatype("DukePlayer_t", player_mt) 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 ===--- 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"), 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", ERRLEV) 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_.gameevent = gameevent -- included in lunatic.c G_.gameactor = gameactor -- included in lunatic.c G_.player = player -- from above ---=== 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.." ("..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() 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) -- This will create 'sprite', 'wall', etc. HERE, i.e. in the environment of this chunk defs_c.create_globals(_G) ---- indirect C array access ---- actor = defs_c.creategtab(ffiC.actor, ffiC.MAXSPRITES, 'actor[]') function TEMP_getvollev() -- REMOVE return ffiC.ud.volume_number+1, ffiC.ud.level_number+1 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)