-- 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 tostring = tostring
local type = type
-- 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]=true 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 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
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 = print
---=== 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;
int32_t zrange;
int16_t angrange, autoaimang;
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;
uint8_t palette;
palette_t pals;
const char name[32];
// 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 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)
-- needed by "control"
actor = defs_c.creategtab(ffiC.actor, ffiC.MAXSPRITES, "actor[]")
--== Custom operations for BUILD data structures ==--
-- Among other things, 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])
assert(not (i >= ffiC.MAXSPRITES+0ULL))
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)
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
-- XXX: CODEDUP control.lua
local function check_sound_idx(sndidx)
if (sndidx >= con_lang.MAXSOUNDS+0ULL) then
error("invalid sound number "..sndidx, 3)
end
end
-- XXX: CODEDUP control.lua
local function check_tile_idx(tilenum)
if (tilenum >= ffiC.MAXTILES+0ULL) then
error("invalid argument: must be a valid tile number", 3)
end
end
--- PER-PLAYER WEAPON SETTINGS
local weapondata_mt = {
__newindex = function(wd, member, val)
if (string.match(member, "sound")) then
-- TODO: set to 0 if oob? (e.g. CrackDown)
check_sound_idx(val)
elseif (member=="workslike") then
check_weapon_idx(val)
elseif (member=="shoots" or member=="spawn") then
-- TODO: set to 0 if oob? (e.g. AMC TC)
check_tile_idx(val)
end
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
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)
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,
stomp = con._pstomp,
-- 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...
_palfrom = function(p, f, r,g,b)
local pals = p.pals
pals.f = f
pals.r, pals.g, pals.b = r, g, b
end,
},
}
ffi.metatype("DukePlayer_t", player_mt)
local camera_mt = {
-- TODO: "set position" method, which also updates the sectnum
__index = ffiC.g_camera,
__newindex = function(_, key, val)
if (key=="sect") then
defs_c.check_sector_idx(val)
end
ffiC.g_camera[key] = val
end,
__metatable = true
}
gv_access.cam = setmetatable({}, camera_mt)
-- Declare all con_lang.labels constants in the global FFI namespace.
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 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
-- 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_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
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] = 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
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' has no varargs ("option functions" in Lua).
-- 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 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_
local gameactor_internal = gameactor_internal -- included in lunatic.c
-- gameactor(tilenum [, strength [, act [, mov [, movflags]]]], actor_func)
local function our_gameactor(tilenum, ...)
if (ffiC.g_elCallDepth > 0) then
error("Invalid use of gameactor: must be called from top level", 2)
end
local args = {...}
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 .. MAXTILES-1)", 2)
end
if (#args == 0) then
error("invalid call to gameactor: must have at least two arguments (tilenum, func)", 2)
end
if (#args > 6) then
error("invalid call to gameactor: must have at most six arguments", 2)
end
if (type(args[#args]) ~= "function") then
error("invalid last argument to gameactor: must be a function", 2)
end
local strength = (#args >= 2) and args[1] or 0
if (type(strength) ~= "number") then
error("invalid 'strength' argument to gameactor: must be a number", 2)
end
-- TODO: literal number action other than 0?
local act = (#args >= 3) and args[2] or "NO"
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
-- TODO: literal number move other than 0?
local mov = (#args >= 4) and args[3] or "NO"
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
local movflags = (#args >= 5) and args[4] or 0
if (type(movflags) ~= "number") then
error("invalid 'movflags' argument to gameactor: must be a number", 2)
end
gameactor_internal(tilenum, strength, act, mov, movflags, args[#args])
end
-- Event functions, saved for event chaining
local event_funcs = {}
local gameevent_internal = gameevent_internal -- included in lunatic.c
-- gameevent(, )
local function our_gameevent(event, 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", 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
if (type(func) ~= "function") then
error("invalid argument #2 to gameevent: must be a function", 2)
end
local newfunc = func
local oldfunc = event_funcs[event]
-- event chaining: events defined later come first
if (oldfunc ~= nil) then
newfunc = function(aci, pli, dist)
func(aci, pli, dist)
return oldfunc(aci, pli, dist)
end
end
gameevent_internal(event, newfunc)
event_funcs[event] = newfunc
end
--- non-default data and functions
G_.gameevent = our_gameevent
G_.gameactor = our_gameactor
G_.player = player -- from above
G_.actor = actor -- from above
---=== 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
concode = lunacon.compile(confn)
if (concode == nil) then
error("Failure compiling CON code, exiting.", 0)
end
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
gv = gv_
local tmpmt = {
__index = function(gv, key)
if (gv_access[key] == nil) then
return ffiC[key]
end
if (type(gv_access[key])=="table") then
-- overridden...
return gv_access[key]
end
error("access forbidden", 2)
end,
__newindex = function() error("write access forbidden", 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)
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
DBG_.oom = function()
local s = "1"
for i=1,math.huge do
s = s..s
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,
})
-- 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)
-- 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)
end
confunc()
end