1-- Game control module for Lunatic.
2
3local require = require
4local ffi = require("ffi")
5local ffiC = ffi.C
6local jit = require("jit")
7
8-- Lua C API functions, this comes from El_PushCFunctions() in lunatic_game.c.
9local CF = CF
10
11local bit = require("bit")
12local debug = require("debug")
13local io = require("io")
14local math = require("math")
15local table = require("table")
16
17local bcheck = require("bcheck")
18local con_lang = require("con_lang")
19
20local byte = require("string").byte
21local setmetatable = setmetatable
22
23local band, bor = bit.band, bit.bor
24local rshift = bit.rshift
25local tobit = bit.tobit
26
27local floor = math.floor
28
29local assert = assert
30local error = error
31local ipairs = ipairs
32local pairs = pairs
33local print = print
34local rawget = rawget
35local rawset = rawset
36local select = select
37local tostring = tostring
38local type = type
39local unpack = unpack
40
41local format = require("string").format
42
43local actor, player = assert(actor), assert(player)
44local dc = require("defs_common")
45local cansee, hitscan, neartag = dc.cansee, dc.hitscan, dc.neartag
46local inside = dc.inside
47
48local sector, wall, sprite = dc.sector, dc.wall, dc.sprite
49local wallsofsect = dc.wallsofsect
50local spritesofsect, spritesofstat = dc.spritesofsect, dc.spritesofstat
51
52local check_sector_idx = bcheck.sector_idx
53local check_tile_idx = bcheck.tile_idx
54local check_sprite_idx = bcheck.sprite_idx
55local check_player_idx = bcheck.player_idx
56local check_sound_idx = bcheck.sound_idx
57local check_number = bcheck.number
58local check_type = bcheck.type
59
60local lprivate = require("lprivate")
61local GET, WEAPON = lprivate.GET, lprivate.WEAPON
62
63ffi.cdef[[
64size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, void * restrict stream);
65]]
66
67local OUR_REQUIRE_STRING = [[
68 local _con=require'con'
69 local _ga,_av,_pv=_con._gamearray,_con.actorvar,_con.playervar
70]]
71local function our_get_require()
72    return OUR_REQUIRE_STRING
73end
74
75
76module(...)
77
78
79---=== ACTION/MOVE/AI HELPERS ===---
80
81local lastid = { action=0, move=0, ai=0 }
82
83local con_action_ct = ffi.typeof("const con_action_t")
84local con_move_ct = ffi.typeof("const con_move_t")
85local con_ai_ct = ffi.typeof("const con_ai_t")
86
87-- All-zero action and move with IDs. Mostly for CON support.
88local literal_act = { [0]=con_action_ct(0), [1]=con_action_ct(1) }
89local literal_mov = { [0]=con_move_ct(0), [1]=con_move_ct(1) }
90
91local literal_am = { action=literal_act, move=literal_mov }
92-- Const-qualified 'full' action and move (with ID):
93local am_ctype_full_const = { action=con_action_ct, move=con_move_ct }
94-- Non-const-qualified 'bare' action and move (without ID):
95local am_ctype_bare = { action=ffi.typeof("struct action"), move=ffi.typeof("struct move") }
96
97-- CODEDUP lunacon.lua
98local function truetab(tab)
99    local ttab = {}
100    for i=1,#tab do
101        ttab[tab[i]] = true
102    end
103    return ttab
104end
105
106-- KEEPINSYNC lunacon.lua
107local ALLOWED_VIEWTYPE = truetab { 0, 1, 2, 3,4, 5, 7, 8, -5, -7, -8 }
108
109local function def_action_or_move(what, tab)
110    if (lastid[what] <= -(2^31)) then
111        error("Too many "..what.."s defined", 3);
112    end
113
114    bcheck.top_level(what, 4)
115
116    -- NOTE: tab[0]~=nil check for "Special default values" below.
117    if (type(tab) ~= "table" or tab[0]~=nil) then
118        error("invalid argument to con."..what..": must be a table", 3)
119    end
120
121    -- Pass args table to ffi.new, which can take either: a table with numeric
122    -- indices, or a table with key-value pairs, *but not in combination*.
123    -- See http://luajit.org/ext_ffi_semantics.html#init_table
124    local am = am_ctype_bare[what](tab)
125
126    -- Now, set all string keys as they have been ignored if tab[1] was
127    -- non-nil.
128    for key, val in pairs(tab) do
129        if (type(key)=="string") then
130            am[key] = val
131        end
132    end
133
134    if (what=="action") then
135        -- Special default values or checking of actor members.
136        -- KEEPINSYNC with ACTOR_CHECK in lunacon.lua for consistency.
137        local numframes = tab[2] or tab.numframes
138        local viewtype = tab[3] or tab.viewtype
139        local incval = tab[4] or tab.incval
140
141        if (numframes==nil) then
142            am.numframes = 1
143        else
144            check_number(numframes, 4)
145            if (numframes < 0) then
146                error("action has negative number of frames", 3)
147            end
148        end
149
150        if (viewtype==nil) then
151            am.viewtype = 1
152        else
153            check_number(viewtype, 4)
154            if (ALLOWED_VIEWTYPE[viewtype] == nil) then
155                error("action has disallowed viewtype "..viewtype, 3)
156            end
157        end
158
159        if (incval==nil) then
160            am.incval = 1
161        end
162    end
163
164    -- Named actions or moves have negative ids so that non-negative ones
165    -- can be used as (different) placeholders for all-zero ones.
166    lastid[what] = lastid[what]-1
167
168    return am_ctype_full_const[what](lastid[what], am)
169end
170
171---=== ACTION/MOVE/AI FUNCTIONS ===---
172
173function action(tab)
174    return def_action_or_move("action", tab)
175end
176
177function move(tab)
178    return def_action_or_move("move", tab)
179end
180
181-- Get action or move for an 'ai' definition.
182local function get_action_or_move(what, val, argi)
183    if (val == nil) then
184        return literal_am[what][0]
185    elseif (ffi.istype(am_ctype_full_const[what], val)) then
186        return val
187    elseif (type(val)=="number") then
188        if (val==0 or val==1) then
189            return literal_am[what][val]
190        end
191    end
192
193    error("bad argument #"..argi.." to ai: must be nil/nothing, 0, 1, or "..what, 3)
194end
195
196function ai(action, move, flags)
197    bcheck.top_level("ai")
198
199    if (lastid.ai <= -(2^31)) then
200        error("Too many AIs defined", 2);
201    end
202
203    local act = get_action_or_move("action", action, 2)
204    local mov = get_action_or_move("move", move, 3)
205
206    if (flags~=nil) then
207        if (type(flags)~="number" or not (flags>=0 and flags<=32767)) then
208            error("bad argument #4 to ai: must be a number in [0..32767]", 2)
209        end
210    else
211        flags = 0
212    end
213
214    lastid.ai = lastid.ai-1
215    return con_ai_ct(lastid.ai, act, mov, flags)
216end
217
218
219---=== RUNTIME CON FUNCTIONS ===---
220
221-- Will contain [<label>]=number mappings after CON translation.
222local D = { true }
223
224
225local function krandand(mask)
226    return band(ffiC.krand(), mask)
227end
228
229local function check_allnumbers(...)
230    local vals = {...}
231    for i=1,#vals do
232        assert(type(vals[i])=="number")
233    end
234end
235
236
237-- Table of all non-NODEFAULT per-actor gamevars active in the system.
238-- [<actorvar reference>] = true
239local g_actorvar = setmetatable({}, { __mode="k" })
240
241local function A_ResetVars(i)
242    for acv in pairs(g_actorvar) do
243        acv:_clear(i)
244    end
245end
246
247ffiC.A_ResetVars = A_ResetVars
248
249-- Reset per-actor gamevars for the sprite that would be inserted by the next
250-- insertsprite() call.
251-- TODO_MP (Net_InsertSprite() is not handled)
252--
253-- NOTE: usually, a particular actor's code doesn't use ALL per-actor gamevars,
254-- so there should be a way to clear only a subset of them (maybe those that
255-- were defined in "its" module?).
256local function A_ResetVarsNextIns()
257    -- KEEPINSYNC with insertsprite() logic in engine.c!
258    local i = ffiC.headspritestat[ffiC.MAXSTATUS]
259    if (i < 0) then
260        return
261    end
262
263    ffiC.g_noResetVars = 1
264    return A_ResetVars(i)
265end
266
267
268-- Lunatic's "insertsprite" is a wrapper around the game "A_InsertSprite", not
269-- the engine "insertsprite".
270--
271-- Forms:
272--  1. table-call: insertsprite{tilenum, pos, sectnum [, statnum [, owner]] [, key=val...]}
273--     valid keys are: owner, statnum, shade, xrepeat, yrepeat, xvel, zvel
274--  2. position-call: insertsprite(tilenum, pos, sectnum [, statnum [, owner]])
275function insertsprite(tab_or_tilenum, ...)
276    local tilenum, pos, sectnum  -- mandatory
277    -- optional with defaults:
278    local owner, statnum
279    local shade, xrepeat, yrepeat, ang, xvel, zvel = 0, 48, 48, 0, 0, 0
280
281    if (type(tab_or_tilenum)=="table") then
282        local tab = tab_or_tilenum
283        tilenum, pos, sectnum = unpack(tab, 1, 3)
284        statnum = tab[4] or tab.statnum or 0
285        owner = tab[5] or tab.owner or -1
286        shade = tab.shade or shade
287        xrepeat = tab.xrepeat or xrepeat
288        yrepeat = tab.yrepeat or yrepeat
289        ang = tab.ang or ang
290        xvel = tab.xvel or xvel
291        zvel = tab.zvel or zvel
292    else
293        tilenum = tab_or_tilenum
294        local args = {...}
295        pos, sectnum = unpack(args, 1, 2)
296        statnum = args[3] or 0
297        owner = args[4] or -1
298    end
299
300    if (type(sectnum)~="number" or type(tilenum) ~= "number") then
301        error("invalid insertsprite call: 'sectnum' and 'tilenum' must be numbers", 2)
302    end
303
304    check_tile_idx(tilenum)
305    check_sector_idx(sectnum)
306    check_allnumbers(shade, xrepeat, yrepeat, ang, xvel, zvel, owner)
307    if (owner ~= -1) then
308        check_sprite_idx(owner)
309    end
310
311    if (not (statnum >= 0 and statnum < ffiC.MAXSTATUS)) then
312        error("invalid 'statnum' argument to insertsprite: must be a status number [0 .. MAXSTATUS-1]", 2)
313    end
314
315    A_ResetVarsNextIns()
316
317    local i = CF.A_InsertSprite(sectnum, pos.x, pos.y, pos.z, tilenum,
318                                shade, xrepeat, yrepeat, ang, xvel, zvel,
319                                owner, statnum)
320    if (owner == -1) then
321        ffiC.sprite[i]:_set_owner(i)
322    end
323    return i
324end
325
326-- INTERNAL USE ONLY.
327function _addtodelqueue(spritenum)
328    check_sprite_idx(spritenum)
329    CF.A_AddToDeleteQueue(spritenum)
330end
331
332-- This corresponds to the first (spawn from parent sprite) form of A_Spawn().
333function spawn(tilenum, parentspritenum, addtodelqueue)
334    check_tile_idx(tilenum)
335    check_sprite_idx(parentspritenum)
336
337    if (addtodelqueue and ffiC.g_deleteQueueSize == 0) then
338        return -1
339    end
340
341    A_ResetVarsNextIns()
342
343    local i = CF.A_Spawn(parentspritenum, tilenum)
344    if (addtodelqueue) then
345        CF.A_AddToDeleteQueue(i)
346    end
347    return i
348end
349
350-- This is the second A_Spawn() form. INTERNAL USE ONLY.
351function _spawnexisting(spritenum)
352    check_sprite_idx(spritenum)
353    return CF.A_Spawn(-1, spritenum)
354end
355
356-- A_SpawnMultiple clone
357-- ow: parent sprite number
358function _spawnmany(ow, label, n)
359    local tilenum = D[label]
360    if (tilenum ~= nil) then
361        local spr = sprite[ow]
362
363        for i=n,1, -1 do
364            local j = insertsprite{ tilenum, spr^(ffiC.krand()%(47*256)), spr.sectnum, 5, ow,
365                                    shade=-32, xrepeat=8, yrepeat=8, ang=krandand(2047) }
366            _spawnexisting(j)
367            sprite[j].cstat = krandand(8+4)
368        end
369    end
370end
371
372local int16_st = ffi.typeof "struct { int16_t s; }"
373
374-- Get INT32_MIN for the following constant; passing 0x80000000 would be
375-- out of the range for an int32_t and thus undefined behavior!
376local SHOOT_HARDCODED_ZVEL = tobit(0x80000000)
377
378function shoot(tilenum, i, zvel)
379    check_sprite_idx(i)
380    check_sector_idx(ffiC.sprite[i].sectnum)  -- accessed in A_ShootWithZvel
381    check_tile_idx(tilenum)
382
383    zvel = zvel and int16_st(zvel).s or SHOOT_HARDCODED_ZVEL
384
385    return CF.A_ShootWithZvel(i, tilenum, zvel)
386end
387
388local BADGUY_MASK = bor(con_lang.SFLAG.SFLAG_HARDCODED_BADGUY, con_lang.SFLAG.SFLAG_BADGUY)
389
390function isenemytile(tilenum)
391    return (band(ffiC.g_tile[tilenum]._flags, BADGUY_MASK)~=0)
392end
393
394-- The 'rotatesprite' wrapper used by the CON commands.
395function _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation,
396                 alpha, cx1, cy1, cx2, cy2)
397    check_tile_idx(tilenum)
398    orientation = band(orientation, 4095)  -- ROTATESPRITE_MAX-1
399
400    if (band(orientation, 2048) == 0) then  -- ROTATESPRITE_FULL16
401        x = 65536*x
402        y = 65536*y
403    end
404
405    local blendidx = 0
406    if (alpha < 0) then
407        -- See NEG_ALPHA_TO_BLEND.
408        blendidx = -alpha
409        alpha = 0
410        orientation = bor(orientation, 1)  -- RS_TRANS1
411    end
412
413    ffiC.rotatesprite_(x, y, zoom, ang, tilenum, shade, pal, bor(2,orientation),
414                       alpha, blendidx, cx1, cy1, cx2, cy2)
415end
416
417-- The external legacy tile drawing function for Lunatic.
418function rotatesprite(x, y, zoom, ang, tilenum, shade, pal, orientation,
419                      alpha, cx1, cy1, cx2, cy2)
420    -- Disallow <<16 coordinates from Lunatic. They only unnecessarily increase
421    -- complexity; you already have more precision in the FP number fraction.
422    if (band(orientation, 2048) ~= 0) then
423        error('left-shift-by-16 coordinates forbidden', 2)
424    end
425
426    return _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation,
427                   alpha, cx1, cy1, cx2, cy2)
428end
429
430function _myos(x, y, zoom, tilenum, shade, orientation, pal)
431    if (pal==nil) then
432        local sect = player[ffiC.screenpeek].cursectnum
433        pal = (sect>=0) and sector[sect].floorpal or 0
434    end
435
436    ffiC.VM_DrawTileGeneric(x, y, zoom, tilenum, shade, orientation, pal)
437end
438
439function _inittimer(ticspersec)
440    if (not (ticspersec >= 1)) then
441        error("ticspersec must be >= 1", 2)
442    end
443    ffiC.G_InitTimer(ticspersec)
444end
445
446function _gettimedate()
447    local v = ffi.new("int32_t [8]")
448    ffiC.G_GetTimeDate(v)
449    return v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]
450end
451
452function rnd(x)
453    return (rshift(ffiC.krand(), 8) >= (255-x))
454end
455
456--- Legacy operators ---
457
458function _rand(x)
459    return floor((ffiC.krand()*(x+1))/65536)
460end
461
462function _displayrand(x)
463    return floor((math.random(0, 32767)*(x+1))/32768)
464end
465
466do
467    -- Arithmetic operations --
468    local INT32_MIN = tobit(0x80000000)
469    local INT32_MAX = tobit(0x7fffffff)
470
471    -- Trapping multiplication.
472    function _mulTR(a,b)
473        local c = a*b
474        if (not (c >= INT32_MIN and c <= INT32_MAX)) then
475            error("overflow in multiplication", 2)
476        end
477        return c
478    end
479
480    -- Wrapping multiplication.
481    function _mulWR(a,b)
482        -- XXX: problematic if a*b in an infinity or NaN.
483        return tobit(a*b)
484    end
485
486    function _div(a,b)
487        if (b==0) then
488            error("divide by zero", 2)
489        end
490        -- NOTE: don't confuse with math.modf!
491        return (a - math.fmod(a,b))/b
492    end
493
494    function _mod(a,b)
495        if (b==0) then
496            error("mod by zero", 2)
497        end
498        return (math.fmod(a,b))
499    end
500end
501
502-- Sect_ToggleInterpolation() clone
503function _togglesectinterp(sectnum, doset)
504    for w in wallsofsect(sectnum) do
505        ffiC.G_ToggleWallInterpolation(w, doset)
506
507        local nw = wall[w].nextwall
508        if (nw >= 0) then
509            ffiC.G_ToggleWallInterpolation(nw, doset)
510            ffiC.G_ToggleWallInterpolation(wall[nw].point2, doset)
511        end
512    end
513end
514
515-- Support for translated CON code: get cached sprite, actor and player structs
516-- (-fcache-sap option).
517function _getsap(aci, pli)
518    return (aci>=0) and sprite[aci], (aci>=0) and actor[aci], (pli>=0) and player[pli]
519end
520
521function _get_userdef_check(pli)
522    if (pli ~= ffiC.myconnectindex) then
523        error(format("userdefs access with non-local current player %d (we: %d)",
524                     pli, ffiC.myconnectindex), 2)
525    end
526    return ffiC.ud
527end
528
529function _get_userdef(pli)
530    return ffiC.ud
531end
532
533function _err_if_negative(val)
534    if (not (val >= 0)) then
535        error("setting tag to negative value", 2)
536    end
537    return val
538end
539
540--- player/actor/sprite searching functions ---
541
542local xmath = require("xmath")
543local abs = math.abs
544local bangvec, kangvec = xmath.bangvec, xmath.kangvec
545local dist, ldist = xmath.dist, xmath.ldist
546local vec3, ivec3 = xmath.vec3, xmath.ivec3
547local rotate = xmath.rotate
548
549local function A_FP_ManhattanDist(ps, spr)
550    local distvec = ps.pos - spr^(28*256)
551    return distvec:touniform():mhlen()
552end
553
554-- Returns: player index, distance
555-- TODO_MP
556function _findplayer(pli, spritenum)
557    return 0, A_FP_ManhattanDist(player[pli], sprite[spritenum])
558end
559
560local STAT = actor.STAT
561
562local FN_STATNUMS = {
563    [false] = { STAT.ACTOR },
564    [true] = {},
565}
566
567-- TODO: Python-like range() and xrange()?
568for i=0,ffiC.MAXSTATUS-1 do
569    FN_STATNUMS[true][i+1] = ffiC.MAXSTATUS-1-i
570end
571
572local FN_DISTFUNC = {
573    d2 = function(s1, s2, d)
574        return (ldist(s1, s2) < d)
575    end,
576
577    d3 = function(s1, s2, d)
578        return (dist(s1, s2) < d)
579    end,
580
581    z = function(s1, s2, d, zd)
582        return (ldist(s1, s2) < d and abs(s1.z-s2.z) < zd)
583    end,
584}
585
586function _findnear(spritenum, allspritesp, distkind, picnum, maxdist, maxzdist)
587    local statnums = FN_STATNUMS[allspritesp]
588    local distfunc = FN_DISTFUNC[distkind]
589    local spr = sprite[spritenum]
590
591    for _,st in ipairs(statnums) do
592        for i in spritesofstat(st) do
593            if (i ~= spritenum and sprite[i].picnum==picnum) then
594                if (distfunc(spr, sprite[i], maxdist, maxzdist)) then
595                    return i
596                end
597            end
598        end
599    end
600
601    return -1
602end
603
604
605---=== Weapon stuff ===---
606
607
608--- Helper functions (might be exported later) ---
609
610local function have_ammo_at_max(ps, weap)
611    return (ps.ammo_amount[weap] >= ps.max_ammo_amount[weap])
612end
613
614function _tossweapon(pli)  -- P_DropWeapon replacement
615    -- NOTE: We're passing player index, C-CON passes APLAYER sprite.
616    check_player_idx(pli)
617    local ps = ffiC.g_player[pli].ps
618
619    bcheck.weapon_idx(ps.curr_weapon)
620    local cw = ffiC.g_playerWeapon[pli][ps.curr_weapon].workslike
621
622    if (cw >= ffiC.MAX_WEAPONS+0ULL) then
623        return
624    end
625
626    if (krandand(1) ~= 0) then
627        spawn(ffiC.WeaponPickupSprites[cw], ps.i)
628    elseif (cw==WEAPON.RPG or cw==WEAPON.HANDBOMB) then
629        if (D.EXPLOSION2 ~= nil) then
630            spawn(D.EXPLOSION2, ps.i)
631        end
632    end
633end
634
635local function P_AddAmmo(ps, weap, amount)
636    if (not have_ammo_at_max(ps, weap)) then
637        local curamount = ps.ammo_amount[weap]
638        local maxamount = ps.max_ammo_amount[weap]
639        -- NOTE: no clamping towards the bottom
640        ps.ammo_amount[weap] = math.min(curamount+amount, maxamount)
641    end
642end
643
644local function P_AddWeaponAmmoCommon(ps, weap, amount)
645    P_AddAmmo(ps, weap, amount)
646
647    if (ps.curr_weapon==WEAPON.KNEE and ps:has_weapon(weap)) then
648        CF.P_AddWeaponMaybeSwitchI(ps.weapon._p, weap);
649    end
650end
651
652
653--- Functions that must be exported because they are used by LunaCON generated code,
654--- but which are off limits to users.  (That is, we need to think about how to
655--- expose the functionality in a better fashion than merely giving access to
656--- the C functions.)
657--- TODO: Move these to a separate module like "con_private".
658
659-- quotes
660local REALMAXQUOTES = con_lang.REALMAXQUOTES
661local MAXQUOTELEN = con_lang.MAXQUOTELEN
662
663-- CON redefinequote command
664function _definequote(qnum, quotestr)
665    -- NOTE: this is more permissive than C-CON: we allow to redefine quotes
666    -- that were not previously defined.
667    bcheck.quote_idx(qnum, true)
668    assert(type(quotestr)=="string")
669    ffiC.C_DefineQuote(qnum, quotestr)
670    return (#quotestr >= MAXQUOTELEN)
671end
672
673function _quote(pli, qnum)
674    bcheck.quote_idx(qnum)
675    check_player_idx(pli)
676    ffiC.P_DoQuote(qnum+REALMAXQUOTES, ffiC.g_player[pli].ps)
677end
678
679function _echo(qnum)
680    local cstr = bcheck.quote_idx(qnum)
681    ffiC.OSD_Printf("%s\n", cstr)
682end
683
684function _userquote(qnum)
685    local cstr = bcheck.quote_idx(qnum)
686    -- NOTE: G_AddUserQuote strcpy's the string
687    ffiC.G_AddUserQuote(cstr)
688end
689
690local function strlen(cstr)
691    for i=0,math.huge do
692        if (cstr[i]==0) then
693            return i
694        end
695    end
696    assert(false)
697end
698
699-- NOTE: dst==src is OK (effectively a no-op)
700local function strcpy(dst, src)
701    local i=-1
702    repeat
703        i = i+1
704        dst[i] = src[i]
705    until (src[i]==0)
706end
707
708function _qstrlen(qnum)
709    return strlen(bcheck.quote_idx(qnum))
710end
711
712function _qsubstr(qdst, qsrc, start, length)
713    local cstr_dst = bcheck.quote_idx(qdst)
714    local cstr_src = bcheck.quote_idx(qsrc)
715
716    if (not (start >= 0 and start < MAXQUOTELEN)) then
717        error("invalid start position "..start, 2)
718    end
719
720    if (not (length >= 0)) then  -- NOTE: no check for start+length!
721        error("invalid length "..length, 2)
722    end
723
724    local si = 0
725    while (cstr_src[si] ~= 0 and si < start) do
726        si = si+1
727    end
728
729    for i=0,math.huge do
730        cstr_dst[i] = cstr_src[si + i]
731
732        if (si + i == MAXQUOTELEN-1 or i==length or cstr_dst[i] == 0) then
733            cstr_dst[i] = 0
734            break
735        end
736    end
737end
738
739function _qstrcpy(qdst, qsrc)
740    local cstr_dst = bcheck.quote_idx(qdst)
741    local cstr_src = bcheck.quote_idx(qsrc)
742    strcpy(cstr_dst, cstr_src)
743end
744
745-- NOTE: qdst==qsrc is OK (duplicates the quote)
746function _qstrcat(qdst, qsrc, n)
747    local cstr_dst = bcheck.quote_idx(qdst)
748    local cstr_src = bcheck.quote_idx(qsrc)
749
750    if (cstr_src[0]==0) then
751        return
752    end
753
754    if (cstr_dst[0]==0) then
755        return strcpy(cstr_dst, cstr_src)
756    end
757
758    if (n == nil) then
759        n = 0x7fffffff
760    elseif (n < 0) then
761        error("invalid number of chars to concatenate: "..n, 2)
762    end
763
764    -- From here on: destination and source quote (potentially aliased) are
765    -- nonempty.
766
767    local slen_dst = strlen(cstr_dst)
768    assert(slen_dst <= MAXQUOTELEN-1)
769
770    if (slen_dst == MAXQUOTELEN-1) then
771        return
772    end
773
774    local i = slen_dst
775    local j = 0
776
777    repeat
778        -- NOTE: don't copy the first char yet, so that the qdst==qsrc case
779        -- works correctly.
780        n = n-1
781        i = i+1
782        j = j+1
783        cstr_dst[i] = cstr_src[j]
784    until (n == 0 or i >= MAXQUOTELEN-1 or cstr_src[j]==0)
785
786    -- Now copy the first char!
787    cstr_dst[slen_dst] = cstr_src[0]
788    cstr_dst[i] = 0
789end
790
791local buf = ffi.new("char [?]", MAXQUOTELEN)
792
793function _qsprintf(qdst, qsrc, ...)
794    -- NOTE: more permissive than C-CON, see _definequote
795    if (bcheck.quote_idx(qdst, true) == nil) then
796        ffiC.C_DefineQuote(qdst, "")  -- allocate quote
797    end
798
799    local dst = bcheck.quote_idx(qdst)
800    local src = bcheck.quote_idx(qsrc)
801    local vals = {...}
802
803    local i, j, vi = 0, 0, 1
804
805    while (true) do
806        local ch = src[j]
807        local didfmt = false
808
809        if (ch==0) then
810            break
811        end
812
813        if (ch==byte'%') then
814            local nch = src[j+1]
815            if (nch==byte'd' or (nch==byte'l' and src[j+2]==byte'd')) then
816                -- number
817                didfmt = true
818
819                if (vi > #vals) then
820                    break
821                end
822
823                local numstr = tostring(vals[vi])
824                assert(type(numstr)=="string")
825                vi = vi+1
826
827                local ncopied = math.min(#numstr, MAXQUOTELEN-1-i)
828                ffi.copy(buf+i, numstr, ncopied)
829
830                i = i+ncopied
831                j = j+1+(nch==byte'd' and 1 or 2)
832            elseif (nch==byte's') then
833                -- string
834                didfmt = true
835                if (vi > #vals) then
836                    break
837                end
838
839                local k = -1
840                local tmpsrc = bcheck.quote_idx(vals[vi])
841                vi = vi+1
842
843                i = i-1
844                repeat
845                    i = i+1
846                    k = k+1
847                    buf[i] = tmpsrc[k]
848                until (i >= MAXQUOTELEN-1 or tmpsrc[k]==0)
849
850                j = j+2
851            end
852        end
853
854        if (not didfmt) then
855            buf[i] = src[j]
856            i = i+1
857            j = j+1
858        end
859
860        if (i >= MAXQUOTELEN-1) then
861            break
862        end
863    end
864
865    buf[i] = 0
866    strcpy(dst, buf)
867end
868
869function _getkeyname(qdst, gfuncnum, which)
870    local cstr_dst = bcheck.quote_idx(qdst)
871
872    if (not (gfuncnum >= 0 and gfuncnum < ffiC.NUMGAMEFUNCTIONS)) then
873        error("invalid game function number "..gfuncnum, 2)
874    end
875
876    if (not (which >= 0 and which < 3)) then
877        error("third argument to getkeyname must be 0, 1 or 2", 2)
878    end
879
880    local cstr_src
881
882    for i = (which==2 and 0 or which), (which==2 and 1 or which) do
883        local scancode = ffiC.ud.config.KeyboardKeys[gfuncnum][i]
884        cstr_src = ffiC.KB_ScanCodeToString(scancode)
885        if (cstr_src[0] ~= 0) then
886            break
887        end
888    end
889
890    if (cstr_src[0] ~= 0) then
891        -- All key names are short, no problem strcpy'ing them
892        strcpy(cstr_dst, cstr_src)
893    end
894end
895
896local EDUKE32_VERSION_STR = "EDuke32 2.0.0devel "..ffi.string(ffiC.s_buildRev)
897
898local function quote_strcpy(dst, src)
899    local i=-1
900    repeat
901        i = i+1
902        dst[i] = src[i]
903    until (src[i]==0 or i==MAXQUOTELEN-1)
904    dst[i] = 0
905end
906
907function _qgetsysstr(qdst, what, pli)
908    local dst = bcheck.quote_idx(qdst)
909
910    local idx = ffiC.ud.volume_number*con_lang.MAXLEVELS + ffiC.ud.level_number
911    local MAXIDX = ffi.sizeof(ffiC.g_mapInfo) / ffi.sizeof(ffiC.g_mapInfo[0])
912    local mapnamep = (what == ffiC.STR_MAPNAME)
913
914    if (mapnamep or what == ffiC.STR_MAPFILENAME) then
915        assert(not (idx >= MAXIDX+0ULL))
916        local src = mapnamep and ffiC.g_mapInfo[idx].name or ffiC.g_mapInfo[idx].filename
917        if (src == nil) then
918            error(format("attempted access to %s of non-existent map (vol=%d, lev=%d)",
919                         mapnamep and "name" or "file name",
920                         ffiC.ud.volume_number, ffiC.ud.level_number), 2)
921        end
922        quote_strcpy(dst, src)
923    elseif (what == ffiC.STR_PLAYERNAME) then
924        check_player_idx(pli)
925        ffi.copy(dst, ffiC.g_player[pli].user_name, ffi.sizeof(ffiC.g_player[0].user_name))
926    elseif (what == ffiC.STR_VERSION) then
927        ffi.copy(dst, EDUKE32_VERSION_STR)
928    elseif (what == ffiC.STR_GAMETYPE) then
929        ffi.copy(dst, "multiplayer not yet implemented")  -- TODO_MP
930    elseif (what == ffiC.STR_VOLUMENAME) then
931        local vol = ffiC.ud.volume_number
932        bcheck.volume_idx(vol)
933        ffi.copy(dst, ffiC.g_volumeNames[vol], ffi.sizeof(ffiC.g_volumeNames[0]))
934    elseif (what == ffiC.STR_YOURTIME) then
935        ffi.copy(dst, ffi.string(ffiC.G_PrintYourTime()))
936    elseif (what == ffiC.STR_PARTIME) then
937        ffi.copy(dst, ffi.string(ffiC.G_PrintParTime()))
938    elseif (what == ffiC.STR_DESIGNERTIME) then
939        ffi.copy(dst, ffi.string(ffiC.G_PrintDesignerTime()))
940    elseif (what == ffiC.STR_BESTTIME) then
941        ffi.copy(dst, ffi.string(ffiC.G_PrintBestTime()))
942    else
943        error("unknown system string ID "..what, 2)
944    end
945end
946
947function _getpname(qnum, pli)
948    bcheck.quote_idx(qnum, true)
949    check_player_idx(pli)
950    local uname = ffiC.g_player[pli].user_name
951    ffiC.C_DefineQuote(qnum, (uname[0] ~= 0) and uname or tostring(pli))
952end
953
954
955-- switch statement support
956function _switch(swtab, testval, aci,pli,dst)
957    local func = swtab[testval] or swtab.default
958    if (func) then
959        func(aci, pli, dst)
960    end
961end
962
963
964--== Text rendering ==--
965
966-- For external use. NOTE: <pal> comes before <shade>.
967function minitext(x, y, str, pal, shade)
968    check_type(str, "string")
969    ffiC.minitext_(x, y, str, shade or 0, pal or 0, 2+8+16)
970end
971
972-- For CON only.
973function _minitext(x, y, qnum, shade, pal)
974    local cstr = bcheck.quote_idx(qnum)
975    ffiC.minitext_(x, y, cstr, shade, pal, 2+8+16)
976end
977
978function _digitalnumber(tilenum, x, y, num, shade, pal,
979                        orientation, cx1, cy1, cx2, cy2, zoom)
980    if (not (tilenum >= 0 and tilenum < ffiC.MAXTILES-9)) then
981        error("invalid base tile number "..tilenum, 2)
982    end
983
984    ffiC.G_DrawTXDigiNumZ(tilenum, x, y, num, shade, pal,
985                          orientation, cx1, cy1, cx2, cy2, zoom)
986end
987
988local function text_check_common(tilenum, orientation)
989    if (not (tilenum >= 0 and tilenum < ffiC.MAXTILES-255)) then
990        error("invalid base tile number "..tilenum, 3)
991    end
992
993    return band(orientation, 4095)  -- ROTATESPRITE_MAX-1
994end
995
996function _gametext(tilenum, x, y, qnum, shade, pal, orientation,
997                   cx1, cy1, cx2, cy2, zoom)
998    orientation = text_check_common(tilenum, orientation)
999    local cstr = bcheck.quote_idx(qnum)
1000
1001    ffiC.G_PrintGameText(tilenum, bit.arshift(x,1), y, cstr, shade, pal,
1002                         orientation, cx1, cy1, cx2, cy2, zoom)
1003end
1004-- XXX: JIT-compiling FFI calls to G_PrintGameText crashes LuaJIT somewhere in
1005-- its internal routines.  I'm not sure who is to blame here but I suspect we
1006-- have some undefined behavior somewhere.  Reproducible with DukePlus 2.35 on
1007-- x86 when clicking wildly through its menu.
1008jit.off(_gametext)
1009
1010function _screentext(tilenum, x, y, z, blockangle, charangle, q, shade, pal, orientation,
1011                     alpha, xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
1012    orientation = text_check_common(tilenum, orientation)
1013    local cstr = bcheck.quote_idx(q)
1014
1015    ffiC.G_ScreenText(tilenum, x, y, z, blockangle, charangle, cstr, shade, pal, bor(2,orientation),
1016                      alpha, xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
1017end
1018
1019function _qstrdim(tilenum, x, y, z, blockangle, q, orientation,
1020                  xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2)
1021    orientation = text_check_common(tilenum, orientation)
1022    local cstr = bcheck.quote_idx(q)
1023
1024    local dim = ffiC.G_ScreenTextSize(tilenum, x, y, z, blockangle, cstr, orientation,
1025                                      xspace, yline, xbetween, ybetween, f, x1, y1, x2, y2);
1026    return dim.x, dim.y
1027end
1028
1029function _showview(x, y, z, a, horiz, sect, x1, y1, x2, y2, unbiasedp)
1030    check_sector_idx(sect)
1031
1032    if (x1 < 0 or y1 < 0 or x2 >= 320 or y2 >= 200 or x2 < x1 or y2 < y1) then
1033        local str = format("(%d,%d)--(%d,%d)", x1, y1, x2, y2)
1034        error("invalid coordinates "..str, 2)
1035    end
1036
1037    CF.G_ShowViewXYZ(x, y, z, a, horiz, sect, x1, y1, x2, y2, unbiasedp);
1038end
1039
1040
1041---=== DEFINED LABELS ===---
1042
1043-- Check if <picnum> equals to the number defined by <label> from CON.
1044-- If there is no such label, return nil.
1045local function ispic(picnum, label)
1046    return D[label] and (picnum==D[label])
1047end
1048
1049-- Which tiles should use .yvel instead of .hitag for the respawned tile?
1050-- Will be [<number>] = true after CON translation.
1051local RESPAWN_USE_YVEL = {
1052    "STATUE", "NAKED1", "PODFEM1", "FEM1", "FEM2",
1053    "FEM3", "FEM5", "FEM4", "FEM6", "FEM8",
1054    "FEM7", "FEM9", "FEM10",
1055}
1056
1057-- Is an inventory tile?
1058-- Will be [<number>] = true after CON translation.
1059local INVENTILE = {
1060    "FIRSTAID", "STEROIDS", "AIRTANK", "JETPACK", "HEATSENSOR",
1061    "BOOTS", "HOLODUKE",
1062}
1063
1064local function totruetab(tab)
1065    local numelts = #tab
1066    for i=1,numelts do
1067        local label = tab[i]
1068        if (D[label]) then
1069            tab[label] = true
1070        end
1071        tab[i] = nil
1072    end
1073end
1074
1075-- This will be run after CON has been translated.
1076function _setuplabels(conlabels)
1077    assert(D[1])  -- Allow running this function exactly once.
1078    D = conlabels
1079    totruetab(RESPAWN_USE_YVEL)
1080    totruetab(INVENTILE)
1081end
1082
1083
1084function _A_DoGuts(i, gutstile, n)
1085    check_tile_idx(gutstile)
1086    local spr = sprite[i]
1087    local smallguts = spr.xrepeat < 16 and spr:isenemy()
1088    local xsz = smallguts and 8 or 32
1089    local ysz = xsz
1090    local z = math.min(spr.z, sector[spr.sectnum]:floorzat(spr)) - 8*256
1091
1092    if (ispic(spr.picnum, "COMMANDER")) then
1093        z = z - (24*256)
1094    end
1095
1096    for i=n,1, -1 do
1097        local pos = vec3(spr.x+krandand(255)-128, spr.y+krandand(255)-128, z-krandand(8191))
1098        local j = insertsprite{ gutstile, pos, spr.sectnum, 5, i, shade=-32, xrepeat=xsz, yrepeat=ysz,
1099                                ang=krandand(2047), xvel=48+krandand(31), zvel=-512-krandand(2047) }
1100        local newspr = sprite[j]
1101        if (ispic(newspr.picnum, "JIBS2")) then
1102            -- This looks silly, but EVENT_EGS code could have changed the size
1103            -- between the insertion and here.
1104            newspr.xrepeat = newspr.xrepeat/4
1105            newspr.yrepeat = newspr.yrepeat/4
1106        end
1107        newspr.pal = spr.pal
1108    end
1109end
1110
1111function _debris(i, dtile, n)
1112    local spr = sprite[i]
1113    if (spr.sectnum >= ffiC.numsectors+0ULL) then
1114        return
1115    end
1116
1117    for j=n-1,0, -1 do
1118        local isblimpscrap = (ispic(spr.picnum, "BLIMP") and ispic(dtile, "SCRAP1"))
1119        local picofs = isblimpscrap and 0 or krandand(3)
1120        local pos = spr + vec3(krandand(255)-128, krandand(255)-128, -(8*256)-krandand(8191))
1121        local jj = insertsprite{ dtile+picofs, pos, spr.sectnum, 5, i,
1122                                 shade=spr.shade, xrepeat=32+krandand(15), yrepeat=32+krandand(15),
1123                                 ang=krandand(2047), xvel=32+krandand(127), zvel=-krandand(2047) }
1124        -- NOTE: g_blimpSpawnItems[14] (its array size is 15) will never be chosen
1125        sprite[jj]:set_yvel(isblimpscrap and ffiC.g_blimpSpawnItems[math.mod(jj, 14)] or -1)
1126        sprite[jj].pal = spr.pal
1127    end
1128end
1129
1130function _A_SpawnGlass(i, n)
1131    if (D.GLASSPIECES) then
1132        local spr = sprite[i]
1133
1134        for j=n,1, -1 do
1135            local k = insertsprite{ D.GLASSPIECES+n%3, spr^(256*krandand(16)), spr.sectnum, 5, i,
1136                                    shade=krandand(15), xrepeat=36, yrepeat=36, ang=krandand(2047),
1137                                    xvel=32+krandand(63), zvel=-512-krandand(2047) }
1138            sprite[k].pal = spr.pal
1139        end
1140    end
1141end
1142
1143function _A_IncurDamage(sn)
1144    check_sprite_idx(sn)
1145    return ffiC.A_IncurDamage(sn)
1146end
1147
1148function _sizeto(i, xr, yr)
1149    local spr = sprite[i]
1150    local dr = (xr-spr.xrepeat)
1151    -- NOTE: could "overflow" (e.g. goal repeat is 256, gets converted to 0)
1152    spr.xrepeat = spr.xrepeat + ((dr == 0) and 0 or (dr < 0 and -1 or 1))
1153    -- TODO: y stretching is conditional
1154    dr = (yr-spr.yrepeat)
1155    spr.yrepeat = spr.yrepeat + ((dr == 0) and 0 or (dr < 0 and -1 or 1))
1156end
1157
1158function _pstomp(ps, i)
1159    if (ps.knee_incs == 0 and sprite[ps.i].xrepeat >= 40) then
1160        local spr = sprite[i]
1161        if (cansee(spr^(4*256), spr.sectnum, ps.pos^(-16*256), sprite[ps.i].sectnum)) then
1162            for j=ffiC.g_mostConcurrentPlayers-1,0 do
1163                if (player[j].actorsqu == i) then
1164                    return
1165                end
1166            end
1167            ps.actorsqu = i
1168            ps.knee_incs = 1
1169            if (ps.weapon_pos == 0) then
1170                ps.weapon_pos = -1
1171            end
1172        end
1173    end
1174end
1175
1176function _pkick(ps, spr)
1177    -- TODO_MP
1178    if (not ispic(spr.picnum, "APLAYER") and ps.quick_kick==0) then
1179        ps.quick_kick = 14
1180    end
1181end
1182
1183function _VM_ResetPlayer2(snum, flags)
1184    check_player_idx(snum)
1185    return (CF.VM_ResetPlayer2(snum, flags)~=0)
1186end
1187
1188local PALBITS = { [0]=1, [21]=2, [23]=4 }
1189local ICONS = {
1190    [GET.FIRSTAID] = 1,  -- ICON_FIRSTAID
1191    [GET.STEROIDS] = 2,
1192    [GET.HOLODUKE] = 3,
1193    [GET.JETPACK] = 4,
1194    [GET.HEATS] = 5,
1195    [GET.SCUBA] = 6,
1196    [GET.BOOTS] = 7,
1197}
1198
1199function _addinventory(ps, inv, amount, i)
1200    if (inv == GET.ACCESS) then
1201        local pal = sprite[i].pal
1202        if (PALBITS[pal]) then
1203            ps.got_access = bor(ps.got_access, PALBITS[pal])
1204        end
1205    else
1206        if (ICONS[inv]) then
1207            ps.inven_icon = ICONS[inv]
1208        end
1209
1210        if (inv == GET.SHIELD) then
1211            amount = math.min(ps.max_shield_amount, amount)
1212        end
1213        -- NOTE: this is more permissive than CON, e.g. allows
1214        -- GET_DUMMY1 too.
1215        ps.inv_amount[inv] = amount
1216    end
1217end
1218
1219function _checkpinventory(ps, inv, amount, i)
1220    if (inv==GET.SHIELD) then
1221        return ps.inv_amount[inv] ~= ps.max_shield_amount
1222    elseif (inv==GET.ACCESS) then
1223        local palbit = PALBITS[sprite[i].pal]
1224        return palbit and (band(ps.got_access, palbit)~=0)
1225    else
1226        return ps.inv_amount[inv] ~= amount
1227    end
1228end
1229
1230local INV_SELECTION_ORDER = {
1231    GET.FIRSTAID,
1232    GET.STEROIDS,
1233    GET.JETPACK,
1234    GET.HOLODUKE,
1235    GET.HEATS,
1236    GET.SCUBA,
1237    GET.BOOTS,
1238}
1239
1240-- checkavailinven CON command
1241function _selectnextinv(ps)
1242    for _,inv in ipairs(INV_SELECTION_ORDER) do
1243        if (ps.inv_amount[inv] > 0) then
1244            ps.inven_icon = ICONS[inv]
1245            return
1246        end
1247    end
1248
1249    ps.inven_icon = 0
1250end
1251
1252function _checkavailweapon(pli)
1253    check_player_idx(pli)
1254    CF.P_CheckWeaponI(pli)
1255end
1256
1257function _addphealth(ps, aci, hlthadd)
1258    if (ps.newowner >= 0) then
1259        ffiC.G_ClearCameraView(ps)
1260    end
1261
1262    if (ffiC.ud.god ~= 0) then
1263        return
1264    end
1265
1266    local notatomic = not ispic(sprite[aci].picnum, "ATOMICHEALTH")
1267    local j = sprite[ps.i].extra
1268
1269    if (notatomic and j > ps.max_player_health and hlthadd > 0) then
1270        return
1271    end
1272
1273    if (j > 0) then
1274        j = j + hlthadd
1275    end
1276
1277    if (notatomic) then
1278        if (hlthadd > 0) then
1279            j = math.min(j, ps.max_player_health)
1280        end
1281    else
1282        j = math.min(j, 2*ps.max_player_health)
1283    end
1284
1285    j = math.max(j, 0)
1286
1287    if (hlthadd > 0) then
1288        local qmaxhlth = rshift(ps.max_player_health, 2)
1289        if (j-hlthadd < qmaxhlth and j >= qmaxhlth) then
1290            -- XXX: DUKE_GOTHEALTHATLOW
1291            _sound(aci, 229)
1292        end
1293
1294        ps.last_extra = j
1295    end
1296
1297    sprite[ps.i].extra = j
1298end
1299
1300-- The return value is true iff the ammo was at the weapon's max.
1301-- In that case, no action is taken.
1302function _addammo(ps, weap, amount)
1303    return have_ammo_at_max(ps, weap) or P_AddWeaponAmmoCommon(ps, weap, amount)
1304end
1305
1306function _addweapon(ps, weap, amount)
1307    bcheck.weapon_idx(weap)
1308
1309    if (not ps:has_weapon(weap)) then
1310        CF.P_AddWeaponMaybeSwitchI(ps.weapon._p, weap);
1311    elseif (have_ammo_at_max(ps, weap)) then
1312        return true
1313    end
1314
1315    P_AddWeaponAmmoCommon(ps, weap, amount)
1316end
1317
1318function _A_RadiusDamage(i, r, hp1, hp2, hp3, hp4)
1319    check_sprite_idx(i)
1320    check_allnumbers(r, hp1, hp2, hp3, hp4)
1321    CF.A_RadiusDamage(i, r, hp1, hp2, hp3, hp4)
1322end
1323
1324local NEAROP = {
1325    [9] = true,
1326    [15] = true,
1327    [16] = true,
1328    [17] = true,
1329    [18] = true,
1330    [19] = true,
1331    [20] = true,
1332    [21] = true,
1333    [22] = true,
1334    [23] = true,
1335    [25] = true,
1336    [26] = true,
1337    [29] = true,
1338}
1339
1340function _operate(spritenum)
1341    local spr = sprite[spritenum]
1342
1343    if (sector[spr.sectnum].lotag == 0) then
1344        local tag = neartag(spr^(32*256), spr.sectnum, spr.ang, 768, 4+1)
1345        if (tag.sector >= 0) then
1346            local sect = sector[tag.sector]
1347            local lotag = sect.lotag
1348            local lotag_lo = band(lotag, 0xff)
1349
1350            if (NEAROP[lotag_lo]) then
1351                if (lotag_lo == 23 or sect.floorz == sect.ceilingz) then
1352                    if (band(lotag, 32768+16384) == 0) then
1353                        for j in spritesofsect(tag.sector) do
1354                            if (ispic(sprite[j].picnum, "ACTIVATOR")) then
1355                                return
1356                            end
1357                        end
1358                        CF.G_OperateSectors(tag.sector, spritenum)
1359                    end
1360                end
1361            end
1362        end
1363    end
1364end
1365
1366function _operatesectors(sectnum, spritenum)
1367    check_sector_idx(sectnum)
1368    check_sprite_idx(spritenum)  -- XXX: -1 permissible under certain circumstances?
1369    CF.G_OperateSectors(sectnum, spritenum)
1370end
1371
1372function _operateactivators(tag, playernum)
1373    check_player_idx(playernum)
1374    -- NOTE: passing oob playernum would be safe because G_OperateActivators
1375    -- bound-checks it
1376    assert(type(tag)=="number")
1377    CF.G_OperateActivators(tag, playernum)
1378end
1379
1380function _activatebysector(sectnum, spritenum)
1381    local didit = false
1382    for i in spriteofsect(sectnum) do
1383        if (ispic(sprite[i].picnum, "ACTIVATOR")) then
1384            CF.G_OperateActivators(sprite[i].lotag, -1)
1385        end
1386    end
1387    if (didit) then
1388        _operatesectors(sectnum, spritenum)
1389    end
1390end
1391
1392function _checkactivatormotion(tag)
1393    return ffiC.G_CheckActivatorMotion(tag)
1394end
1395
1396function _endofgame(pli, timebeforeexit)
1397    player[pli].timebeforeexit = timebeforeexit
1398    player[pli].customexitsound = -1
1399    ffiC.ud.eog = 1
1400end
1401
1402function _bulletnear(i)
1403    return (ffiC.A_Dodge(sprite[i]) == 1)
1404end
1405
1406-- d is a distance
1407function _awayfromwall(spr, d)
1408    local vec2 = xmath.vec2
1409    local vecs = { vec2(d,d), vec2(-d,-d), vec2(d,-d), vec2(-d,d) }
1410    for i=1,4 do
1411        if (not inside(vecs[i]+spr, spr.sectnum)) then
1412            return false
1413        end
1414    end
1415    return true
1416end
1417
1418-- TODO: xmath.vec3 'mhlen2' method?
1419local function manhatdist(v1, v2)
1420    return abs(v1.x-v2.x) + abs(v1.y-v2.y)
1421end
1422
1423-- "otherspr" is either player or holoduke sprite
1424local function A_FurthestVisiblePoint(aci, otherspr)
1425    if (band(actor[aci]:get_count(), 63) ~= 0) then
1426        return
1427    end
1428
1429    -- TODO_MP
1430    local angincs = (ffiC.ud.player_skill < 3) and 1024 or 2048/(1+krandand(1))
1431    local spr = sprite[aci]
1432
1433    local j = 0
1434    repeat
1435        local ray = kangvec(otherspr.ang + j, 16384-krandand(32767))
1436        local hit = hitscan(otherspr^(16*256), otherspr.sectnum, ray, ffiC.CLIPMASK1)
1437        local dother = manhatdist(hit.pos, otherspr)
1438        local dactor = manhatdist(hit.pos, spr)
1439
1440        if (dother < dactor and hit.sector >= 0) then
1441            if (cansee(hit.pos, hit.sector, spr^(16*256), spr.sectnum)) then
1442                return hit
1443            end
1444        end
1445
1446        j = j + (angincs - krandand(511))
1447    until (j >= 2048)
1448end
1449
1450local MAXSLEEPDIST = 16384
1451local SLEEPTIME = 1536
1452
1453function _cansee(aci, ps)
1454    -- Select sprite for monster to target.
1455    local spr = sprite[aci]
1456    local s = sprite[ps.i]
1457
1458    -- This is kind of redundant, but points the error messages to the CON code.
1459    check_sector_idx(spr.sectnum)
1460    check_sector_idx(s.sectnum)
1461
1462    if (ps.holoduke_on >= 0) then
1463        -- If holoduke is on, let them target holoduke first.
1464        local hs = sprite[ps.holoduke_on]
1465
1466        if (cansee(spr^krandand(8191), spr.sectnum, s, s.sectnum)) then
1467            s = hs
1468        end
1469    end
1470
1471    -- Can they see player (or player's holoduke)?
1472    local can = cansee(spr^krandand(47*256), spr.sectnum, s^(24*256), s.sectnum)
1473
1474    if (not can) then
1475        -- Search around for target player.
1476        local hit = A_FurthestVisiblePoint(aci, s)
1477        if (hit ~= nil) then
1478            can = true
1479            actor[aci].lastvx = hit.pos.x
1480            actor[aci].lastvy = hit.pos.y
1481        end
1482    else
1483        -- Else, they did see it. Save where we were looking...
1484        actor[aci].lastvx = s.x
1485        actor[aci].lastvy = s.y
1486    end
1487
1488    if (can and (spr.statnum==STAT.ACTOR or spr.statnum==STAT.STANDABLE)) then
1489        actor[aci].timetosleep = SLEEPTIME
1490    end
1491
1492    return can
1493end
1494
1495function _isvalid(i)
1496    check_sprite_idx(i)
1497    return ffiC.sprite[i].statnum ~= ffiC.MAXSTATUS and 1 or 0
1498end
1499
1500function _canseespr(s1, s2)
1501    check_sprite_idx(s1)
1502    check_sprite_idx(s2)
1503
1504    local spr1, spr2 = ffiC.sprite[s1], ffiC.sprite[s2]
1505    -- Redundant, but points the error messages to the CON code:
1506    check_sector_idx(spr1.sectnum)
1507    check_sector_idx(spr2.sectnum)
1508
1509    return cansee(spr1, spr1.sectnum, spr2, spr2.sectnum) and 1 or 0
1510end
1511
1512-- TODO: replace ivec3 allocations with stores to a static ivec3, like in
1513-- updatesector*?
1514
1515-- CON "hitscan" command
1516function _hitscan(x, y, z, sectnum, vx, vy, vz, cliptype)
1517    local srcv = ivec3(x, y, z)
1518    local ray = ivec3(vx, vy, vz)
1519    local hit = hitscan(srcv, sectnum, ray, cliptype)
1520    return hit.sector, hit.wall, hit.sprite, hit.pos.x, hit.pos.y, hit.pos.z
1521end
1522
1523-- CON "neartag" command
1524function _neartag(x, y, z, sectnum, ang, range, tagsearch)
1525    local pos = ivec3(x, y, z)
1526    local near = neartag(pos, sectnum, ang, range, tagsearch)
1527    return near.sector, near.wall, near.sprite, near.dist
1528end
1529
1530-- CON "getzrange" command
1531function _getzrange(x, y, z, sectnum, walldist, clipmask)
1532    check_sector_idx(sectnum)
1533    local ipos = ivec3(x, y, z)
1534    local hit = sector[sectnum]:zrangeat(ipos, walldist, clipmask)
1535    -- return: ceilz, ceilhit, florz, florhit
1536    return hit.c.z, hit.c.num + (hit.c.spritep and 49152 or 16384),
1537           hit.f.z, hit.f.num + (hit.f.spritep and 49152 or 16384)
1538end
1539
1540-- CON "clipmove" and "clipmovenoslide" commands
1541function _clipmovex(x, y, z, sectnum, xv, yv, wd, cd, fd, clipmask, noslidep)
1542    check_sector_idx(sectnum)
1543    local ipos = ivec3(x, y, z)
1544    local sect = ffi.new("int16_t [1]", sectnum)
1545    local ret = ffiC.clipmovex(ipos, sect, xv, yv, wd, cd, fd, clipmask, noslidep)
1546    -- Return: clipmovex() return value; updated x, y, sectnum
1547    return ret, ipos.x, ipos.y, sect[0]
1548end
1549
1550function _sleepcheck(aci, dst)
1551    local acs = actor[aci]
1552    if (dst > MAXSLEEPDIST and acs.timetosleep == 0) then
1553        acs.timetosleep = SLEEPTIME
1554    end
1555end
1556
1557function _canseetarget(spr, ps)
1558    -- NOTE: &41 ?
1559    return cansee(spr^(256*krandand(41)), spr.sectnum,
1560                  ps.pos, sprite[ps.i].sectnum)
1561end
1562
1563function _movesprite(spritenum, x, y, z, cliptype)
1564    check_sprite_idx(spritenum)
1565    local vel = ivec3(x, y, z)
1566    return ffiC.A_MoveSpriteClipdist(spritenum, vel, cliptype, -1)
1567end
1568
1569-- Also known as A_SetSprite().
1570function _ssp(i, cliptype)
1571    check_sprite_idx(i)
1572    local spr = ffiC.sprite[i]
1573    local vec = spr.xvel * bangvec(spr.ang)  -- XXX: slightly different rounding?
1574    local ivec = vec:toivec3()
1575    ivec.z = spr.zvel
1576
1577    return (ffiC.A_MoveSpriteClipdist(i, ivec, cliptype, -1)==0)
1578end
1579
1580-- CON's 'setsprite' function on top of the Lunatic-provided ones.
1581-- (Lunatic's sprite setting functions have slightly different semantics.)
1582local updatesect = sprite.updatesect
1583function _setsprite(i, pos)
1584    check_sprite_idx(i)
1585    local spr = ffiC.sprite[i]
1586
1587    -- First, unconditionally set the sprite's position.
1588    spr:setpos(pos)
1589
1590    -- Next, update the sector number, but if updatesector() returns -1, don't
1591    -- change it. (This is exactly what sprite.updatesect() provides.)
1592    updatesect(i)
1593end
1594
1595-- NOTE: returns two args (in C version, hit sprite is a pointer input arg)
1596local function A_CheckHitSprite(spr, angadd)
1597    local zoff = (spr:isenemy() and 42*256) or (ispic(spr.picnum, "APLAYER") and 39*256) or 0
1598
1599    local hit = hitscan(spr^zoff, spr.sectnum, kangvec(spr.ang+angadd), ffiC.CLIPMASK1)
1600    if (hit.wall >= 0 and wall[hit.wall]:ismasked() and spr:isenemy()) then
1601        return -1, nil
1602    end
1603
1604    return hit.sprite, ldist(hit.pos, spr)
1605end
1606
1607function _canshoottarget(dst, aci)
1608    if (dst > 1024) then
1609        local spr = sprite[aci]
1610
1611        local hitspr, hitdist = A_CheckHitSprite(spr, 0)
1612        if (hitdist == nil) then
1613            return true
1614        end
1615
1616        local bigenemy = (spr:isenemy() and spr.xrepeat > 56)
1617
1618        local sclip = bigenemy and 3084 or 768
1619        local angdif = bigenemy and 48 or 16
1620
1621        local sclips = { sclip, sclip, 768 }
1622        local angdifs = { 0, angdif, -angdif }
1623
1624        for i=1,3 do
1625            if (i > 1) then
1626                hitspr, hitdist = A_CheckHitSprite(spr, angdifs[i])
1627            end
1628
1629            if (hitspr >= 0 and sprite[hitspr].picnum == spr.picnum) then
1630                if (hitdist > sclips[i]) then
1631                    return false
1632                end
1633            end
1634        end
1635    end
1636
1637    return true
1638end
1639
1640function _getlastpal(spritenum)
1641    local spr = sprite[spritenum]
1642    if (ispic(spr.picnum, "APLAYER")) then
1643        local pidx = spr.yvel
1644        check_player_idx(pidx)
1645        spr.pal = player[pidx].palookup
1646    else
1647        if (spr.pal == 1 and spr.extra == 0) then  -- hack for frozen
1648            spr.extra = spr.extra+1
1649        end
1650        spr.pal = actor[spritenum].tempang
1651    end
1652    actor[spritenum].tempang = 0
1653end
1654
1655-- G_GetAngleDelta(a1, a2)
1656function _angdiff(a1, a2)
1657    a1 = band(a1, 2047)
1658    a2 = band(a2, 2047)
1659
1660    if (abs(a2-a1) >= 1024) then
1661        if (a2 > 1024) then a2 = a2 - 2048 end
1662        if (a1 > 1024) then a1 = a1 - 2048 end
1663    end
1664
1665    -- a1, a2 are in [-1023, 1024]
1666    return a2-a1
1667end
1668
1669function _angdiffabs(a1, a2)
1670    return abs(_angdiff(a1, a2))
1671end
1672
1673function _angtotarget(aci)
1674    local spr = sprite[aci]
1675    return ffiC.getangle(actor[aci].lastvx-spr.x, actor[aci].lastvy-spr.y)
1676end
1677
1678function _hypot(a, b)
1679    return math.sqrt(a*a + b*b)
1680end
1681
1682function _rotatepoint(pivotx, pivoty, posx, posy, ang)
1683    local pos = ivec3(posx, posy)
1684    local pivot = ivec3(pivotx, pivoty)
1685    pos = rotate(pos, ang, pivot):toivec3()
1686    return pos.x, pos.y
1687end
1688
1689local holdskey = player.holdskey
1690
1691function _ifp(flags, pli, aci)
1692    local l = flags
1693    local ps = player[pli]
1694    local vel = sprite[ps.i].xvel
1695
1696    if (band(l,8)~=0 and ps.on_ground and holdskey(pli, "CROUCH")) then
1697        return true
1698    elseif (band(l,16)~=0 and ps.jumping_counter == 0 and not ps.on_ground and ps.vel.z > 2048) then
1699        return true
1700    elseif (band(l,32)~=0 and ps.jumping_counter > 348) then
1701        return true
1702    elseif (band(l,1)~=0 and vel >= 0 and vel < 8) then
1703        return true
1704    elseif (band(l,2)~=0 and vel >= 8 and not holdskey(pli, "RUN")) then
1705        return true
1706    elseif (band(l,4)~=0 and vel >= 8 and holdskey(pli, "RUN")) then
1707        return true
1708    elseif (band(l,64)~=0 and ps.pos.z < (sprite[aci].z-(48*256))) then
1709        return true
1710    elseif (band(l,128)~=0 and vel <= -8 and not holdskey(pli, "RUN")) then
1711        return true
1712    elseif (band(l,256)~=0 and vel <= -8 and holdskey(pli, "RUN")) then
1713        return true
1714    elseif (band(l,512)~=0 and (ps.quick_kick > 0 or (ps.curr_weapon == 0 and ps.kickback_pic > 0))) then
1715        return true
1716    elseif (band(l,1024)~=0 and sprite[ps.i].xrepeat < 32) then
1717        return true
1718    elseif (band(l,2048)~=0 and ps.jetpack_on) then
1719        return true
1720    elseif (band(l,4096)~=0 and ps.inv_amount.STEROIDS > 0 and ps.inv_amount.STEROIDS < 400) then
1721        return true
1722    elseif (band(l,8192)~=0 and ps.on_ground) then
1723        return true
1724    elseif (band(l,16384)~=0 and sprite[ps.i].xrepeat > 32 and sprite[ps.i].extra > 0 and ps.timebeforeexit == 0) then
1725        return true
1726    elseif (band(l,32768)~=0 and sprite[ps.i].extra <= 0) then
1727        return true
1728    elseif (band(l,65536)~=0) then
1729        -- TODO_MP
1730        if (_angdiffabs(ps.ang, ffiC.getangle(sprite[aci].x-ps.pos.x, sprite[aci].y-ps.pos.y)) < 128) then
1731            return true
1732        end
1733    end
1734
1735    return false
1736end
1737
1738function _squished(aci, pli)
1739    check_sprite_idx(aci)
1740    check_player_idx(pli)
1741    check_sector_idx(sprite[aci].sectnum)
1742
1743    return (ffiC.VM_CheckSquished2(aci, pli)~=0)
1744end
1745
1746function _checkspace(sectnum, floorp)
1747    local sect = sector[sectnum]
1748    local picnum = floorp and sect.floorpicnum or sect.ceilingpicnum
1749    local stat = floorp and sect.floorstat or sect.ceilingstat
1750    return band(stat,1)~=0 and sect.ceilingpal == 0 and
1751        (ispic(picnum, "MOONSKY1") or ispic(picnum, "BIGORBIT1"))
1752end
1753
1754function _flash(spr, ps)
1755   spr.shade = -127
1756   ps.visibility = -127  -- NOTE: negative value not a problem anymore
1757end
1758
1759function _G_OperateRespawns(tag)
1760    for i in spritesofstat(STAT.FX) do
1761        local spr = sprite[i]
1762
1763        if (spr.lotag==tag and ispic(spr.picnum, "RESPAWN")) then
1764            if (ffiC.ud.monsters_off==0 or not isenemytile(spr.hitag)) then
1765                if (D.TRANSPORTERSTAR) then
1766                    local j = spawn(D.TRANSPORTERSTAR, i)
1767                    sprite[j].z = sprite[j].z - (32*256)
1768                end
1769
1770                -- Just a way to killit (see G_MoveFX(): RESPAWN__STATIC)
1771                spr.extra = 66-12
1772            end
1773        end
1774    end
1775end
1776
1777function _G_OperateMasterSwitches(tag)
1778    for i in spritesofstat(STAT.STANDABLE) do
1779        local spr = sprite[i]
1780        if (ispic(spr.picnum, "MASTERSWITCH") and spr.lotag==tag and spr.yvel==0) then
1781            spr:set_yvel(1)
1782        end
1783    end
1784end
1785
1786function _respawnhitag(spr)
1787    if (RESPAWN_USE_YVEL[spr.picnum]) then
1788        if (spr.yvel ~= 0) then
1789            _G_OperateRespawns(spr.yvel)
1790        end
1791    else
1792        _G_OperateRespawns(spr.hitag)
1793    end
1794end
1795
1796function _checkrespawn(spr)
1797    if (spr:isenemy()) then
1798        return (ffiC.ud.respawn_monsters~=0)
1799    end
1800    if (INVENTILE[spr.picnum]) then
1801        return (ffiC.ud.respawn_inventory~=0)
1802    end
1803    return (ffiC.ud.respawn_items~=0)
1804end
1805
1806-- SOUNDS
1807function _ianysound(aci)
1808    check_sprite_idx(aci)
1809    return (ffiC.A_CheckAnySoundPlaying(aci)~=0)
1810end
1811
1812function _sound(aci, sndidx)
1813    check_sprite_idx(aci)
1814    -- A_PlaySound() returns early if the sound index is oob, but IMO it's good
1815    -- style to throw an error instead of silently failing.
1816    check_sound_idx(sndidx)
1817    CF.A_PlaySound(sndidx, aci)
1818end
1819
1820-- NOTE: This command is really badly named in CON. It issues a sound that
1821-- emanates from the current player instead of being 'system-global'.
1822function _globalsound(pli, sndidx)
1823    -- TODO: conditional on coop, fake multimode
1824    if (pli==ffiC.screenpeek) then
1825        _sound(player[pli].i, sndidx)
1826    end
1827end
1828
1829-- This one unconditionally plays a session-wide sound.
1830function _screensound(sndidx)
1831    check_sound_idx(sndidx)
1832    CF.A_PlaySound(sndidx, -1)
1833end
1834
1835-- This is a macro for EDuke32 (game.h)
1836local function S_StopSound(sndidx)
1837    ffiC.S_StopEnvSound(sndidx, -1)
1838end
1839
1840function _soundplaying(aci, sndidx)
1841    if (aci ~= -1) then
1842        check_sprite_idx(aci)
1843    end
1844    check_sound_idx(sndidx)
1845    return (ffiC.S_CheckSoundPlaying(aci, sndidx) ~= 0)
1846end
1847
1848function _stopsound(aci, sndidx)
1849    -- XXX: This is weird: the checking is done wrt a sprite, but the sound not.
1850    -- NOTE: S_StopSound() stops sound <sndidx> that started playing most recently.
1851    if (_soundplaying(aci, sndidx)) then
1852        S_StopSound(sndidx)
1853    end
1854end
1855
1856function _stopactorsound(aci, sndidx)
1857    if (_soundplaying(aci, sndidx)) then
1858        ffiC.S_StopEnvSound(sndidx, aci)
1859    end
1860end
1861
1862function _soundonce(aci, sndidx)
1863    if (not _soundplaying(aci, sndidx)) then
1864        _sound(aci, sndidx)
1865    end
1866end
1867
1868function _stopallsounds(pli)
1869    if (ffiC.screenpeek==pli) then
1870        ffiC.S_StopAllSounds()
1871    end
1872end
1873
1874function _setactorsoundpitch(aci, sndidx, pitchoffset)
1875    check_sprite_idx(aci)
1876    check_sound_idx(sndidx)
1877    ffiC.S_ChangeSoundPitch(sndidx, aci, pitchoffset)
1878end
1879
1880function _starttrack(level)
1881    bcheck.level_idx(level)
1882
1883    if (ffiC.G_StartTrack(level) ~= 0) then
1884        -- Issue a 'soft error', not breaking the control flow.
1885        local errmsg = debug.traceback(
1886            format("null music for volume %d level %d", ffiC.ud.volume_number, level), 2)
1887        errmsg = lprivate.tweak_traceback_msg(errmsg)
1888        ffiC.El_OnError(errmsg)
1889        print("^10error: "..errmsg)
1890    end
1891end
1892
1893function _getmusicposition()
1894    return ffiC.S_GetMusicPosition()
1895end
1896
1897function _setmusicposition(position)
1898    ffiC.S_SetMusicPosition(position)
1899end
1900
1901function _startlevel(volume, level)
1902    bcheck.volume_idx(volume)
1903    bcheck.level_idx(level)
1904
1905    ffiC.ud.m_volume_number = volume
1906    ffiC.ud.volume_number = volume
1907    ffiC.ud.m_level_number = level
1908    ffiC.ud.level_number = level
1909
1910    ffiC.ud.display_bonus_screen = 0
1911
1912    -- TODO_MP
1913    player[0].gm = bor(player[0].gm, 0x00000008)  -- MODE_EOL
1914end
1915
1916function _setaspect(viewingrange, yxaspect)
1917    if (viewingrange==0) then
1918        error('invalid argument #1: must be nonzero', 2)
1919    end
1920    if (yxaspect==0) then
1921        error('invalid argument #2: must be nonzero', 2)
1922    end
1923
1924    -- XXX: surely not all values are sane
1925    ffiC.setaspect(viewingrange, yxaspect)
1926end
1927
1928function _setgamepalette(pli, basepal)
1929    check_player_idx(pli)
1930    ffiC.P_SetGamePalette(ffiC.g_player_ps[pli], basepal, 2+16)
1931end
1932
1933-- Map state persistence.
1934function _savemapstate()
1935    ffiC.G_SaveMapState()
1936end
1937
1938function _loadmapstate()
1939    ffiC.G_RestoreMapState()
1940end
1941
1942function _clearmapstate(idx)
1943    bcheck.linear_map_idx(idx)
1944    ffiC.G_FreeMapState(idx)
1945end
1946
1947-- Gamevar persistence in the configuration file
1948
1949function _savegamevar(name, val)
1950    if (ffiC.ud.config.scripthandle < 0) then
1951        return
1952    end
1953
1954    assert(type(name)=="string")
1955    assert(type(val)=="number")
1956
1957    ffiC.SCRIPT_PutNumber(ffiC.ud.config.scripthandle, "Gamevars", name,
1958                          val, 0, 0);
1959end
1960
1961function _readgamevar(name, ov)
1962    if (ffiC.ud.config.scripthandle < 0) then
1963        return ov
1964    end
1965
1966    assert(type(name)=="string")
1967
1968    local v = ffi.new("int32_t [1]")
1969    ffiC.SCRIPT_GetNumber(ffiC.ud.config.scripthandle, "Gamevars", name, v);
1970    -- NOTE: doesn't examine SCRIPT_GetNumber() return value and returns 0 if
1971    -- there was no such gamevar saved, like C-CON.
1972    return v[0]
1973end
1974
1975
1976--- Wrapper of kopen4load file functions in a Lua-like file API
1977-- TODO: move to common side?
1978
1979local kfile_mt = {
1980    __gc = function(self)
1981        self:close()
1982    end,
1983
1984    __index = {
1985        close = function(self)
1986            if (self.fd > 0) then
1987                ffiC.kclose(self.fd)
1988                self.fd = -1
1989            end
1990        end,
1991
1992        seek = function(self, whence, offset)
1993            local w = whence=="set" and 0  -- SEEK_SET
1994                or whence=="end" and 2  -- SEEK_END
1995                or error("invalid 'whence' for seek", 2)  -- "cur" NYI
1996
1997            local pos = ffiC.klseek(self.fd, offset or 0, w)
1998
1999            if (pos >= 0) then
2000                return pos
2001            else
2002                return nil, "?"
2003            end
2004        end,
2005
2006        read = function(self, nbytes)
2007            assert(type(nbytes)=="number")  -- other formats NYI
2008            assert(nbytes > 0)
2009
2010            local bytes = ffi.new("char [?]", nbytes)
2011            local bytesread = ffiC.kread(self.fd, bytes, nbytes)
2012
2013            if (bytesread ~= nbytes) then
2014                return nil
2015            end
2016
2017            return ffi.string(bytes, nbytes)
2018        end,
2019
2020        -- Read <nints> little-endian 32-bit integers.
2021        read_le_int32 = function(self, nints)
2022            local ints = ffi.new("int32_t [?]", nints)
2023            local bytesread = ffiC.kread(self.fd, ints, nints*4)
2024
2025            if (bytesread ~= nints*4) then
2026                return nil
2027            end
2028
2029            if (ffi.abi("be")) then
2030                for i=0,nints-1 do
2031                    ints[i] = bit.bswap(ints[i])
2032                end
2033            end
2034
2035            return ints
2036        end,
2037    },
2038}
2039
2040local kfile_t = ffi.metatype("struct { int32_t fd; }", kfile_mt)
2041
2042local function kopen4load(fn, searchfirst)
2043    local fd = ffiC.kopen4load(fn, searchfirst)
2044
2045    if (fd < 0) then
2046        return nil, "no such file?"
2047    end
2048
2049    return kfile_t(fd)
2050end
2051
2052
2053local function serialize_value(strtab, i, v)
2054    -- Save only user values (i.e. not 'meta-fields' like '_size').
2055    if (type(i)=="number" and v~=nil) then
2056        strtab[#strtab+1] = "["..i.."]="..tostring(v)..","
2057    end
2058end
2059
2060-- Common serialization function for gamearray and actorvar.
2061local function serialize_array(ar, strtab, maxnum, suffix)
2062    for i=0,maxnum-1 do
2063        serialize_value(strtab, i, rawget(ar, i))
2064    end
2065
2066    strtab[#strtab+1] = "}"..(suffix or "")..")"
2067
2068    return table.concat(strtab)
2069end
2070
2071
2072--- Game arrays ---
2073
2074local function moddir_filename(cstr_fn)
2075    local fn = ffi.string(cstr_fn)
2076    local moddir = ffi.string(ffiC.g_modDir);
2077
2078    if (moddir=="/") then
2079        return fn
2080    else
2081        return format("%s/%s", moddir, fn)
2082    end
2083end
2084
2085local GAR_FOOTER = "\001\002EDuke32GameArray\003\004"
2086local GAR_FOOTER_SIZE = #GAR_FOOTER
2087
2088local function gamearray_file_common(qnum, writep)
2089    -- NOTE: suffix with '.gar' so that we can have C-CON and LunaCON gamearray
2090    -- files side-by-side.
2091    local fn = moddir_filename(bcheck.quote_idx(qnum))..".gar"
2092    local f, errmsg
2093
2094    if (writep) then
2095        f, errmsg = io.open(fn, "rb")
2096        if (f == nil) then
2097            -- file, numints, isnewgar, filename
2098            return nil, nil, true, fn
2099        end
2100    else
2101        f, errmsg = kopen4load(fn, 0)
2102        if (f == nil) then
2103            if (f==false) then
2104                error(format([[failed opening "%s" for reading: %s]], fn, errmsg), 3)
2105            else
2106                return
2107            end
2108        end
2109    end
2110
2111    local fsize = assert(f:seek("end"))
2112
2113    local isnewgar = false
2114    if (fsize >= GAR_FOOTER_SIZE) then
2115        assert(f:seek("end", -GAR_FOOTER_SIZE))
2116        isnewgar = (assert(f:read(GAR_FOOTER_SIZE)) == GAR_FOOTER)
2117        if (isnewgar) then
2118            fsize = fsize - GAR_FOOTER_SIZE
2119        end
2120    end
2121
2122    return f, floor(fsize/4), isnewgar, fn
2123end
2124
2125local function check_gamearray_idx(gar, idx, addstr)
2126    -- If the actual table has no "_size" field, then we're dealing with a
2127    -- system gamearray: currently, only g_tile.sizx/sizy.
2128    local size = rawget(gar, '_size') or ffiC.MAXTILES
2129
2130    if (not (idx >= 0 and idx < size)) then
2131        addstr = addstr or ""
2132        error("invalid "..addstr.."array index "..idx, 3)
2133    end
2134end
2135
2136function _gar_copy(sar, sidx, dar, didx, numelts)
2137    -- XXX: Strictest bound checking, see later if we need to relax it.
2138    check_gamearray_idx(sar, sidx, "lower source ")
2139    check_gamearray_idx(sar, sidx+numelts-1, "upper source ")
2140    check_gamearray_idx(dar, didx, "lower destination ")
2141    check_gamearray_idx(dar, didx+numelts-1, "upper destination ")
2142
2143    -- Source is user gamearray?
2144    local sisuser = (rawget(sar, '_size') ~= nil)
2145
2146    for i=0,numelts-1 do
2147        local val = sisuser and rawget(sar, sidx+i) or sar[sidx+i]
2148        rawset(dar, didx+i, val)
2149    end
2150end
2151
2152local gamearray_methods = {
2153    resize = function(gar, newsize)
2154        -- NOTE: size 0 is valid (then, no index is valid)
2155        if (newsize < 0) then
2156            error("invalid new array size "..newsize, 2)
2157        end
2158
2159        local MAXELTS = floor(0x7fffffff/4)
2160        if (newsize > MAXELTS) then
2161            -- mainly for some sanity with kread() (which we don't use, but still)
2162            error("new array size "..newsize.." too large (max="..MAXELTS.." elements)", 2)
2163        end
2164
2165        -- clear trailing elements in case we're shrinking
2166        for i=gar._size,newsize-1 do
2167            rawset(gar, i, nil)
2168        end
2169
2170        gar._size = newsize
2171    end,
2172
2173    read = function(gar, qnum)
2174        local f, nelts, isnewgar = gamearray_file_common(qnum, false)
2175
2176        if (f==nil) then
2177            return
2178        end
2179
2180        assert(f:seek("set"))
2181        local ints = f:read_le_int32(nelts)
2182        if (ints == nil) then
2183            error("failed reading whole file into gamearray", 2)
2184        end
2185
2186        gar:resize(nelts)
2187
2188        for i=0,nelts-1 do
2189            rawset(gar, i, (ints[i]==0) and nil or ints[i])
2190        end
2191
2192        f:close()
2193    end,
2194
2195    write = function(gar, qnum)
2196        local f, _, isnewgar, fn = gamearray_file_common(qnum, true)
2197
2198        if (f ~= nil) then
2199            f:close()
2200        end
2201
2202        if (not isnewgar) then
2203            error("refusing to overwrite a file not created by a previous `writearraytofile'", 2)
2204        end
2205
2206        local f, errmsg = io.open(fn, "wb+")
2207        if (f == nil) then
2208            error([[failed opening "%s" for writing: %s]], fn, errmsg, 3)
2209        end
2210
2211        local nelts = gar._size
2212        local ar = ffi.new("int32_t [?]", nelts)
2213        local isbe = ffi.abi("be")  -- is big-endian?
2214
2215        for i=0,nelts-1 do
2216            ar[i] = isbe and bit.bswap(gar[i]) or gar[i]
2217        end
2218
2219        local ok = (ffiC.fwrite(ar, 4, nelts, f) == nelts)
2220        if (ok) then
2221            f:write(GAR_FOOTER)
2222        end
2223
2224        f:close()
2225
2226        if (not ok) then
2227            error([[failed writing all data to "%s"]], fn, 3)
2228        end
2229    end,
2230
2231
2232    --- Internal routines ---
2233
2234    --  * All values equal to the default one (0) are cleared.
2235    _cleanup = function(gar)
2236        for i=0,gar._size-1 do
2237            if (rawget(gar, i)==0) then
2238                rawset(gar, i, nil)
2239            end
2240        end
2241    end,
2242
2243
2244    --- Serialization ---
2245    _get_require = our_get_require,
2246
2247    _serialize = function(gar)
2248        gar:_cleanup()
2249        local strtab = { "_ga(", tostring(gar._size), ",{" }
2250        return serialize_array(gar, strtab, gar._size)
2251    end,
2252}
2253
2254local gamearray_mt = {
2255    __index = function(gar, key)
2256        if (type(key)=="number") then
2257            check_gamearray_idx(gar, key)
2258            return 0
2259        else
2260            return gamearray_methods[key]
2261        end
2262    end,
2263
2264    __newindex = function(gar, idx, val)
2265        check_gamearray_idx(gar, idx)
2266        rawset(gar, idx, val)
2267    end,
2268
2269    __metatable = "serializeable",
2270}
2271
2272-- Common constructor helper for gamearray and actorvar.
2273local function set_values_from_table(ar, values)
2274    if (values ~= nil) then
2275        for i,v in pairs(values) do
2276            ar[i] = v
2277        end
2278    end
2279    return ar
2280end
2281
2282-- NOTE: Gamearrays are internal because users are encouraged to use tables
2283-- from Lua code.
2284-- <values>: optional, a table of <index>=value
2285function _gamearray(size, values)
2286    local gar = setmetatable({ _size=size }, gamearray_mt)
2287    return set_values_from_table(gar, values)
2288end
2289
2290
2291--- More functions of the official API ---
2292
2293-- Non-local control flow. These ones call the original error(), not our
2294-- redefinition in _defs_game.lua.
2295function longjmp()
2296    error(false)
2297end
2298
2299function killit()
2300    -- TODO: guard against deletion of player sprite?
2301    error(true)
2302end
2303
2304
2305--== Per-actor variable ==--
2306local perxvar_allowed_types = {
2307    ["boolean"]=true, ["number"]=true,
2308}
2309
2310local function check_perxval_type(val)
2311    if (perxvar_allowed_types[type(val)] == nil) then
2312        error("type forbidden as per-* variable value: "..type(val), 3)
2313    end
2314end
2315
2316local actorvar_methods = {
2317    --- Internal routines ---
2318
2319    --  * All values for sprites not in the game world are cleared (non-NODEFAULT only).
2320    --  * All values equal to the default one are cleared.
2321    _cleanup = function(acv)
2322        for i=0,ffiC.MAXSPRITES-1 do
2323            -- NOTE: NODEFAULT per-actor gamevars are used in a non-actor fashion
2324            if ((not acv:_is_nodefault() and ffiC.sprite[i].statnum == ffiC.MAXSTATUS)
2325                    or rawget(acv, i)==acv._defval) then
2326                acv:_clear(i)
2327            end
2328        end
2329    end,
2330
2331    _clear = function(acv, i)
2332        rawset(acv, i, nil)
2333    end,
2334
2335    _is_nodefault = function(acv, i)
2336        return rawget(acv, '_nodefault')
2337    end,
2338
2339
2340    --- Serialization ---
2341    _get_require = our_get_require,
2342
2343    _serialize = function(acv)
2344        -- NOTE: We also clean up when spawning a sprite, too. (See
2345        -- A_ResetVars() and related functions above.)
2346        acv:_cleanup()
2347        local strtab = { "_av(", tostring(acv._defval), ",{" }
2348        return serialize_array(acv, strtab, ffiC.MAXSPRITES,
2349                               acv:_is_nodefault() and ",true")
2350    end,
2351}
2352
2353local actorvar_mt = {
2354    __index = function(acv, idx)
2355        if (type(idx)=="number") then
2356            check_sprite_idx(idx)
2357            return acv._defval
2358        else
2359            return actorvar_methods[idx]
2360        end
2361    end,
2362
2363    __newindex = function(acv, idx, val)
2364        check_sprite_idx(idx)
2365        check_perxval_type(val)
2366        rawset(acv, idx, val)
2367    end,
2368
2369    __metatable = "serializeable",
2370}
2371
2372-- <initval>: default value for per-actor variable.
2373-- <values>: optional, a table of <spritenum>=value
2374function actorvar(initval, values, nodefault)
2375    check_perxval_type(initval)
2376    local acv = setmetatable({ _defval=initval, _nodefault=nodefault }, actorvar_mt)
2377    if (not nodefault) then
2378        g_actorvar[acv] = true
2379    end
2380    return set_values_from_table(acv, values)
2381end
2382
2383
2384--== Per-player variable (kind of CODEDUP) ==--
2385local playervar_methods = {
2386    --- Serialization ---
2387    _get_require = our_get_require,
2388
2389    _serialize = function(plv)
2390        local strtab = { "_pv(", tostring(plv._defval), ",{" }
2391        return serialize_array(plv, strtab, ffiC.MAXSPRITES)
2392    end,
2393}
2394
2395local playervar_mt = {
2396    __index = function(plv, idx)
2397        if (type(idx)=="number") then
2398            check_player_idx(idx)
2399            return plv._defval
2400        else
2401            return playervar_methods[idx]
2402        end
2403    end,
2404
2405    __newindex = function(plv, idx, val)
2406        check_player_idx(idx)
2407        check_perxval_type(val)
2408        rawset(plv, idx, val)
2409    end,
2410
2411    __metatable = "serializeable",
2412}
2413
2414-- <initval>: default value for per-player variable.
2415-- <values>: optional, a table of <playeridx>=value
2416function playervar(initval, values)
2417    check_perxval_type(initval)
2418    local plv = setmetatable({ _defval=initval }, playervar_mt)
2419    return set_values_from_table(plv, values)
2420end
2421