raze-gles/polymer/eduke32/source/lunatic/control.lua
helixhorned e0433e66fb LunaCON: real user per-player vars.
The previous behavior was to translate them as global gamevars, since LunaCON
currently has no support for multiplayer. However, then some errors would be
missed where such gamevars are accessed in no-player context
(e.g. EVENT_ENTERLEVEL).
On by default, disabled with -fno-playervar.

git-svn-id: https://svn.eduke32.com/eduke32@3842 1a8010ca-5511-0410-912e-c29ae57300e0
2013-06-01 20:09:41 +00:00

2168 lines
59 KiB
Lua

-- 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 table = require("table")
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 pairs = pairs
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 wallsofsect = dc.wallsofsect
local spritesofsect, spritesofstat = dc.spritesofsect, dc.spritesofstat
local OUR_NAME = "_con"
local OUR_REQUIRE_STRING = "local "..OUR_NAME.."=require'con'"
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
-- Table of all per-actor gamevars active in the system.
-- [<actorvar reference>] = true
local g_actorvar = setmetatable({}, { __mode="k" })
local function A_ResetVars(i)
for acv in pairs(g_actorvar) do
acv:_clear(i)
end
end
ffiC.A_ResetVars = A_ResetVars
-- Reset per-actor gamevars for the sprite that would be inserted by the next
-- insertsprite() call.
-- TODO_MP (Net_InsertSprite() is not handled)
--
-- NOTE: usually, a particular actor's code doesn't use ALL per-actor gamevars,
-- so there should be a way to clear only a subset of them (maybe those that
-- were defined in "its" module?).
local function A_ResetVarsNextIns()
-- KEEPINSYNC with insertsprite() logic in engine.c!
local i = ffiC.headspritestat[ffiC.MAXSTATUS]
if (i < 0) then
return
end
ffiC.g_noResetVars = 1
return A_ResetVars(i)
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
A_ResetVarsNextIns()
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
A_ResetVarsNextIns()
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
local BADGUY_MASK = bit.bor(con_lang.SFLAG.SFLAG_HARDCODED_BADGUY, con_lang.SFLAG.SFLAG_BADGUY)
function isenemytile(tilenum)
return (bit.band(ffiC.g_tile[tilenum].flags, BADGUY_MASK)~=0)
end
-- The 'rotatesprite' wrapper used by the CON commands.
function _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation,
alpha, cx1, cy1, cx2, cy2)
check_tile_idx(tilenum)
orientation = bit.band(orientation, 4095) -- ROTATESPRITE_MAX-1
if (bit.band(orientation, 2048) == 0) then -- ROTATESPRITE_FULL16
x = 65536*x
y = 65536*y
end
-- 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 the
-- tile's bounding rectange is beyond the screen.
-- XXX: Currently, classic rotatesprite() is not correct with some large
-- zoom values.
if (not (x >= -320*65536 and x < 640*65536) or not (y >= -200*65536 and y < 400*65536)) then
error(format("invalid coordinates (%.03f, %.03f)", x, y), 2)
end
ffiC.rotatesprite_(x, y, zoom, ang, tilenum, shade, pal, bit.bor(2,orientation),
alpha, cx1, cy1, cx2, cy2)
end
-- The external legacy tile drawing function for Lunatic.
function rotatesprite(x, y, zoom, ang, tilenum, shade, pal, orientation,
alpha, cx1, cy1, cx2, cy2)
-- Disallow <<16 coordinates from Lunatic. They only unnecessarily increase
-- complexity; you already have more precision in the FP number fraction.
if (bit.band(orientation, 2048) ~= 0) then
error('left-shift-by-16 coordinates forbidden', 2)
end
return _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation,
alpha, 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()
local 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.ammo_amount[weap] >= ps.max_ammo_amount[weap])
end
local function P_AddAmmo(ps, weap, amount)
if (not have_ammo_at_max(ps, weap)) then
local curamount = ps.ammo_amount[weap]
local maxamount = ps.max_ammo_amount[weap]
-- NOTE: no clamping towards the bottom
ps.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 REALMAXQUOTES = con_lang.REALMAXQUOTES
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+REALMAXQUOTES, 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, 4095) -- 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.inv_amount[inv] = amount
end
end
function _checkpinventory(ps, inv, amount, i)
if (inv==ffiC.GET_SHIELD) then
return ps.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.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.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(aci, 229)
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)
local spr1, spr2 = sprite[s1], sprite[s2]
-- Redundant, but points the error messages to the CON code:
check_sector_idx(spr1.sectnum)
check_sector_idx(spr2.sectnum)
return cansee(spr1, spr1.sectnum, spr2, spr2.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
-- CON "clipmove" and "clipmovenoslide" commands
function _clipmovex(x, y, z, sectnum, xv, yv, wd, cd, fd, clipmask, noslidep)
check_sector_idx(sectnum)
local ipos = geom.ivec3(x, y, z)
local sect = ffi.new("int16_t [1]")
local ret = ffiC.clipmovex(ipos, sect, xv, yv, wd, cd, fd, clipmask, noslidep)
-- Return: clipmovex() return value; updated x, y, sectnum
return ret, ipos.x, ipos.y, sect[0]
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
-- NOTE: returns two args (in C version, hit sprite is a pointer input arg)
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.inv_amount.STEROIDS > 0 and ps.inv_amount.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 _squished(aci, pli)
check_sprite_idx(aci)
check_player_idx(pli)
check_sector_idx(sprite[aci].sectnum)
return (ffiC.VM_CheckSquished2(aci, pli)~=0)
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
-- NOTE: This command is really badly named in CON. It issues a sound that
-- emanates from the current player instead of being 'system-global'.
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)
if (aci ~= -1) then
check_sprite_idx(aci)
end
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 <sndidx> 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)
check_player_idx(pli)
ffiC.P_SetGamePalette(ffiC.g_player_ps[pli], basepal, 2+16)
end
-- Map state persistence.
-- TODO: saving/restoration of per-player or per-actor gamevars.
function _savemapstate()
ffiC.G_SaveMapState()
end
function _loadmapstate()
ffiC.G_RestoreMapState()
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 <nints> 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
local function serialize_value(strtab, i, v)
-- Save only user values (i.e. not 'meta-fields' like '_size').
if (type(i)=="number" and v~=nil) then
strtab[#strtab+1] = "["..i.."]="..tostring(v)..","
end
end
-- Common serialization function for gamearray and actorvar.
local function serialize_array(ar, strtab, maxnum)
-- if (ffiC._DEBUG_LUNATIC ~= 0) then
-- Iterate in numeric order. XXX: also for non-debug?
for i=0,maxnum-1 do
serialize_value(strtab, i, rawget(ar, i))
end
-- else
-- for i,v in pairs(ar) do
-- serialize_value(strtab, i, v)
-- end
-- end
strtab[#strtab+1] = "})"
return table.concat(strtab)
end
local function our_get_require()
return OUR_REQUIRE_STRING
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 = "\001\002EDuke32GameArray\003\004"
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)
if (f == nil) then
-- file, numints, isnewgar, filename
return nil, nil, true, fn
end
else
f, errmsg = kopen4load(fn, 0)
if (f == nil) then
if (f==false) then
error(format([[failed opening "%s" for reading: %s]], fn, errmsg), 3)
else
return
end
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)
if (f==nil) then
return
end
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, (ints[i]==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
local f, errmsg = 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,
--- Internal routines ---
-- * All values equal to the default one (0) are cleared.
_cleanup = function(gar)
for i=0,gar._size-1 do
if (rawget(gar, i)==0) then
rawset(gar, i, nil)
end
end
end,
--- Serialization ---
_get_require = our_get_require,
_serialize = function(gar)
local strtab = { OUR_NAME.."._gamearray(", tostring(gar._size), ",{" }
gar:_cleanup()
return serialize_array(gar, strtab, gar._size)
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,
__metatable = "serializeable",
}
-- Common constructor helper for gamearray and actorvar.
local function set_values_from_table(ar, values)
if (values ~= nil) then
for i,v in pairs(values) do
ar[i] = v
end
end
return ar
end
-- NOTE: Gamearrays are internal because users are encouraged to use tables
-- from Lua code.
-- <values>: optional, a table of <index>=value
function _gamearray(size, values)
local gar = setmetatable({ _size=size }, gamearray_mt)
return set_values_from_table(gar, values)
end
--- More functions of the official API ---
-- 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 ==--
local actorvar_methods = {
--- Internal routines ---
-- * All values for sprite not in the game world are cleared.
-- * All values equal to the default one are cleared.
_cleanup = function(acv)
for i=0,ffiC.MAXSPRITES-1 do
if (ffiC.sprite[i].statnum == ffiC.MAXSTATUS or rawget(acv, i)==acv._defval) then
acv:_clear(i)
end
end
end,
_clear = function(acv, i)
rawset(acv, i, nil)
end,
--- Serialization ---
_get_require = our_get_require,
_serialize = function(acv)
local strtab = { OUR_NAME..".actorvar(", tostring(acv._defval), ",{" }
-- TODO: Must clean up sometime if not saving, too. (That is, what is
-- A_ResetVars() in the C-CON build.)
acv:_cleanup()
return serialize_array(acv, strtab, ffiC.MAXSPRITES)
end,
}
-- XXX: How about types other than numbers?
local actorvar_mt = {
__index = function(acv, idx)
if (type(idx)=="number") then
check_sprite_idx(idx)
return acv._defval
else
return actorvar_methods[idx]
end
end,
__newindex = function(acv, idx, val)
check_sprite_idx(idx)
rawset(acv, idx, val)
end,
__metatable = "serializeable",
}
-- <initval>: default value for per-actor variable.
-- <values>: optional, a table of <spritenum>=value
function actorvar(initval, values)
local acv = setmetatable({ _defval=initval }, actorvar_mt)
g_actorvar[acv] = true
return set_values_from_table(acv, values)
end
--== Per-player variable (kind of CODEDUP) ==--
local playervar_methods = {
--- Serialization ---
_get_require = our_get_require,
_serialize = function(plv)
local strtab = { OUR_NAME..".playervar(", tostring(plv._defval), ",{" }
return serialize_array(plv, strtab, ffiC.MAXSPRITES)
end,
}
-- XXX: How about types other than numbers?
local playervar_mt = {
__index = function(plv, idx)
if (type(idx)=="number") then
check_player_idx(idx)
return plv._defval
else
return playervar_methods[idx]
end
end,
__newindex = function(plv, idx, val)
check_player_idx(idx)
rawset(plv, idx, val)
end,
__metatable = "serializeable",
}
-- <initval>: default value for per-player variable.
-- <values>: optional, a table of <playeridx>=value
function playervar(initval, values)
local plv = setmetatable({ _defval=initval }, playervar_mt)
return set_values_from_table(plv, values)
end