raze-gles/polymer/eduke32/source/lunatic/control.lua
helixhorned b0c48eec25 CON: always return non-negative values from 'clipmove' and related.
Previously, the C function clipmove() returned negative values when hit a
wall (32768+wallnum) or sprite (49152+spritenum) because internally,
these values were encoded into a *signed* 16-bit integer. This made no
difference to C code using it, since it always proceeded by bit checks,
but was inconsistent with documentation on CON 'clipmove' on the wiki.

The following commands are affected too, since they use the value returned
by clipmove(): 'clipmovenoslide', 'movesprite'. Also, the value of
actor[].movflag ('htmovflag' from CON).

Also, fix 'clipmove*' in LunaCON and add lunatic/test/checknearwall.con
as an example of how to implement a being-close-to-a-wall checker as
requested in
http://forums.duke4.net/topic/7869-determining-closeness-to-a-wall/

git-svn-id: https://svn.eduke32.com/eduke32@4874 1a8010ca-5511-0410-912e-c29ae57300e0
2015-01-04 18:44:58 +00:00

2394 lines
66 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 debug = require("debug")
local io = require("io")
local math = require("math")
local table = require("table")
local bcheck = require("bcheck")
local con_lang = require("con_lang")
local byte = require("string").byte
local setmetatable = setmetatable
local band, bor = bit.band, bit.bor
local rshift = bit.rshift
local tobit = bit.tobit
local floor = math.floor
local assert = assert
local error = error
local ipairs = ipairs
local pairs = pairs
local print = print
local rawget = rawget
local rawset = rawset
local select = select
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 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 check_number = bcheck.number
local check_type = bcheck.type
local lprivate = require("lprivate")
local GET, WEAPON = lprivate.GET, lprivate.WEAPON
ffi.cdef[[
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, void * restrict stream);
]]
local OUR_REQUIRE_STRING = [[
local _con=require'con'
local _ga,_av,_pv=_con._gamearray,_con.actorvar,_con.playervar
]]
local function our_get_require()
return OUR_REQUIRE_STRING
end
module(...)
---=== ACTION/MOVE/AI HELPERS ===---
local lastid = { action=0, move=0, ai=0 }
local con_action_ct = ffi.typeof("const con_action_t")
local con_move_ct = ffi.typeof("const con_move_t")
local con_ai_ct = ffi.typeof("const con_ai_t")
-- All-zero action and move with IDs. Mostly for CON support.
local literal_act = { [0]=con_action_ct(0), [1]=con_action_ct(1) }
local literal_mov = { [0]=con_move_ct(0), [1]=con_move_ct(1) }
local literal_am = { action=literal_act, move=literal_mov }
-- Const-qualified 'full' action and move (with ID):
local am_ctype_full_const = { action=con_action_ct, move=con_move_ct }
-- Non-const-qualified 'bare' action and move (without ID):
local am_ctype_bare = { action=ffi.typeof("struct action"), move=ffi.typeof("struct move") }
-- CODEDUP lunacon.lua
local function truetab(tab)
local ttab = {}
for i=1,#tab do
ttab[tab[i]] = true
end
return ttab
end
-- CODEDUP lunacon.lua
local ALLOWED_VIEWTYPE = truetab { 0, 1, 3,4, 5, 7, 8, -5, -7 }
local function def_action_or_move(what, tab)
if (lastid[what] <= -(2^31)) then
error("Too many "..what.."s defined", 3);
end
bcheck.top_level(what, 4)
-- NOTE: tab[0]~=nil check for "Special default values" below.
if (type(tab) ~= "table" or tab[0]~=nil) then
error("invalid argument to con."..what..": must be a table", 3)
end
-- Pass args table to ffi.new, which can take either: a table with numeric
-- indices, or a table with key-value pairs, *but not in combination*.
-- See http://luajit.org/ext_ffi_semantics.html#init_table
local am = am_ctype_bare[what](tab)
-- Now, set all string keys as they have been ignored if tab[1] was
-- non-nil.
for key, val in pairs(tab) do
if (type(key)=="string") then
am[key] = val
end
end
if (what=="action") then
-- Special default values or checking of actor members.
-- KEEPINSYNC with ACTOR_CHECK in lunacon.lua for consistency.
local numframes = tab[2] or tab.numframes
local viewtype = tab[3] or tab.viewtype
local incval = tab[4] or tab.incval
if (numframes==nil) then
am.numframes = 1
else
check_number(numframes, 4)
if (numframes < 0) then
error("action has negative number of frames", 3)
end
end
if (viewtype==nil) then
am.viewtype = 1
else
check_number(viewtype, 4)
if (ALLOWED_VIEWTYPE[viewtype] == nil) then
error("action has disallowed viewtype "..viewtype, 3)
end
end
if (incval==nil) then
am.incval = 1
end
end
-- 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
return am_ctype_full_const[what](lastid[what], am)
end
---=== ACTION/MOVE/AI FUNCTIONS ===---
function action(tab)
return def_action_or_move("action", tab)
end
function move(tab)
return def_action_or_move("move", tab)
end
-- Get action or move for an 'ai' definition.
local function get_action_or_move(what, val, argi)
if (val == nil) then
return literal_am[what][0]
elseif (ffi.istype(am_ctype_full_const[what], val)) then
return val
elseif (type(val)=="number") then
if (val==0 or val==1) then
return literal_am[what][val]
end
end
error("bad argument #"..argi.." to ai: must be nil/nothing, 0, 1, or "..what, 3)
end
function ai(action, move, flags)
bcheck.top_level("ai")
if (lastid.ai <= -(2^31)) then
error("Too many AIs defined", 2);
end
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
lastid.ai = lastid.ai-1
return con_ai_ct(lastid.ai, act, mov, flags)
end
---=== RUNTIME CON FUNCTIONS ===---
-- Will contain [<label>]=number mappings after CON translation.
local D = { true }
local function krandand(mask)
return band(ffiC.krand(), mask)
end
local function check_allnumbers(...)
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 [, statnum [, owner]] [, key=val...]}
-- valid keys are: owner, statnum, shade, xrepeat, yrepeat, xvel, zvel
-- 2. position-call: insertsprite(tilenum, pos, sectnum [, statnum [, owner]])
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)
statnum = tab[4] or tab.statnum or 0
owner = tab[5] or tab.owner or -1
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)
statnum = args[3] or 0
owner = args[4] or -1
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_allnumbers(shade, xrepeat, yrepeat, ang, xvel, zvel, owner)
if (owner ~= -1) then
check_sprite_idx(owner)
end
if (not (statnum >= 0 and statnum < ffiC.MAXSTATUS)) then
error("invalid 'statnum' argument to insertsprite: must be a status number [0 .. MAXSTATUS-1]", 2)
end
A_ResetVarsNextIns()
local i = CF.A_InsertSprite(sectnum, pos.x, pos.y, pos.z, tilenum,
shade, xrepeat, yrepeat, ang, xvel, zvel,
owner, statnum)
if (owner == -1) then
ffiC.sprite[i]:_set_owner(i)
end
return i
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(tilenum, parentspritenum, addtodelqueue)
check_tile_idx(tilenum)
check_sprite_idx(parentspritenum)
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, label, n)
local tilenum = D[label]
if (tilenum ~= nil) then
local spr = sprite[ow]
for i=n,1, -1 do
local j = insertsprite{ tilenum, spr^(ffiC.krand()%(47*256)), spr.sectnum, 5, ow,
shade=-32, xrepeat=8, yrepeat=8, ang=krandand(2047) }
_spawnexisting(j)
sprite[j].cstat = krandand(8+4)
end
end
end
local int16_st = ffi.typeof "struct { int16_t s; }"
-- Get INT32_MIN for the following constant; passing 0x80000000 would be
-- out of the range for an int32_t and thus undefined behavior!
local SHOOT_HARDCODED_ZVEL = tobit(0x80000000)
function shoot(tilenum, i, zvel)
check_sprite_idx(i)
check_sector_idx(ffiC.sprite[i].sectnum) -- accessed in A_ShootWithZvel
check_tile_idx(tilenum)
zvel = zvel and int16_st(zvel).s or SHOOT_HARDCODED_ZVEL
return CF.A_ShootWithZvel(i, tilenum, zvel)
end
local BADGUY_MASK = bor(con_lang.SFLAG.SFLAG_HARDCODED_BADGUY, con_lang.SFLAG.SFLAG_BADGUY)
function isenemytile(tilenum)
return (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 = band(orientation, 4095) -- ROTATESPRITE_MAX-1
if (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
local blendidx = 0
if (alpha < 0) then
blendidx = -alpha
alpha = 0
end
ffiC.rotatesprite_(x, y, zoom, ang, tilenum, shade, pal, bor(2,orientation),
alpha, blendidx, 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 (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
function rnd(x)
return (rshift(ffiC.krand(), 8) >= (255-x))
end
--- Legacy operators ---
function _rand(x)
return floor((ffiC.krand()*(x+1))/65536)
end
function _displayrand(x)
return floor((math.random(0, 32767)*(x+1))/32768)
end
do
-- Arithmetic operations --
local INT32_MIN = tobit(0x80000000)
local INT32_MAX = tobit(0x7fffffff)
-- Trapping multiplication.
function _mulTR(a,b)
local c = a*b
if (not (c >= INT32_MIN and c <= INT32_MAX)) then
error("overflow in multiplication", 2)
end
return c
end
-- Wrapping multiplication.
function _mulWR(a,b)
-- XXX: problematic if a*b in an infinity or NaN.
return tobit(a*b)
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
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
function _get_userdef_check(pli)
if (pli ~= ffiC.myconnectindex) then
error(format("userdefs access with non-local current player %d (we: %d)",
pli, ffiC.myconnectindex), 2)
end
return ffiC.ud
end
function _get_userdef(pli)
return ffiC.ud
end
function _err_if_negative(val)
if (not (val >= 0)) then
error("setting tag to negative value", 2)
end
return val
end
--- player/actor/sprite searching functions ---
local xmath = require("xmath")
local abs = math.abs
local bangvec, kangvec = xmath.bangvec, xmath.kangvec
local dist, ldist = xmath.dist, xmath.ldist
local vec3, ivec3 = xmath.vec3, xmath.ivec3
local rotate = xmath.rotate
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
function _findplayer(pli, spritenum)
return 0, A_FP_ManhattanDist(player[pli], sprite[spritenum])
end
local STAT = actor.STAT
local FN_STATNUMS = {
[false] = { 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 (ldist(s1, s2) < d)
end,
d3 = function(s1, s2, d)
return (dist(s1, s2) < d)
end,
z = function(s1, s2, d, zd)
return (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
function _tossweapon(pli) -- P_DropWeapon replacement
-- NOTE: We're passing player index, C-CON passes APLAYER sprite.
check_player_idx(pli)
local ps = ffiC.g_player[pli].ps
bcheck.weapon_idx(ps.curr_weapon)
local cw = ffiC.g_playerWeapon[pli][ps.curr_weapon].workslike
if (cw >= ffiC.MAX_WEAPONS+0ULL) then
return
end
if (krandand(1) ~= 0) then
spawn(ffiC.WeaponPickupSprites[cw], ps.i)
elseif (cw==WEAPON.RPG or cw==WEAPON.HANDBOMB) then
if (D.EXPLOSION2 ~= nil) then
spawn(D.EXPLOSION2, ps.i)
end
end
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==WEAPON.KNEE and ps:has_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.)
--- TODO: Move these to a separate module like "con_private".
-- 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 _qsubstr(qdst, qsrc, start, length)
local cstr_dst = bcheck.quote_idx(qdst)
local cstr_src = bcheck.quote_idx(qsrc)
if (not (start >= 0 and start < MAXQUOTELEN)) then
error("invalid start position "..start, 2)
end
if (not (length >= 0)) then -- NOTE: no check for start+length!
error("invalid length "..length, 2)
end
local si = 0
while (cstr_src[si] ~= 0 and si < start) do
si = si+1
end
for i=0,math.huge do
cstr_dst[i] = cstr_src[si + i]
if (si + i == MAXQUOTELEN-1 or i==length or cstr_dst[i] == 0) then
cstr_dst[i] = 0
break
end
end
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, n)
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
if (n == nil) then
n = 0x7fffffff
elseif (n < 0) then
error("invalid number of chars to concatenate: "..n, 2)
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.
n = n-1
i = i+1
j = j+1
cstr_dst[i] = cstr_src[j]
until (n == 0 or 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 (not (gfuncnum >= 0 and gfuncnum < ffiC.NUMGAMEFUNCTIONS)) then
error("invalid game function number "..gfuncnum, 2)
end
if (not (which >= 0 and which < 3)) 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])
local mapnamep = (what == ffiC.STR_MAPNAME)
if (mapnamep or what == ffiC.STR_MAPFILENAME) then
assert(not (idx >= MAXIDX+0ULL))
local src = mapnamep and ffiC.MapInfo[idx].name or ffiC.MapInfo[idx].filename
if (src == nil) then
error(format("attempted access to %s of non-existent map (vol=%d, lev=%d)",
mapnamep and "name" or "file name",
ffiC.ud.volume_number, ffiC.ud.level_number), 2)
end
quote_strcpy(dst, src)
elseif (what == ffiC.STR_PLAYERNAME) then
check_player_idx(pli)
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
local vol = ffiC.ud.volume_number
bcheck.volume_idx(vol)
ffi.copy(dst, ffiC.EpisodeNames[vol], ffi.sizeof(ffiC.EpisodeNames[0]))
else
error("unknown system string ID "..what, 2)
end
end
function _getpname(qnum, pli)
bcheck.quote_idx(qnum, true)
check_player_idx(pli)
local uname = ffiC.g_player[pli].user_name
ffiC.C_DefineQuote(qnum, (uname[0] ~= 0) and uname or tostring(pli))
end
-- switch statement support
function _switch(swtab, testval, aci,pli,dst)
local func = swtab[testval] or swtab.default
if (func) then
func(aci, pli, dst)
end
end
--== Text rendering ==--
-- For external use. NOTE: <pal> comes before <shade>.
function minitext(x, y, str, pal, shade)
check_type(str, "string")
ffiC.minitext_(x, y, str, shade or 0, pal or 0, 2+8+16)
end
-- For CON only.
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 (not (tilenum >= 0 and tilenum < ffiC.MAXTILES-9)) 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
local function text_check_common(tilenum, orientation)
if (not (tilenum >= 0 and tilenum < ffiC.MAXTILES-255)) then
error("invalid base tile number "..tilenum, 3)
end
return band(orientation, 4095) -- ROTATESPRITE_MAX-1
end
function _gametext(tilenum, x, y, qnum, shade, pal, orientation,
cx1, cy1, cx2, cy2, zoom)
orientation = text_check_common(tilenum, orientation)
local cstr = bcheck.quote_idx(qnum)
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)
function _screentext(tilenum, x, y, z, blockangle, charangle, q, shade, pal, orientation,
alpha, xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
orientation = text_check_common(tilenum, orientation)
local cstr = bcheck.quote_idx(q)
ffiC.G_ScreenText(tilenum, x, y, z, blockangle, charangle, cstr, shade, pal, bor(2,orientation),
alpha, xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
end
function _qstrdim(tilenum, x, y, z, blockangle, q, orientation,
xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
orientation = text_check_common(tilenum, orientation)
local cstr = bcheck.quote_idx(q)
local dim = ffiC.G_ScreenTextSize(tilenum, x, y, z, blockangle, cstr, orientation,
xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2);
return dim.x, dim.y
end
function _showview(x, y, z, a, horiz, sect, x1, y1, x2, y2, unbiasedp)
check_sector_idx(sect)
if (x1 < 0 or y1 < 0 or x2 >= 320 or y2 >= 200 or x2 < x1 or y2 < y1) then
local str = format("(%d,%d)--(%d,%d)", x1, y1, x2, y2)
error("invalid coordinates "..str, 2)
end
CF.G_ShowView(x, y, z, a, horiz, sect, x1, y1, x2, y2, unbiasedp);
end
---=== DEFINED LABELS ===---
-- Check if <picnum> equals to the number defined by <label> from CON.
-- If there is no such label, return nil.
local function ispic(picnum, label)
return D[label] and (picnum==D[label])
end
-- Which tiles should use .yvel instead of .hitag for the respawned tile?
-- Will be [<number>] = true after CON translation.
local RESPAWN_USE_YVEL = {
"STATUE", "NAKED1", "PODFEM1", "FEM1", "FEM2",
"FEM3", "FEM5", "FEM4", "FEM6", "FEM8",
"FEM7", "FEM9", "FEM10",
}
-- Is an inventory tile?
-- Will be [<number>] = true after CON translation.
local INVENTILE = {
"FIRSTAID", "STEROIDS", "AIRTANK", "JETPACK", "HEATSENSOR",
"BOOTS", "HOLODUKE",
}
local function totruetab(tab)
local numelts = #tab
for i=1,numelts do
local label = tab[i]
if (D[label]) then
tab[label] = true
end
tab[i] = nil
end
end
-- This will be run after CON has been translated.
function _setuplabels(conlabels)
assert(D[1]) -- Allow running this function exactly once.
D = conlabels
totruetab(RESPAWN_USE_YVEL)
totruetab(INVENTILE)
end
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 (ispic(spr.picnum, "COMMANDER")) then
z = z - (24*256)
end
for i=n,1, -1 do
local pos = vec3(spr.x+krandand(255)-128, spr.y+krandand(255)-128, z-krandand(8191))
local j = insertsprite{ gutstile, pos, spr.sectnum, 5, i, shade=-32, xrepeat=xsz, yrepeat=ysz,
ang=krandand(2047), xvel=48+krandand(31), zvel=-512-krandand(2047) }
local newspr = sprite[j]
if (ispic(newspr.picnum, "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 = (ispic(spr.picnum, "BLIMP") and ispic(dtile, "SCRAP1"))
local picofs = isblimpscrap and 0 or krandand(3)
local pos = spr + vec3(krandand(255)-128, krandand(255)-128, -(8*256)-krandand(8191))
local jj = insertsprite{ dtile+picofs, pos, spr.sectnum, 5, i,
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)
if (D.GLASSPIECES) then
local spr = sprite[i]
for j=n,1, -1 do
local k = insertsprite{ D.GLASSPIECES+n%3, spr^(256*krandand(16)), spr.sectnum, 5, i,
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
end
function _A_IncurDamage(sn)
check_sprite_idx(sn)
return ffiC.A_IncurDamage(sn)
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
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_MP
if (not ispic(spr.picnum, "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 = {
[GET.FIRSTAID] = 1, -- ICON_FIRSTAID
[GET.STEROIDS] = 2,
[GET.HOLODUKE] = 3,
[GET.JETPACK] = 4,
[GET.HEATS] = 5,
[GET.SCUBA] = 6,
[GET.BOOTS] = 7,
}
function _addinventory(ps, inv, amount, i)
if (inv == GET.ACCESS) then
local pal = sprite[i].pal
if (PALBITS[pal]) then
ps.got_access = bor(ps.got_access, PALBITS[pal])
end
else
if (ICONS[inv]) then
ps.inven_icon = ICONS[inv]
end
if (inv == 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==GET.SHIELD) then
return ps.inv_amount[inv] ~= ps.max_shield_amount
elseif (inv==GET.ACCESS) then
local palbit = PALBITS[sprite[i].pal]
return palbit and (band(ps.got_access, palbit)~=0)
else
return ps.inv_amount[inv] ~= amount
end
end
local INV_SELECTION_ORDER = {
GET.FIRSTAID,
GET.STEROIDS,
GET.JETPACK,
GET.HOLODUKE,
GET.HEATS,
GET.SCUBA,
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 = not ispic(sprite[aci].picnum, "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 = 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)
bcheck.weapon_idx(weap)
if (not ps:has_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_allnumbers(r, hp1, hp2, hp3, hp4)
CF.A_RadiusDamage(i, r, hp1, hp2, hp3, hp4)
end
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,
}
function _operate(spritenum)
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
local lotag_lo = band(lotag, 0xff)
if (NEAROP[lotag_lo]) then
if (lotag_lo == 23 or sect.floorz == sect.ceilingz) then
if (band(lotag, 32768+16384) == 0) then
for j in spritesofsect(tag.sector) do
if (ispic(sprite[j].picnum, "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 (ispic(sprite[i].picnum, "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 = xmath.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
-- TODO: xmath.vec3 'mhlen2' method?
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 (band(actor[aci]:get_count(), 63) ~= 0) then
return
end
-- TODO_MP
local angincs = (ffiC.ud.player_skill < 3) and 1024 or 2048/(1+krandand(1))
local spr = sprite[aci]
local j = 0
repeat
local ray = kangvec(otherspr.ang + j, 16384-krandand(32767))
local hit = hitscan(otherspr^(16*256), otherspr.sectnum, ray, ffiC.CLIPMASK1)
local dother = manhatdist(hit.pos, otherspr)
local dactor = manhatdist(hit.pos, spr)
if (dother < dactor and hit.sector >= 0) then
if (cansee(hit.pos, hit.sector, spr^(16*256), spr.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==STAT.ACTOR or spr.statnum==STAT.STANDABLE)) then
actor[aci].timetosleep = SLEEPTIME
end
return can
end
function _isvalid(i)
check_sprite_idx(i)
return ffiC.sprite[i].statnum ~= ffiC.MAXSTATUS and 1 or 0
end
function _canseespr(s1, s2)
check_sprite_idx(s1)
check_sprite_idx(s2)
local spr1, spr2 = ffiC.sprite[s1], ffiC.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 = ivec3(x, y, z)
local ray = ivec3(vx, vy, vz)
local hit = hitscan(srcv, sectnum, ray, cliptype)
return hit.sector, 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 = 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 = 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 = ivec3(x, y, z)
local sect = ffi.new("int16_t [1]", sectnum)
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, dst)
local acs = actor[aci]
if (dst > 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 = ivec3(x, y, z)
return ffiC.A_MoveSpriteClipdist(spritenum, vel, cliptype, -1)
end
-- Also known as A_SetSprite().
function _ssp(i, cliptype)
check_sprite_idx(i)
local spr = ffiC.sprite[i]
local vec = spr.xvel * bangvec(spr.ang) -- XXX: slightly different rounding?
local ivec = vec:toivec3()
ivec.z = spr.zvel
return (ffiC.A_MoveSpriteClipdist(i, ivec, cliptype, -1)==0)
end
-- CON's 'setsprite' function on top of the Lunatic-provided ones.
-- (Lunatic's sprite setting functions have slightly different semantics.)
local updatesect = sprite.updatesect
function _setsprite(i, pos)
check_sprite_idx(i)
local spr = ffiC.sprite[i]
-- First, unconditionally set the sprite's position.
spr:setpos(pos)
-- Next, update the sector number, but if updatesector() returns -1, don't
-- change it. (This is exactly what sprite.updatesect() provides.)
updatesect(i)
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 (ispic(spr.picnum, "APLAYER") and 39*256) or 0
local hit = hitscan(spr^zoff, spr.sectnum, kangvec(spr.ang+angadd), ffiC.CLIPMASK1)
if (hit.wall >= 0 and wall[hit.wall]:ismasked() and spr:isenemy()) then
return -1, nil
end
return hit.sprite, ldist(hit.pos, spr)
end
function _canshoottarget(dst, aci)
if (dst > 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 (ispic(spr.picnum, "APLAYER")) then
local pidx = spr.yvel
check_player_idx(pidx)
spr.pal = player[pidx].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 = band(a1, 2047)
a2 = band(a2, 2047)
if (abs(a2-a1) >= 1024) then
if (a2 > 1024) then a2 = a2 - 2048 end
if (a1 > 1024) then a1 = a1 - 2048 end
end
-- a1, a2 are 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 = ivec3(posx, posy)
local pivot = ivec3(pivotx, pivoty)
pos = rotate(pos, ang, pivot):toivec3()
return pos.x, pos.y
end
local holdskey = player.holdskey
function _ifp(flags, pli, aci)
local l = flags
local ps = player[pli]
local vel = sprite[ps.i].xvel
if (band(l,8)~=0 and ps.on_ground and holdskey(pli, "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 holdskey(pli, "RUN")) then
return true
elseif (band(l,4)~=0 and vel >= 8 and holdskey(pli, "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 holdskey(pli, "RUN")) then
return true
elseif (band(l,256)~=0 and vel <= -8 and holdskey(pli, "RUN")) then
return true
elseif (band(l,512)~=0 and (ps.quick_kick > 0 or (ps.curr_weapon == 0 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_MP
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 band(stat,1)~=0 and sect.ceilingpal == 0 and
(ispic(picnum, "MOONSKY1") or ispic(picnum, "BIGORBIT1"))
end
function _flash(spr, ps)
spr.shade = -127
ps.visibility = -127 -- NOTE: negative value not a problem anymore
end
function _G_OperateRespawns(tag)
for i in spritesofstat(STAT.FX) do
local spr = sprite[i]
if (spr.lotag==tag and ispic(spr.picnum, "RESPAWN")) then
if (ffiC.ud.monsters_off==0 or not isenemytile(spr.hitag)) then
if (D.TRANSPORTERSTAR) then
local j = spawn(D.TRANSPORTERSTAR, i)
sprite[j].z = sprite[j].z - (32*256)
end
-- Just a way to killit (see G_MoveFX(): RESPAWN__STATIC)
spr.extra = 66-12
end
end
end
end
function _G_OperateMasterSwitches(tag)
for i in spritesofstat(STAT.STANDABLE) do
local spr = sprite[i]
if (ispic(spr.picnum, "MASTERSWITCH") and spr.lotag==tag and spr.yvel==0) then
spr:set_yvel(1)
end
end
end
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
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 one unconditionally plays a session-wide sound.
function _screensound(sndidx)
check_sound_idx(sndidx)
CF.A_PlaySound(sndidx, -1)
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
-- Issue a 'soft error', not breaking the control flow.
local errmsg = debug.traceback(
format("null music for volume %d level %d", ffiC.ud.volume_number, level), 2)
errmsg = lprivate.tweak_traceback_msg(errmsg)
ffiC.El_OnError(errmsg)
print("^10error: "..errmsg)
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 = 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.
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, ov)
if (ffiC.ud.config.scripthandle < 0) then
return ov
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)
for i=0,maxnum-1 do
serialize_value(strtab, i, rawget(ar, i))
end
strtab[#strtab+1] = "})"
return table.concat(strtab)
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, "rb")
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, floor(fsize/4), isnewgar, fn
end
local function check_gamearray_idx(gar, idx, addstr)
-- If the actual table has no "_size" field, then we're dealing with a
-- system gamearray: currently, only g_tile.sizx/sizy.
local size = rawget(gar, '_size') or ffiC.MAXTILES
if (not (idx >= 0 and idx < size)) then
addstr = addstr or ""
error("invalid "..addstr.."array index "..idx, 3)
end
end
function _gar_copy(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 ")
-- Source is user gamearray?
local sisuser = (rawget(sar, '_size') ~= nil)
for i=0,numelts-1 do
local val = sisuser and rawget(sar, sidx+i) or sar[sidx+i]
rawset(dar, didx+i, val)
end
end
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 = 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,
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, "wb+")
if (f == nil) then
error([[failed opening "%s" for writing: %s]], fn, errmsg, 3)
end
local nelts = gar._size
local ar = ffi.new("int32_t [?]", nelts)
local isbe = ffi.abi("be") -- is big-endian?
for i=0,nelts-1 do
ar[i] = isbe and bit.bswap(gar[i]) or gar[i]
end
local ok = (ffiC.fwrite(ar, 4, nelts, f) == nelts)
if (ok) then
f:write(GAR_FOOTER)
end
f:close()
if (not ok) then
error([[failed writing all data to "%s"]], fn, 3)
end
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)
gar:_cleanup()
local strtab = { "_ga(", tostring(gar._size), ",{" }
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 perxvar_allowed_types = {
["boolean"]=true, ["number"]=true,
}
local function check_perxval_type(val)
if (perxvar_allowed_types[type(val)] == nil) then
error("type forbidden as per-* variable value: "..type(val), 3)
end
end
local actorvar_methods = {
--- Internal routines ---
-- * All values for sprites 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)
-- NOTE: We also clean up when spawning a sprite, too. (See
-- A_ResetVars() and related functions above.)
acv:_cleanup()
local strtab = { "_av(", tostring(acv._defval), ",{" }
return serialize_array(acv, strtab, ffiC.MAXSPRITES)
end,
}
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)
check_perxval_type(val)
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)
check_perxval_type(initval)
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 = { "_pv(", tostring(plv._defval), ",{" }
return serialize_array(plv, strtab, ffiC.MAXSPRITES)
end,
}
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)
check_perxval_type(val)
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)
check_perxval_type(initval)
local plv = setmetatable({ _defval=initval }, playervar_mt)
return set_values_from_table(plv, values)
end