-- Game control module for Lunatic. local require = require local ffi = require("ffi") local ffiC = ffi.C local jit = require("jit") -- Lua C API functions, this comes from El_PushCFunctions() in lunatic_game.c. local CF = CF local bit = require("bit") local io = require("io") local math = require("math") local geom = require("geom") local bcheck = require("bcheck") local con_lang = require("con_lang") local byte = require("string").byte local setmetatable = setmetatable local assert = assert local error = error local ipairs = ipairs local print = print local rawget = rawget local rawset = rawset local tostring = tostring local type = type local unpack = unpack local format = require("string").format local actor, player = assert(actor), assert(player) local dc = require("defs_common") local cansee, hitscan, neartag = dc.cansee, dc.hitscan, dc.neartag local inside = dc.inside local sector, wall, sprite = dc.sector, dc.wall, dc.sprite local spritesofsect, spritesofstat = dc.spritesofsect, dc.spritesofstat module(...) local lastid = { action=0, move=0, ai=0 } local def = { action = { NO=ffi.new("con_action_t") }, move = { NO=ffi.new("con_move_t") }, ai = { NO=ffi.new("con_ai_t") }, } local function forbidden() error("newindex forbidden", 2) end AC = setmetatable({}, { __index=def.action, __newindex=forbidden }) MV = setmetatable({}, { __index=def.move, __newindex=forbidden }) AI = setmetatable({}, { __index=def.ai, __newindex=forbidden }) local function check_name(name, what, errlev) if (type(name)~="string" or #name > 63) then error("bad argument #1 to "..what..": must be a string of length <= 63", errlev+1) end end local function action_or_move(what, numargs, tab, name, ...) if (lastid[what] <= -(2^31)) then error("Too many "..what.."s defined", 3); end check_name(name, what, 3) local args = {...} if (#args > numargs) then error("Too many arguments passed to "..what, 3) end for i=1,#args do local n = args[i] if (type(n)~="number" or not (n >= -32768 and n <= 32767)) then error("bad argument #".. i+1 .." to "..what.. ": must be numbers in [-32768..32767]", 3) end end -- missing fields are initialized to 0 by ffi.new -- Named actions or moves have negative ids so that non-negative ones -- can be used as (different) placeholders for all-zero ones. lastid[what] = lastid[what]-1 -- ffi.new takes either for initialization: varargs, a table with numeric -- indices, or a table with key-value pairs -- See http://luajit.org/ext_ffi_semantics.html#init_table tab[name] = ffi.new("const con_"..what.."_t", lastid[what], args) end ---=== ACTION / MOVE / AI ===--- function action(name, ...) bcheck.top_level("action") action_or_move("action", 5, def.action, name, ...) end function move(name, ...) bcheck.top_level("move") action_or_move("move", 2, def.move, name, ...) end -- Get action or move for an 'ai' definition. local function get_action_or_move(what, val, argi) if (val == nil) then return {} -- ffi.new will init the struct to all zeros elseif (type(val)=="string") then local am = def[what][val] if (am==nil) then error("no "..what.." '"..val.."' defined", 3) end return am elseif (ffi.istype("con_"..what.."_t", val)) then return val elseif (type(val)=="number") then if (val==0 or val==1) then -- Create an action or move with an ID of 0 or 1 but all other -- fields cleared. return ffi.new("con_"..what.."_t", val) end end error("bad argument #"..argi.." to ai: must be string or (literal) "..what, 3) end function ai(name, action, move, flags) bcheck.top_level("ai") if (lastid.ai <= -(2^31)) then error("Too many AIs defined", 2); end check_name(name, "ai", 2) lastid.ai = lastid.ai-1 local act = get_action_or_move("action", action, 2) local mov = get_action_or_move("move", move, 3) if (flags~=nil) then if (type(flags)~="number" or not (flags>=0 and flags<=32767)) then error("bad argument #4 to ai: must be a number in [0..32767]", 2) end else flags = 0 end def.ai[name] = ffi.new("const con_ai_t", lastid.ai, act, mov, flags) end ---=== RUNTIME CON FUNCTIONS ===--- local check_sector_idx = bcheck.sector_idx local check_tile_idx = bcheck.tile_idx local check_sprite_idx = bcheck.sprite_idx local check_player_idx = bcheck.player_idx local check_sound_idx = bcheck.sound_idx local function krandand(mask) return bit.band(ffiC.krand(), mask) end local function check_isnumber(...) local vals = {...} for i=1,#vals do assert(type(vals[i])=="number") end end -- Lunatic's "insertsprite" is a wrapper around the game "A_InsertSprite", not -- the engine "insertsprite". -- -- Forms: -- 1. table-call: insertsprite{tilenum, pos, sectnum [, owner [, statnum]] [, key=val...]} -- valid keys are: owner, statnum, shade, xrepeat, yrepeat, xvel, zvel -- 2. position-call: insertsprite(tilenum, pos, sectnum [, owner [, statnum]]) function insertsprite(tab_or_tilenum, ...) local tilenum, pos, sectnum -- mandatory -- optional with defaults: local owner, statnum local shade, xrepeat, yrepeat, ang, xvel, zvel = 0, 48, 48, 0, 0, 0 if (type(tab_or_tilenum)=="table") then local tab = tab_or_tilenum tilenum, pos, sectnum = unpack(tab, 1, 3) owner = tab[4] or tab.owner or -1 statnum = tab[5] or tab.statnum or 0 shade = tab.shade or shade xrepeat = tab.xrepeat or xrepeat yrepeat = tab.yrepeat or yrepeat ang = tab.ang or ang xvel = tab.xvel or xvel zvel = tab.zvel or zvel else tilenum = tab_or_tilenum local args = {...} pos, sectnum = unpack(args, 1, 2) owner = args[3] or -1 statnum = args[4] or 0 end if (type(sectnum)~="number" or type(tilenum) ~= "number") then error("invalid insertsprite call: 'sectnum' and 'tilenum' must be numbers", 2) end check_tile_idx(tilenum) check_sector_idx(sectnum) check_isnumber(shade, xrepeat, yrepeat, ang, xvel, zvel, owner) if (statnum >= ffiC.MAXSTATUS+0ULL) then error("invalid 'statnum' argument to insertsprite: must be a status number (0 .. MAXSTATUS-1)", 2) end return CF.A_InsertSprite(sectnum, pos.x, pos.y, pos.z, tilenum, shade, xrepeat, yrepeat, ang, xvel, zvel, owner, statnum) end -- INTERNAL USE ONLY. function _addtodelqueue(spritenum) check_sprite_idx(spritenum) CF.A_AddToDeleteQueue(spritenum) end -- This corresponds to the first (spawn from parent sprite) form of A_Spawn(). function spawn(parentspritenum, tilenum, addtodelqueue) check_sprite_idx(parentspritenum) check_tile_idx(tilenum) if (addtodelqueue and ffiC.g_spriteDeleteQueueSize == 0) then return -1 end local i = CF.A_Spawn(parentspritenum, tilenum) if (addtodelqueue) then CF.A_AddToDeleteQueue(i) end return i end -- This is the second A_Spawn() form. INTERNAL USE ONLY. function _spawnexisting(spritenum) check_sprite_idx(spritenum) return CF.A_Spawn(-1, spritenum) end -- A_SpawnMultiple clone -- ow: parent sprite number function _spawnmany(ow, tilenum, n) local spr = sprite[ow] for i=n,1, -1 do local j = insertsprite{ tilenum, spr^(ffiC.krand()%(47*256)), spr.sectnum, ow, 5, shade=-32, xrepeat=8, yrepeat=8, ang=krandand(2047) } _spawnexisting(j) sprite[j].cstat = krandand(8+4) end end local int16_st = ffi.typeof "struct { int16_t s; }" function _shoot(i, tilenum, zvel) check_sprite_idx(i) check_sector_idx(sprite[i].sectnum) -- accessed in A_ShootWithZvel check_tile_idx(tilenum) zvel = zvel and int16_st(zvel).s or 0x80000000 -- SHOOT_HARDCODED_ZVEL return CF.A_ShootWithZvel(i, tilenum, zvel) end function isenemytile(tilenum) if (bit.band(ffiC.g_tile[tilenum].flags, 0x00040000)~=0) then return true end return (bit.band(ffiC.g_tile[tilenum].flags, ffiC.SFLAG_BADGUY)~=0) end function rotatesprite(x, y, zoom, ang, tilenum, shade, pal, orientation, cx1, cy1, cx2, cy2) check_tile_idx(tilenum) orientation = bit.band(orientation, 2047) -- ROTATESPRITE_MAX-1 -- XXX: This is the same as the check in gameexec.c, but ideally we'd want -- rotatesprite to accept all coordinates and simply draw nothing if they -- denote an area beyond the screen. if (not (x >= -320 and x < 640) or not (y >= -200 and y < 400)) then error(format("invalid coordinates (%.03f, %.03f)", x, y), 2) end -- TODO: check that it works correctly with all coordinates, also if one -- border is outside the screen etc... ffiC.rotatesprite(65536*x, 65536*y, zoom, ang, tilenum, shade, pal, bit.bor(2,orientation), cx1, cy1, cx2, cy2) end function _myos(x, y, zoom, tilenum, shade, orientation, pal) if (pal==nil) then local sect = player[ffiC.screenpeek].cursectnum pal = (sect>=0) and sector[sect].floorpal or 0 end ffiC.G_DrawTileGeneric(x, y, zoom, tilenum, shade, orientation, pal) end function _inittimer(ticspersec) if (not (ticspersec >= 1)) then error("ticspersec must be >= 1", 2) end ffiC.G_InitTimer(ticspersec) end function _gettimedate() v = ffi.new("int32_t [8]") ffiC.G_GetTimeDate(v) return v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7] end local rshift = bit.rshift function rnd(x) return (rshift(ffiC.krand(), 8) >= (255-x)) end -- Legacy operators function _rand(x) return rshift(ffiC.krand()*(x+1), 16) end function _displayrand(x) return rshift(math.random(0, 32767)*(x+1), 15) end function _div(a,b) if (b==0) then error("divide by zero", 2) end -- NOTE: don't confuse with math.modf! return (a - math.fmod(a,b))/b end function _mod(a,b) if (b==0) then error("mod by zero", 2) end return (math.fmod(a,b)) end -- Sect_ToggleInterpolation() clone function _togglesectinterp(sectnum, doset) for w in wallsofsect(sectnum) do ffiC.G_ToggleWallInterpolation(w, doset) local nw = wall[w].nextwall if (nw >= 0) then ffiC.G_ToggleWallInterpolation(nw, doset) ffiC.G_ToggleWallInterpolation(wall[nw].point2, doset) end end end -- Support for translated CON code: get cached sprite, actor and player structs -- (-fcache-sap option). function _getsap(aci, pli) return (aci>=0) and sprite[aci], (aci>=0) and actor[aci], (pli>=0) and player[pli] end --- player/actor/sprite searching functions --- local xmath = require("xmath") local abs = math.abs local dist, ldist = xmath.dist, xmath.ldist local function A_FP_ManhattanDist(ps, spr) local distvec = ps.pos - spr^(28*256) return distvec:touniform():mhlen() end -- Returns: player index, distance -- TODO: MP case function _findplayer(pli, spritenum) return 0, A_FP_ManhattanDist(player[pli], sprite[spritenum]) end local FN_STATNUMS = { [false] = { con_lang.STAT.STAT_ACTOR }, [true] = {}, } -- TODO: Python-like range() and xrange()? for i=0,ffiC.MAXSTATUS-1 do FN_STATNUMS[true][i+1] = ffiC.MAXSTATUS-1-i end local FN_DISTFUNC = { d2 = function(s1, s2, d) return (xmath.ldist(s1, s2) < d) end, d3 = function(s1, s2, d) return (xmath.dist(s1, s2) < d) end, z = function(s1, s2, d, zd) return (xmath.ldist(s1, s2) < d and abs(s1.z-s2.z) < zd) end, } function _findnear(spritenum, allspritesp, distkind, picnum, maxdist, maxzdist) local statnums = FN_STATNUMS[allspritesp] local distfunc = FN_DISTFUNC[distkind] local spr = sprite[spritenum] for _,st in ipairs(statnums) do for i in spritesofstat(st) do if (i ~= spritenum and sprite[i].picnum==picnum) then if (distfunc(spr, sprite[i], maxdist, maxzdist)) then return i end end end end return -1 end ---=== Weapon stuff ===--- --- Helper functions (might be exported later) --- local function have_ammo_at_max(ps, weap) return (ps:get_ammo_amount(weap) >= ps:get_max_ammo_amount(weap)) end local function P_AddAmmo(ps, weap, amount) if (not have_ammo_at_max(ps, weap)) then local curamount = ps:get_ammo_amount(weap) local maxamount = ps:get_max_ammo_amount(weap) -- NOTE: no clamping towards the bottom ps:set_ammo_amount(weap, math.min(curamount+amount, maxamount)) end end local function P_AddWeaponAmmoCommon(ps, weap, amount) P_AddAmmo(ps, weap, amount) if (ps.curr_weapon==ffiC.KNEE_WEAPON and ps:have_weapon(weap)) then CF.P_AddWeaponMaybeSwitchI(ps.weapon._p, weap); end end --- Functions that must be exported because they are used by LunaCON generated code, --- but which are off limits to users. (That is, we need to think about how to --- expose the functionality in a better fashion than merely giving access to --- the C functions.) -- quotes local MAXQUOTES = con_lang.MAXQUOTES local MAXQUOTELEN = con_lang.MAXQUOTELEN -- CON redefinequote command function _definequote(qnum, quotestr) -- NOTE: this is more permissive than C-CON: we allow to redefine quotes -- that were not previously defined. bcheck.quote_idx(qnum, true) assert(type(quotestr)=="string") ffiC.C_DefineQuote(qnum, quotestr) return (#quotestr >= MAXQUOTELEN) end function _quote(pli, qnum) bcheck.quote_idx(qnum) check_player_idx(pli) ffiC.P_DoQuote(qnum+MAXQUOTES, ffiC.g_player[pli].ps) end function _echo(qnum) local cstr = bcheck.quote_idx(qnum) ffiC.OSD_Printf("%s\n", cstr) end function _userquote(qnum) local cstr = bcheck.quote_idx(qnum) -- NOTE: G_AddUserQuote strcpy's the string ffiC.G_AddUserQuote(cstr) end local function strlen(cstr) for i=0,math.huge do if (cstr[i]==0) then return i end end assert(false) end -- NOTE: dst==src is OK (effectively a no-op) local function strcpy(dst, src) local i=-1 repeat i = i+1 dst[i] = src[i] until (src[i]==0) end function _qstrlen(qnum) return strlen(bcheck.quote_idx(qnum)) end function _qstrcpy(qdst, qsrc) local cstr_dst = bcheck.quote_idx(qdst) local cstr_src = bcheck.quote_idx(qsrc) strcpy(cstr_dst, cstr_src) end -- NOTE: qdst==qsrc is OK (duplicates the quote) function _qstrcat(qdst, qsrc) local cstr_dst = bcheck.quote_idx(qdst) local cstr_src = bcheck.quote_idx(qsrc) if (cstr_src[0]==0) then return end if (cstr_dst[0]==0) then return strcpy(cstr_dst, cstr_src) end -- From here on: destination and source quote (potentially aliased) are -- nonempty. local slen_dst = strlen(cstr_dst) assert(slen_dst <= MAXQUOTELEN-1) if (slen_dst == MAXQUOTELEN-1) then return end local i = slen_dst local j = 0 repeat -- NOTE: don't copy the first char yet, so that the qdst==qsrc case -- works correctly. i = i+1 j = j+1 cstr_dst[i] = cstr_src[j] until (i >= MAXQUOTELEN-1 or cstr_src[j]==0) -- Now copy the first char! cstr_dst[slen_dst] = cstr_src[0] cstr_dst[i] = 0 end local buf = ffi.new("char [?]", MAXQUOTELEN) function _qsprintf(qdst, qsrc, ...) -- NOTE: more permissive than C-CON, see _definequote if (bcheck.quote_idx(qdst, true) == nil) then ffiC.C_DefineQuote(qdst, "") -- allocate quote end local dst = bcheck.quote_idx(qdst) local src = bcheck.quote_idx(qsrc) local vals = {...} local i, j, vi = 0, 0, 1 while (true) do local ch = src[j] local didfmt = false if (ch==0) then break end if (ch==byte'%') then local nch = src[j+1] if (nch==byte'd' or (nch==byte'l' and src[j+2]==byte'd')) then -- number didfmt = true if (vi > #vals) then break end local numstr = tostring(vals[vi]) assert(type(numstr)=="string") vi = vi+1 local ncopied = math.min(#numstr, MAXQUOTELEN-1-i) ffi.copy(buf+i, numstr, ncopied) i = i+ncopied j = j+1+(nch==byte'd' and 1 or 2) elseif (nch==byte's') then -- string didfmt = true if (vi > #vals) then break end local k = -1 local tmpsrc = bcheck.quote_idx(vals[vi]) vi = vi+1 i = i-1 repeat i = i+1 k = k+1 buf[i] = tmpsrc[k] until (i >= MAXQUOTELEN-1 or tmpsrc[k]==0) j = j+2 end end if (not didfmt) then buf[i] = src[j] i = i+1 j = j+1 end if (i >= MAXQUOTELEN-1) then break end end buf[i] = 0 strcpy(dst, buf) end function _getkeyname(qdst, gfuncnum, which) local cstr_dst = bcheck.quote_idx(qdst) if (gfuncnum >= ffiC.NUMGAMEFUNCTIONS+0ULL) then error("invalid game function number "..gfuncnum, 2) end if (which >= 3+0ULL) then error("third argument to getkeyname must be 0, 1 or 2", 2) end local cstr_src for i = (which==2 and 0 or which), (which==2 and 1 or which) do local scancode = ffiC.ud.config.KeyboardKeys[gfuncnum][i] cstr_src = ffiC.KB_ScanCodeToString(scancode) if (cstr_src[0] ~= 0) then break end end if (cstr_src[0] ~= 0) then -- All key names are short, no problem strcpy'ing them strcpy(cstr_dst, cstr_src) end end local EDUKE32_VERSION_STR = "EDuke32 2.0.0devel "..ffi.string(ffiC.s_buildRev) local function quote_strcpy(dst, src) local i=-1 repeat i = i+1 dst[i] = src[i] until (src[i]==0 or i==MAXQUOTELEN-1) dst[i] = 0 end function _qgetsysstr(qdst, what, pli) local dst = bcheck.quote_idx(qdst) local idx = ffiC.ud.volume_number*con_lang.MAXLEVELS + ffiC.ud.level_number local MAXIDX = ffi.sizeof(ffiC.MapInfo) / ffi.sizeof(ffiC.MapInfo[0]) if (what == ffiC.STR_MAPNAME) then assert(not (idx >= MAXIDX+0ULL)) local src = ffiC.MapInfo[idx].name assert(src ~= nil) quote_strcpy(dst, src) elseif (what == ffiC.STR_MAPFILENAME) then assert(not (idx >= MAXIDX+0ULL)) local src = ffiC.MapInfo[idx].filename assert(src ~= nil) quote_strcpy(dst, src) elseif (what == ffiC.STR_PLAYERNAME) then ffi.copy(dst, ffiC.g_player[pli].user_name, ffi.sizeof(ffiC.g_player[0].user_name)) elseif (what == ffiC.STR_VERSION) then ffi.copy(dst, EDUKE32_VERSION_STR) elseif (what == ffiC.STR_GAMETYPE) then ffi.copy(dst, "multiplayer not yet implemented") -- TODO_MP elseif (what == ffiC.STR_VOLUMENAME) then ffi.copy(dst, "STR_VOLUMENAME: NYI") else error("unknown system string ID "..what, 2) end end -- switch statement support function _switch(swtab, testval, aci,pli,dist) local func = swtab[testval] or swtab.default if (func) then func(aci, pli, dist) end end -- text rendering function _minitext(x, y, qnum, shade, pal) local cstr = bcheck.quote_idx(qnum) ffiC.minitext_(x, y, cstr, shade, pal, 2+8+16) end function _digitalnumber(tilenum, x, y, num, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) if (tilenum >= ffiC.MAXTILES-9+0ULL) then error("invalid base tile number "..tilenum, 2) end ffiC.G_DrawTXDigiNumZ(tilenum, x, y, num, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) end function _gametext(tilenum, x, y, qnum, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) if (tilenum >= ffiC.MAXTILES-255+0ULL) then error("invalid base tile number "..tilenum, 2) end local cstr = bcheck.quote_idx(qnum) orientation = bit.band(orientation, 2047) -- ROTATESPRITE_MAX-1 ffiC.G_PrintGameText(0, tilenum, bit.arshift(x,1), y, cstr, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) end -- XXX: JIT-compiling FFI calls to G_PrintGameText crashes LuaJIT somewhere in -- its internal routines. I'm not sure who is to blame here but I suspect we -- have some undefined behavior somewhere. Reproducible with DukePlus 2.35 on -- x86 when clicking wildly through its menu. jit.off(_gametext) local D = { -- TODO: dynamic tile remapping ACTIVATOR = 2, MASTERSWITCH = 8, RESPAWN = 9, APLAYER = 1405, FIRSTAID = 53, STEROIDS = 55, AIRTANK = 56, JETPACK = 57, HEATSENSOR = 59, BOOTS = 61, HOLODUKE = 1348, STATUE = 753, NAKED1 = 603, PODFEM1 = 1294, FEM1 = 1312, FEM2 = 1317, FEM3 = 1321, FEM5 = 1323, FEM4 = 1325, FEM6 = 1334, FEM8 = 1336, FEM7 = 1395, FEM9 = 3450, FEM10 = 4864, ATOMICHEALTH = 100, GLASSPIECES = 1031, TRANSPORTERSTAR = 1630, COMMANDER = 1920, JIBS2 = 2250, SCRAP1 = 2400, BLIMP = 3400, } function _A_DoGuts(i, gutstile, n) check_tile_idx(gutstile) local spr = sprite[i] local smallguts = spr.xrepeat < 16 and spr:isenemy() local xsz = smallguts and 8 or 32 local ysz = xsz local z = math.min(spr.z, sector[spr.sectnum]:floorzat(spr)) - 8*256 if (spr.picnum == D.COMMANDER) then z = z - (24*256) end for i=n,1, -1 do local pos = geom.vec3(spr.x+krandand(255)-128, spr.y+krandand(255)-128, z-krandand(8191)) local j = insertsprite{ gutstile, pos, spr.sectnum, i, 5, shade=-32, xrepeat=xsz, yrepeat=ysz, ang=krandand(2047), xvel=48+krandand(31), zvel=-512-krandand(2047) } local newspr = sprite[j] if (newspr.picnum==D.JIBS2) then -- This looks silly, but EVENT_EGS code could have changed the size -- between the insertion and here. newspr.xrepeat = newspr.xrepeat/4 newspr.yrepeat = newspr.yrepeat/4 end newspr.pal = spr.pal end end function _debris(i, dtile, n) local spr = sprite[i] if (spr.sectnum >= ffiC.numsectors+0ULL) then return end for j=n-1,0, -1 do local isblimpscrap = (spr.picnum==D.BLIMP and dtile==D.SCRAP1) local picofs = isblimpscrap and 0 or krandand(3) local pos = spr + geom.vec3(krandand(255)-128, krandand(255)-128, -(8*256)-krandand(8191)) local jj = insertsprite{ dtile+picofs, pos, spr.sectnum, i, 5, shade=spr.shade, xrepeat=32+krandand(15), yrepeat=32+krandand(15), ang=krandand(2047), xvel=32+krandand(127), zvel=-krandand(2047) } -- NOTE: BlimpSpawnSprites[14] (its array size is 15) will never be chosen sprite[jj]:_set_yvel(isblimpscrap and ffiC.BlimpSpawnSprites[math.mod(jj, 14)] or -1) sprite[jj].pal = spr.pal end end function _A_SpawnGlass(i, n) local spr = sprite[i] for j=n,1, -1 do local k = insertsprite{ D.GLASSPIECES+n%3, spr^(256*krandand(16)), spr.sectnum, i, 5, shade=krandand(15), xrepeat=36, yrepeat=36, ang=krandand(2047), xvel=32+krandand(63), zvel=-512-krandand(2047) } sprite[k].pal = spr.pal end end function _A_Shoot(i, atwith) check_sprite_idx(i) check_tile_idx(atwith) return CF.A_ShootWithZvel(i, atwith, 0x80000000) -- SHOOT_HARDCODED_ZVEL end function _A_IncurDamage(sn) check_sprite_idx(sn) return ffiC.A_IncurDamage(sn) end function _VM_FallSprite(i) check_sprite_idx(i) CF.VM_FallSprite(i) end function _sizeto(i, xr, yr) local spr = sprite[i] local dr = (xr-spr.xrepeat) -- NOTE: could "overflow" (e.g. goal repeat is 256, gets converted to 0) spr.xrepeat = spr.xrepeat + ((dr == 0) and 0 or (dr < 0 and -1 or 1)) -- TODO: y stretching is conditional dr = (yr-spr.yrepeat) spr.yrepeat = spr.yrepeat + ((dr == 0) and 0 or (dr < 0 and -1 or 1)) end -- NOTE: function args of the C function have overloaded meaning function _A_Spawn(j, pn) local bound_check = sector[sprite[j].sectnum] -- two in one whack check_tile_idx(pn) return CF.A_Spawn(j, pn) end function _pstomp(ps, i) if (ps.knee_incs == 0 and sprite[ps.i].xrepeat >= 40) then local spr = sprite[i] if (cansee(spr^(4*256), spr.sectnum, ps.pos^(-16*256), sprite[ps.i].sectnum)) then for j=ffiC.playerswhenstarted-1,0 do if (player[j].actorsqu == i) then return end end ps.actorsqu = i ps.knee_incs = 1 if (ps.weapon_pos == 0) then ps.weapon_pos = -1 end end end end function _pkick(ps, spr) -- TODO: multiplayer branch if (spr.picnum~=D.APLAYER and ps.quick_kick==0) then ps.quick_kick = 14 end end function _VM_ResetPlayer2(snum) check_player_idx(snum) return (CF.VM_ResetPlayer2(snum)~=0) end local PALBITS = { [0]=1, [21]=2, [23]=4 } local ICONS = { [ffiC.GET_FIRSTAID] = 1, -- ICON_FIRSTAID [ffiC.GET_STEROIDS] = 2, [ffiC.GET_HOLODUKE] = 3, [ffiC.GET_JETPACK] = 4, [ffiC.GET_HEATS] = 5, [ffiC.GET_SCUBA] = 6, [ffiC.GET_BOOTS] = 7, } function _addinventory(ps, inv, amount, i) if (inv == ffiC.GET_ACCESS) then local pal = sprite[i].pal if (PALBITS[pal]) then ps.got_access = bit.bor(ps.got_access, PALBITS[pal]) end else if (ICONS[inv]) then ps.inven_icon = ICONS[inv] end if (inv == ffiC.GET_SHIELD) then amount = math.min(ps.max_shield_amount, amount) end -- NOTE: this is more permissive than CON, e.g. allows -- GET_DUMMY1 too. ps:set_inv_amount(inv, amount) end end function _checkpinventory(ps, inv, amount, i) if (inv==ffiC.GET_SHIELD) then return ps:get_inv_amount(inv) ~= ps.max_shield_amount elseif (inv==ffiC.GET_ACCESS) then local palbit = PALBITS[sprite[i].pal] return palbit and (bit.band(ps.got_access, palbit)~=0) else return ps:get_inv_amount(inv) ~= amount end end local INV_SELECTION_ORDER = { ffiC.GET_FIRSTAID, ffiC.GET_STEROIDS, ffiC.GET_JETPACK, ffiC.GET_HOLODUKE, ffiC.GET_HEATS, ffiC.GET_SCUBA, ffiC.GET_BOOTS, } -- checkavailinven CON command function _selectnextinv(ps) for _,inv in ipairs(INV_SELECTION_ORDER) do if (ps:get_inv_amount(inv) > 0) then ps.inven_icon = ICONS[inv] return end end ps.inven_icon = 0 end function _checkavailweapon(pli) check_player_idx(pli) CF.P_CheckWeaponI(pli) end function _addphealth(ps, aci, hlthadd) if (ps.newowner >= 0) then ffiC.G_ClearCameraView(ps) end if (ffiC.ud.god ~= 0) then return end local notatomic = (sprite[aci].picnum ~= D.ATOMICHEALTH) local j = sprite[ps.i].extra if (notatomic and j > ps.max_player_health and hlthadd > 0) then return end if (j > 0) then j = j + hlthadd end if (notatomic) then if (hlthadd > 0) then j = math.min(j, ps.max_player_health) end else j = math.min(j, 2*ps.max_player_health) end j = math.max(j, 0) if (hlthadd > 0) then local qmaxhlth = bit.rshift(ps.max_player_health, 2) if (j-hlthadd < qmaxhlth and j >= qmaxhlth) then -- XXX: DUKE_GOTHEALTHATLOW _sound(229, aci) end ps.last_extra = j end sprite[ps.i].extra = j end -- The return value is true iff the ammo was at the weapon's max. -- In that case, no action is taken. function _addammo(ps, weap, amount) return have_ammo_at_max(ps, weap) or P_AddWeaponAmmoCommon(ps, weap, amount) end function _addweapon(ps, weap, amount) if (weap >= ffiC.MAX_WEAPONS+0ULL) then error("Invalid weapon ID "..weap, 2) end if (not ps:have_weapon(weap)) then CF.P_AddWeaponMaybeSwitchI(ps.weapon._p, weap); elseif (have_ammo_at_max(ps, weap)) then return true end P_AddWeaponAmmoCommon(ps, weap, amount) end function _A_RadiusDamage(i, r, hp1, hp2, hp3, hp4) check_sprite_idx(i) check_isnumber(r, hp1, hp2, hp3, hp4) CF.A_RadiusDamage(i, r, hp1, hp2, hp3, hp4) end function _testkey(pli, synckey) local bound_check = player[pli] if (synckey >= 32ULL) then error("Invalid argument #2 to _testkey: must be in [0..31]", 2) end local bits = ffiC.g_player[pli].sync.bits return (bit.band(bits, bit.lshift(1,synckey)) ~= 0) end function _operate(spritenum) local NEAROP = { [9] = true, [15] = true, [16] = true, [17] = true, [18] = true, [19] = true, [20] = true, [21] = true, [22] = true, [23] = true, [25] = true, [26] = true, [29] = true, } local spr = sprite[spritenum] if (sector[spr.sectnum].lotag == 0) then local tag = neartag(spr^(32*256), spr.sectnum, spr.ang, 768, 4+1) if (tag.sector >= 0) then local sect = sector[tag.sector] local lotag = sect.lotag if (NEAROP[bit.band(lotag, 0xff)]) then if (lotag==23 or sect.floorz==sect.ceilingz) then if (bit.band(lotag, 32768+16384) == 0) then for j in spritesofsect(tag.sector) do if (sprite[j].picnum==D.ACTIVATOR) then return end end CF.G_OperateSectors(tag.sector, spritenum) end end end end end end function _operatesectors(sectnum, spritenum) check_sector_idx(sectnum) check_sprite_idx(spritenum) -- XXX: -1 permissible under certain circumstances? CF.G_OperateSectors(sectnum, spritenum) end function _operateactivators(tag, playernum) check_player_idx(playernum) -- NOTE: passing oob playernum would be safe because G_OperateActivators -- bound-checks it assert(type(tag)=="number") CF.G_OperateActivators(tag, playernum) end function _activatebysector(sectnum, spritenum) local didit = false for i in spriteofsect(sectnum) do if (sprite[i].picnum==D.ACTIVATOR) then CF.G_OperateActivators(sprite[i].lotag, -1) end end if (didit) then _operatesectors(sectnum, spritenum) end end function _checkactivatormotion(tag) return ffiC.G_CheckActivatorMotion(tag) end function _endofgame(pli, timebeforeexit) player[pli].timebeforeexit = timebeforeexit player[pli].customexitsound = -1 ffiC.ud.eog = 1 end function _bulletnear(i) return (ffiC.A_Dodge(sprite[i]) == 1) end -- d is a distance function _awayfromwall(spr, d) local vec2 = geom.vec2 local vecs = { vec2(d,d), vec2(-d,-d), vec2(d,-d), vec2(-d,d) } for i=1,4 do if (not inside(vecs[i]+spr, spr.sectnum)) then return false end end return true end local function cossinb(bang) return xmath.cosb(bang), xmath.sinb(bang) end local function manhatdist(v1, v2) return abs(v1.x-v2.x) + abs(v1.y-v2.y) end -- "otherspr" is either player or holoduke sprite local function A_FurthestVisiblePoint(aci, otherspr) if (bit.band(actor[aci]:get_t_data(0), 63) ~= 0) then return end local angincs = (ffiC.ud.player_skill < 3) and 1024 or 2048/(1+krandand(1)) local j = 0 repeat local c, s = cossinb(otherspr.ang + j) local hit = hitscan(otherspr^(16*256), otherspr.sectnum, c, s, 16384-krandand(32767), ffiC.CLIPMASK1) local dother = manhatdist(hit.pos, otherspr) local dactor = manhatdist(hit.pos, sprite[aci]) if (dother < dactor and hit.sect >= 0) then if (cansee(hit.pos, hit.sect, otherspr^(16*256), otherspr.sectnum)) then return hit end end j = j + (angincs - krandand(511)) until (j >= 2048) end local MAXSLEEPDIST = 16384 local SLEEPTIME = 1536 function _cansee(aci, ps) -- Select sprite for monster to target. local spr = sprite[aci] local s = sprite[ps.i] -- This is kind of redundant, but points the error messages to the CON code. check_sector_idx(spr.sectnum) check_sector_idx(s.sectnum) if (ps.holoduke_on >= 0) then -- If holoduke is on, let them target holoduke first. local hs = sprite[ps.holoduke_on] if (cansee(spr^krandand(8191), spr.sectnum, s, s.sectnum)) then s = hs end end -- Can they see player (or player's holoduke)? local can = cansee(spr^krandand(47*256), spr.sectnum, s^(24*256), s.sectnum) if (not can) then -- Search around for target player. local hit = A_FurthestVisiblePoint(aci, s) if (hit ~= nil) then can = true actor[aci].lastvx = hit.pos.x actor[aci].lastvy = hit.pos.y end else -- Else, they did see it. Save where we were looking... actor[aci].lastvx = s.x actor[aci].lastvy = s.y end if (can and (spr.statnum==ffiC.STAT_ACTOR or spr.statnum==ffiC.STAT_STANDABLE)) then actor[aci].timetosleep = SLEEPTIME end return can end function _canseespr(s1, s2) return cansee(sprite[s1], sprite[s1].sectnum, sprite[s2], sprite[s2].sectnum) and 1 or 0 end -- TODO: replace ivec3 allocations with stores to a static ivec3, like in -- updatesector*? -- CON "hitscan" command function _hitscan(x, y, z, sectnum, vx, vy, vz, cliptype) local srcv = geom.ivec3(x, y, z) local hit = hitscan(srcv, sectnum, vx, vy, vz, cliptype) return hit.sect, hit.wall, hit.sprite, hit.pos.x, hit.pos.y, hit.pos.z end -- CON "neartag" command function _neartag(x, y, z, sectnum, ang, range, tagsearch) local pos = geom.ivec3(x, y, z) local near = neartag(pos, sectnum, ang, range, tagsearch) return near.sector, near.wall, near.sprite, near.dist end -- CON "getzrange" command function _getzrange(x, y, z, sectnum, walldist, clipmask) check_sector_idx(sectnum) local ipos = geom.ivec3(x, y, z) local hit = sector[sectnum]:zrangeat(ipos, walldist, clipmask) -- return: ceilz, ceilhit, florz, florhit return hit.c.z, hit.c.num + (hit.c.spritep and 49152 or 16384), hit.f.z, hit.f.num + (hit.f.spritep and 49152 or 16384) end function _sleepcheck(aci, dist) local acs = actor[aci] if (dist > MAXSLEEPDIST and acs.timetosleep == 0) then acs.timetosleep = SLEEPTIME end end function _canseetarget(spr, ps) -- NOTE: &41 ? return cansee(spr^(256*krandand(41)), spr.sectnum, ps.pos, sprite[ps.i].sectnum) end function _movesprite(spritenum, x, y, z, cliptype) check_sprite_idx(spritenum) local vel = geom.ivec3(x, y, z) return ffiC.A_MoveSprite(spritenum, vel, cliptype) end local function A_CheckHitSprite(spr, angadd) local zoff = (spr:isenemy() and 42*256) or (spr.picnum==D.APLAYER and 39*256) or 0 local c, s = cossinb(spr.ang+angadd) local hit = hitscan(spr^zoff, spr.sectnum, c, s, 0, ffiC.CLIPMASK1) if (hit.wall >= 0 and wall[hit.wall]:ismasked() and spr:isenemy()) then return -1, nil end local dx = hit.pos.x-spr.x local dy = hit.pos.y-spr.y return hit.sprite, math.sqrt(dx*dx+dy*dy) -- TODO: use "ldist" approximation for authenticity end function _canshoottarget(dist, aci) if (dist > 1024) then local spr = sprite[aci] local hitspr, hitdist = A_CheckHitSprite(spr, 0) if (hitdist == nil) then return true end local bigenemy = (spr:isenemy() and spr.xrepeat > 56) local sclip = bigenemy and 3084 or 768 local angdif = bigenemy and 48 or 16 local sclips = { sclip, sclip, 768 } local angdifs = { 0, angdif, -angdif } for i=1,3 do if (i > 1) then hitspr, hitdist = A_CheckHitSprite(spr, angdifs[i]) end if (hitspr >= 0 and sprite[hitspr].picnum == spr.picnum) then if (hitdist > sclips[i]) then return false end end end end return true end function _getlastpal(spritenum) local spr = sprite[spritenum] if (spr.picnum == D.APLAYER) then spr.pal = player[spr.yvel].palookup else if (spr.pal == 1 and spr.extra == 0) then -- hack for frozen spr.extra = spr.extra+1 end spr.pal = actor[spritenum].tempang end actor[spritenum].tempang = 0 end -- G_GetAngleDelta(a1, a2) function _angdiff(a1, a2) a1 = bit.band(a1, 2047) a2 = bit.band(a2, 2047) -- a1 and a2 are in [0, 2047] if (abs(a2-a1) < 1024) then return abs(a2-a1) end -- |a2-a1| >= 1024 if (a2 > 1024) then a2=a2-2048 end if (a1 > 1024) then a1=a1-2048 end -- a1 and a2 is in [-1023, 1024] return a2-a1 end function _angdiffabs(a1, a2) return abs(_angdiff(a1, a2)) end function _angtotarget(aci) local spr = sprite[aci] return ffiC.getangle(actor[aci].lastvx-spr.x, actor[aci].lastvy-spr.y) end function _hypot(a, b) return math.sqrt(a*a + b*b) end function _rotatepoint(pivotx, pivoty, posx, posy, ang) local pos = geom.ivec3(posx, posy) local pivot = geom.ivec3(pivotx, pivoty) pos = xmath.rotate(pos, pivot, ang):toivec3() return pos.x, pos.y end local SK = { CROUCH = 1, RUN = 5, } function _ifp(flags, pli, aci) local l = flags local ps = player[pli] local vel = sprite[ps.i].xvel local band = bit.band if (band(l,8)~=0 and ps.on_ground and _testkey(pli, SK.CROUCH)) then return true elseif (band(l,16)~=0 and ps.jumping_counter == 0 and not ps.on_ground and ps.vel.z > 2048) then return true elseif (band(l,32)~=0 and ps.jumping_counter > 348) then return true elseif (band(l,1)~=0 and vel >= 0 and vel < 8) then return true elseif (band(l,2)~=0 and vel >= 8 and not _testkey(pli, SK.RUN)) then return true elseif (band(l,4)~=0 and vel >= 8 and _testkey(pli, SK.RUN)) then return true elseif (band(l,64)~=0 and ps.pos.z < (sprite[aci].z-(48*256))) then return true elseif (band(l,128)~=0 and vel <= -8 and not _testkey(pli, SK.RUN)) then return true elseif (band(l,256)~=0 and vel <= -8 and _testkey(pli, SK.RUN)) then return true elseif (band(l,512)~=0 and (ps.quick_kick > 0 or (ps.curr_weapon == ffiC.KNEE_WEAPON and ps.kickback_pic > 0))) then return true elseif (band(l,1024)~=0 and sprite[ps.i].xrepeat < 32) then return true elseif (band(l,2048)~=0 and ps.jetpack_on) then return true elseif (band(l,4096)~=0 and ps:get_inv_amount(ffiC.GET_STEROIDS) > 0 and ps:get_inv_amount(ffiC.GET_STEROIDS) < 400) then return true elseif (band(l,8192)~=0 and ps.on_ground) then return true elseif (band(l,16384)~=0 and sprite[ps.i].xrepeat > 32 and sprite[ps.i].extra > 0 and ps.timebeforeexit == 0) then return true elseif (band(l,32768)~=0 and sprite[ps.i].extra <= 0) then return true elseif (band(l,65536)~=0) then -- TODO: multiplayer branch if (_angdiffabs(ps.ang, ffiC.getangle(sprite[aci].x-ps.pos.x, sprite[aci].y-ps.pos.y)) < 128) then return true end end return false end function _checkspace(sectnum, floorp) local sect = sector[sectnum] local picnum = floorp and sect.floorpicnum or sect.ceilingpicnum local stat = floorp and sect.floorstat or sect.ceilingstat return bit.band(stat,1)~=0 and sect.ceilingpal == 0 and (picnum==D.MOONSKY1 or picnum==D.BIGORBIT1) end function _flash(spr, ps) spr.shade = -127 ps.visibility = -127 -- XXX ffiC.lastvisinc = ffiC.totalclock+32 end function _G_OperateRespawns(tag) for i in spritesofstat(ffiC.STAT_FX) do local spr = sprite[i] if (spr.lotag==tag and spr.picnum==D.RESPAWN) then if (ffiC.ud.monsters_off~=0 and isenemytile(spr.hitag)) then return end local j = spawn(i, D.TRANSPORTERSTAR) sprite[j].z = sprite[j].z - (32*256) -- Just a way to killit (see G_MoveFX(): RESPAWN__STATIC) spr.extra = 66-12 end end end function _G_OperateMasterSwitches(tag) for i in spritesofstat(ffiC.STAT_STANDABLE) do local spr = sprite[i] if (spr.picnum==D.MASTERSWITCH and spr.lotag==tag and spr.yvel==0) then spr:_set_yvel(1) end end end local RESPAWN_USE_YVEL = { [D.STATUE] = true, [D.NAKED1] = true, [D.PODFEM1] = true, [D.FEM1] = true, [D.FEM2] = true, [D.FEM3] = true, [D.FEM5] = true, [D.FEM4] = true, [D.FEM6] = true, [D.FEM8] = true, [D.FEM7] = true, [D.FEM9] = true, [D.FEM10] = true, } function _respawnhitag(spr) if (RESPAWN_USE_YVEL[spr.picnum]) then if (spr.yvel ~= 0) then _G_OperateRespawns(spr.yvel) end else _G_OperateRespawns(spr.hitag) end end local INVENTILE = { [D.FIRSTAID] = true, [D.STEROIDS] = true, [D.AIRTANK] = true, [D.JETPACK] = true, [D.HEATSENSOR] = true, [D.BOOTS] = true, [D.HOLODUKE] = true, } function _checkrespawn(spr) if (spr:isenemy()) then return (ffiC.ud.respawn_monsters~=0) end if (INVENTILE[spr.picnum]) then return (ffiC.ud.respawn_inventory~=0) end return (ffiC.ud.respawn_items~=0) end -- SOUNDS function _ianysound(aci) check_sprite_idx(aci) return (ffiC.A_CheckAnySoundPlaying(aci)~=0) end function _sound(aci, sndidx) check_sprite_idx(aci) -- A_PlaySound() returns early if the sound index is oob, but IMO it's good -- style to throw an error instead of silently failing. check_sound_idx(sndidx) CF.A_PlaySound(sndidx, aci) end function _globalsound(pli, sndidx) -- TODO: conditional on coop, fake multimode if (pli==ffiC.screenpeek) then _sound(player[pli].i, sndidx) end end -- This is a macro for EDuke32 (game.h) local function S_StopSound(sndidx) ffiC.S_StopEnvSound(sndidx, -1) end function _soundplaying(aci, sndidx) check_sprite_idx(aci) check_sound_idx(sndidx) return (ffiC.S_CheckSoundPlaying(aci, sndidx) ~= 0) end function _stopsound(aci, sndidx) -- XXX: This is weird: the checking is done wrt a sprite, but the sound not. -- NOTE: S_StopSound() stops sound that started playing most recently. if (_soundplaying(aci, sndidx)) then S_StopSound(sndidx) end end function _stopactorsound(aci, sndidx) if (_soundplaying(aci, sndidx)) then ffiC.S_StopEnvSound(sndidx, aci) end end function _soundonce(aci, sndidx) if (not _soundplaying(aci, sndidx)) then _sound(aci, sndidx) end end function _stopallsounds(pli) if (ffiC.screenpeek==pli) then ffiC.FX_StopAllSounds() end end function _setactorsoundpitch(aci, sndidx, pitchoffset) check_sprite_idx(aci) check_sound_idx(sndidx) ffiC.S_ChangeSoundPitch(sndidx, aci, pitchoffset) end function _starttrack(level) bcheck.level_idx(level) if (ffiC.G_StartTrack(level) ~= 0) then error("null music for volume "..ffiC.ud.volume_number.. " level "..level, 2) end end function _startlevel(volume, level) bcheck.volume_idx(volume) bcheck.level_idx(level) ffiC.ud.m_volume_number = volume ffiC.ud.volume_number = volume ffiC.ud.m_level_number = level ffiC.ud.level_number = level ffiC.ud.display_bonus_screen = 0 -- TODO_MP player[0].gm = bit.bor(player[0].gm, 0x00000008) -- MODE_EOL end function _setaspect(viewingrange, yxaspect) if (viewingrange==0) then error('invalid argument #1: must be nonzero', 2) end if (yxaspect==0) then error('invalid argument #2: must be nonzero', 2) end -- XXX: surely not all values are sane ffiC.setaspect(viewingrange, yxaspect) end function _setgamepalette(pli, basepal) ffiC.P_SetGamePalette(player[pli], basepal, 2+16) end -- Gamevar persistence in the configuration file function _savegamevar(name, val) if (ffiC.ud.config.scripthandle < 0) then return end assert(type(name)=="string") assert(type(val)=="number") ffiC.SCRIPT_PutNumber(ffiC.ud.config.scripthandle, "Gamevars", name, val, 0, 0); end function _readgamevar(name) if (ffiC.ud.config.scripthandle < 0) then return end assert(type(name)=="string") local v = ffi.new("int32_t [1]") ffiC.SCRIPT_GetNumber(ffiC.ud.config.scripthandle, "Gamevars", name, v); -- NOTE: doesn't examine SCRIPT_GetNumber() return value and returns 0 if -- there was no such gamevar saved, like C-CON. return v[0] end --- Wrapper of kopen4load file functions in a Lua-like file API -- TODO: move to common side? local kfile_mt = { __gc = function(self) self:close() end, __index = { close = function(self) if (self.fd > 0) then ffiC.kclose(self.fd) self.fd = -1 end end, seek = function(self, whence, offset) local w = whence=="set" and 0 -- SEEK_SET or whence=="end" and 2 -- SEEK_END or error("invalid 'whence' for seek", 2) -- "cur" NYI local pos = ffiC.klseek(self.fd, offset or 0, w) if (pos >= 0) then return pos else return nil, "?" end end, read = function(self, nbytes) assert(type(nbytes)=="number") -- other formats NYI assert(nbytes > 0) local bytes = ffi.new("char [?]", nbytes) local bytesread = ffiC.kread(self.fd, bytes, nbytes) if (bytesread ~= nbytes) then return nil end return ffi.string(bytes, nbytes) end, -- Read little-endian 32-bit integers. read_le_int32 = function(self, nints) local ints = ffi.new("int32_t [?]", nints) local bytesread = ffiC.kread(self.fd, ints, nints*4) if (bytesread ~= nints*4) then return nil end if (ffi.abi("be")) then for i=0,nints-1 do ints[i] = bit.bswap(ints[i]) end end return ints end, }, } local kfile_t = ffi.metatype("struct { int32_t fd; }", kfile_mt) local function kopen4load(fn, searchfirst) local fd = ffiC.kopen4load(fn, searchfirst) if (fd < 0) then return nil, "no such file?" end return kfile_t(fd) end --- Game arrays --- local function moddir_filename(cstr_fn) local fn = ffi.string(cstr_fn) local moddir = ffi.string(ffiC.g_modDir); if (moddir=="/") then return fn else return format("%s/%s", moddir, fn) end end local GAR_FOOTER = "##EDuke32GameArray##" local GAR_FOOTER_SIZE = #GAR_FOOTER local function gamearray_file_common(qnum, writep) local fn = moddir_filename(bcheck.quote_idx(qnum)) local f, errmsg if (writep) then f, errmsg = io.open(fn) else f = kopen4load(fn, 0) end if (f == nil) then if (not writep) then error(format([[failed opening "%s" for reading: %s]], fn, errmsg), 3) else -- file, numints, isnewgar, filename return nil, nil, true, fn end end local fsize = assert(f:seek("end")) local isnewgar = false if (fsize >= GAR_FOOTER_SIZE) then assert(f:seek("end", -GAR_FOOTER_SIZE)) isnewgar = (assert(f:read(GAR_FOOTER_SIZE)) == GAR_FOOTER) if (isnewgar) then fsize = fsize - GAR_FOOTER_SIZE end end return f, math.floor(fsize/4), isnewgar, fn end local function check_gamearray_idx(gar, idx, addstr) if (idx >= gar._size+0ULL) then addstr = addstr or "" error("invalid "..addstr.."array index "..idx, 3) end end local intbytes_t = ffi.typeof("union { int32_t i; uint8_t b[4]; }") local gamearray_methods = { resize = function(gar, newsize) -- NOTE: size 0 is valid (then, no index is valid) if (newsize < 0) then error("invalid new array size "..newsize, 2) end local MAXELTS = math.floor(0x7fffffff/4) if (newsize > MAXELTS) then -- mainly for some sanity with kread() (which we don't use, but still) error("new array size "..newsize.." too large (max="..MAXELTS.." elements)", 2) end -- clear trailing elements in case we're shrinking for i=gar._size,newsize-1 do rawset(gar, i, nil) end gar._size = newsize end, copyto = function(sar, sidx, dar, didx, numelts) -- XXX: Strictest bound checking, see later if we need to relax it. check_gamearray_idx(sar, sidx, "lower source ") check_gamearray_idx(sar, sidx+numelts-1, "upper source ") check_gamearray_idx(dar, didx, "lower destination ") check_gamearray_idx(dar, didx+numelts-1, "upper destination ") for i=0,numelts-1 do rawset(dar, didx+i, rawget(sar, sidx+i)) end end, read = function(gar, qnum) local f, nelts, isnewgar = gamearray_file_common(qnum, false) assert(f:seek("set")) local ints = f:read_le_int32(nelts) if (ints == nil) then error("failed reading whole file into gamearray", 2) end gar:resize(nelts) for i=0,nelts-1 do rawset(gar, i, (int==0) and nil or ints[i]) end f:close() end, write = function(gar, qnum) local f, _, isnewgar, fn = gamearray_file_common(qnum, true) if (f ~= nil) then f:close() end if (not isnewgar) then error("refusing to overwrite a file not created by a previous `writearraytofile'", 2) end f = io.open(fn, "w+") if (f == nil) then error([[failed opening "%s" for writing: %s]], fn, errmsg, 3) end local nelts = gar._size local cstr = ffi.new("uint8_t [?]", 4*nelts) local isbe = ffi.abi("be") -- is big-endian? for i=0,nelts-1 do local diskval = intbytes_t(isbe and bit.bswap(gar[i]) or gar[i]) for bi=0,3 do cstr[4*i+bi] = diskval.b[bi] end end f:write(ffi.string(cstr, 4*nelts)) f:write(GAR_FOOTER) f:close() end } local gamearray_mt = { __index = function(gar, key) if (type(key)=="number") then check_gamearray_idx(gar, key) return 0 else return gamearray_methods[key] end end, __newindex = function(gar, idx, val) check_gamearray_idx(gar, idx) rawset(gar, idx, val) end, -- Calling a gamearray causes its cleanup: -- * All values equal to the default one (0) are cleared. __call = function(gar) for i=0,gar._size-1 do if (rawget(gar, i)==0) then rawset(gar, i, nil) end end end, __metatable = true, } function _gamearray(size) return setmetatable({ _size=size }, gamearray_mt) end --- Exported functions --- -- Non-local control flow. These ones call the original error(), not our -- redefinition in defs.ilua. function longjmp() error(false) end function killit() -- TODO: guard against deletion of player sprite? error(true) end -- Per-actor variable -- TODO: serialization local peractorvar_mt = { __index = function(acv, idx) check_sprite_idx(idx) return acv._defval end, __newindex = function(acv, idx, val) check_sprite_idx(idx) rawset(acv, idx, val) end, -- Calling a per-actor variable causes its cleanup: -- * All values for sprite not in the game world are cleared. -- * All values equal to the default one are cleared. __call = function(acv) for i=0,ffiC.MAXSPRITES-1 do if (ffiC.sprite[i].statnum == ffiC.MAXSTATUS or rawget(acv, i)==acv._defval) then rawset(acv, i, nil) end end end, __metatable = true, } function peractorvar(initval) return setmetatable({ _defval=initval }, peractorvar_mt) end