-- Game control module for Lunatic. local require = require local ffi = require("ffi") local ffiC = ffi.C local jit = require("jit") -- Lua C API functions, this comes from El_PushCFunctions() in lunatic_game.c. local CF = CF local bit = require("bit") local io = require("io") local math = require("math") local table = require("table") local geom = require("geom") local bcheck = require("bcheck") local con_lang = require("con_lang") local byte = require("string").byte local setmetatable = setmetatable local assert = assert local error = error local ipairs = ipairs local pairs = pairs local print = print local rawget = rawget local rawset = rawset local tostring = tostring local type = type local unpack = unpack local format = require("string").format local actor, player = assert(actor), assert(player) local dc = require("defs_common") local cansee, hitscan, neartag = dc.cansee, dc.hitscan, dc.neartag local inside = dc.inside local sector, wall, sprite = dc.sector, dc.wall, dc.sprite local wallsofsect = dc.wallsofsect local spritesofsect, spritesofstat = dc.spritesofsect, dc.spritesofstat local OUR_NAME = "_con" local OUR_REQUIRE_STRING = "local "..OUR_NAME.."=require'con'" module(...) local lastid = { action=0, move=0, ai=0 } local def = { action = { NO=ffi.new("con_action_t") }, move = { NO=ffi.new("con_move_t") }, ai = { NO=ffi.new("con_ai_t") }, } local function forbidden() error("newindex forbidden", 2) end AC = setmetatable({}, { __index=def.action, __newindex=forbidden }) MV = setmetatable({}, { __index=def.move, __newindex=forbidden }) AI = setmetatable({}, { __index=def.ai, __newindex=forbidden }) local function check_name(name, what, errlev) if (type(name)~="string" or #name > 63) then error("bad argument #1 to "..what..": must be a string of length <= 63", errlev+1) end end local function action_or_move(what, numargs, tab, name, ...) if (lastid[what] <= -(2^31)) then error("Too many "..what.."s defined", 3); end check_name(name, what, 3) local args = {...} if (#args > numargs) then error("Too many arguments passed to "..what, 3) end for i=1,#args do local n = args[i] if (type(n)~="number" or not (n >= -32768 and n <= 32767)) then error("bad argument #".. i+1 .." to "..what.. ": must be numbers in [-32768..32767]", 3) end end -- missing fields are initialized to 0 by ffi.new -- Named actions or moves have negative ids so that non-negative ones -- can be used as (different) placeholders for all-zero ones. lastid[what] = lastid[what]-1 -- ffi.new takes either for initialization: varargs, a table with numeric -- indices, or a table with key-value pairs -- See http://luajit.org/ext_ffi_semantics.html#init_table tab[name] = ffi.new("const con_"..what.."_t", lastid[what], args) end ---=== ACTION / MOVE / AI ===--- function action(name, ...) bcheck.top_level("action") action_or_move("action", 5, def.action, name, ...) end function move(name, ...) bcheck.top_level("move") action_or_move("move", 2, def.move, name, ...) end -- Get action or move for an 'ai' definition. local function get_action_or_move(what, val, argi) if (val == nil) then return {} -- ffi.new will init the struct to all zeros elseif (type(val)=="string") then local am = def[what][val] if (am==nil) then error("no "..what.." '"..val.."' defined", 3) end return am elseif (ffi.istype("con_"..what.."_t", val)) then return val elseif (type(val)=="number") then if (val==0 or val==1) then -- Create an action or move with an ID of 0 or 1 but all other -- fields cleared. return ffi.new("con_"..what.."_t", val) end end error("bad argument #"..argi.." to ai: must be string or (literal) "..what, 3) end function ai(name, action, move, flags) bcheck.top_level("ai") if (lastid.ai <= -(2^31)) then error("Too many AIs defined", 2); end check_name(name, "ai", 2) lastid.ai = lastid.ai-1 local act = get_action_or_move("action", action, 2) local mov = get_action_or_move("move", move, 3) if (flags~=nil) then if (type(flags)~="number" or not (flags>=0 and flags<=32767)) then error("bad argument #4 to ai: must be a number in [0..32767]", 2) end else flags = 0 end def.ai[name] = ffi.new("const con_ai_t", lastid.ai, act, mov, flags) end ---=== RUNTIME CON FUNCTIONS ===--- local check_sector_idx = bcheck.sector_idx local check_tile_idx = bcheck.tile_idx local check_sprite_idx = bcheck.sprite_idx local check_player_idx = bcheck.player_idx local check_sound_idx = bcheck.sound_idx local function krandand(mask) return bit.band(ffiC.krand(), mask) end local function check_isnumber(...) local vals = {...} for i=1,#vals do assert(type(vals[i])=="number") end end -- Table of all per-actor gamevars active in the system. -- [] = true local g_actorvar = setmetatable({}, { __mode="k" }) local function A_ResetVars(i) for acv in pairs(g_actorvar) do acv:_clear(i) end end ffiC.A_ResetVars = A_ResetVars -- Reset per-actor gamevars for the sprite that would be inserted by the next -- insertsprite() call. -- TODO_MP (Net_InsertSprite() is not handled) -- -- NOTE: usually, a particular actor's code doesn't use ALL per-actor gamevars, -- so there should be a way to clear only a subset of them (maybe those that -- were defined in "its" module?). local function A_ResetVarsNextIns() -- KEEPINSYNC with insertsprite() logic in engine.c! local i = ffiC.headspritestat[ffiC.MAXSTATUS] if (i < 0) then return end ffiC.g_noResetVars = 1 return A_ResetVars(i) end -- Lunatic's "insertsprite" is a wrapper around the game "A_InsertSprite", not -- the engine "insertsprite". -- -- Forms: -- 1. table-call: insertsprite{tilenum, pos, sectnum [, owner [, statnum]] [, key=val...]} -- valid keys are: owner, statnum, shade, xrepeat, yrepeat, xvel, zvel -- 2. position-call: insertsprite(tilenum, pos, sectnum [, owner [, statnum]]) function insertsprite(tab_or_tilenum, ...) local tilenum, pos, sectnum -- mandatory -- optional with defaults: local owner, statnum local shade, xrepeat, yrepeat, ang, xvel, zvel = 0, 48, 48, 0, 0, 0 if (type(tab_or_tilenum)=="table") then local tab = tab_or_tilenum tilenum, pos, sectnum = unpack(tab, 1, 3) owner = tab[4] or tab.owner or -1 statnum = tab[5] or tab.statnum or 0 shade = tab.shade or shade xrepeat = tab.xrepeat or xrepeat yrepeat = tab.yrepeat or yrepeat ang = tab.ang or ang xvel = tab.xvel or xvel zvel = tab.zvel or zvel else tilenum = tab_or_tilenum local args = {...} pos, sectnum = unpack(args, 1, 2) owner = args[3] or -1 statnum = args[4] or 0 end if (type(sectnum)~="number" or type(tilenum) ~= "number") then error("invalid insertsprite call: 'sectnum' and 'tilenum' must be numbers", 2) end check_tile_idx(tilenum) check_sector_idx(sectnum) check_isnumber(shade, xrepeat, yrepeat, ang, xvel, zvel, owner) if (statnum >= ffiC.MAXSTATUS+0ULL) then error("invalid 'statnum' argument to insertsprite: must be a status number (0 .. MAXSTATUS-1)", 2) end A_ResetVarsNextIns() return CF.A_InsertSprite(sectnum, pos.x, pos.y, pos.z, tilenum, shade, xrepeat, yrepeat, ang, xvel, zvel, owner, statnum) end -- INTERNAL USE ONLY. function _addtodelqueue(spritenum) check_sprite_idx(spritenum) CF.A_AddToDeleteQueue(spritenum) end -- This corresponds to the first (spawn from parent sprite) form of A_Spawn(). function spawn(parentspritenum, tilenum, addtodelqueue) check_sprite_idx(parentspritenum) check_tile_idx(tilenum) if (addtodelqueue and ffiC.g_spriteDeleteQueueSize == 0) then return -1 end A_ResetVarsNextIns() local i = CF.A_Spawn(parentspritenum, tilenum) if (addtodelqueue) then CF.A_AddToDeleteQueue(i) end return i end -- This is the second A_Spawn() form. INTERNAL USE ONLY. function _spawnexisting(spritenum) check_sprite_idx(spritenum) return CF.A_Spawn(-1, spritenum) end -- A_SpawnMultiple clone -- ow: parent sprite number function _spawnmany(ow, tilenum, n) local spr = sprite[ow] for i=n,1, -1 do local j = insertsprite{ tilenum, spr^(ffiC.krand()%(47*256)), spr.sectnum, ow, 5, shade=-32, xrepeat=8, yrepeat=8, ang=krandand(2047) } _spawnexisting(j) sprite[j].cstat = krandand(8+4) end end local int16_st = ffi.typeof "struct { int16_t s; }" function _shoot(i, tilenum, zvel) check_sprite_idx(i) check_sector_idx(ffiC.sprite[i].sectnum) -- accessed in A_ShootWithZvel check_tile_idx(tilenum) zvel = zvel and int16_st(zvel).s or 0x80000000 -- SHOOT_HARDCODED_ZVEL return CF.A_ShootWithZvel(i, tilenum, zvel) end local BADGUY_MASK = bit.bor(con_lang.SFLAG.SFLAG_HARDCODED_BADGUY, con_lang.SFLAG.SFLAG_BADGUY) function isenemytile(tilenum) return (bit.band(ffiC.g_tile[tilenum].flags, BADGUY_MASK)~=0) end -- The 'rotatesprite' wrapper used by the CON commands. function _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation, alpha, cx1, cy1, cx2, cy2) check_tile_idx(tilenum) orientation = bit.band(orientation, 4095) -- ROTATESPRITE_MAX-1 if (bit.band(orientation, 2048) == 0) then -- ROTATESPRITE_FULL16 x = 65536*x y = 65536*y end -- XXX: This is the same as the check in gameexec.c, but ideally we'd want -- rotatesprite to accept all coordinates and simply draw nothing if the -- tile's bounding rectange is beyond the screen. -- XXX: Currently, classic rotatesprite() is not correct with some large -- zoom values. if (not (x >= -320*65536 and x < 640*65536) or not (y >= -200*65536 and y < 400*65536)) then error(format("invalid coordinates (%.03f, %.03f)", x, y), 2) end ffiC.rotatesprite_(x, y, zoom, ang, tilenum, shade, pal, bit.bor(2,orientation), alpha, cx1, cy1, cx2, cy2) end -- The external legacy tile drawing function for Lunatic. function rotatesprite(x, y, zoom, ang, tilenum, shade, pal, orientation, alpha, cx1, cy1, cx2, cy2) -- Disallow <<16 coordinates from Lunatic. They only unnecessarily increase -- complexity; you already have more precision in the FP number fraction. if (bit.band(orientation, 2048) ~= 0) then error('left-shift-by-16 coordinates forbidden', 2) end return _rotspr(x, y, zoom, ang, tilenum, shade, pal, orientation, alpha, cx1, cy1, cx2, cy2) end function _myos(x, y, zoom, tilenum, shade, orientation, pal) if (pal==nil) then local sect = player[ffiC.screenpeek].cursectnum pal = (sect>=0) and sector[sect].floorpal or 0 end ffiC.G_DrawTileGeneric(x, y, zoom, tilenum, shade, orientation, pal) end function _inittimer(ticspersec) if (not (ticspersec >= 1)) then error("ticspersec must be >= 1", 2) end ffiC.G_InitTimer(ticspersec) end function _gettimedate() local v = ffi.new("int32_t [8]") ffiC.G_GetTimeDate(v) return v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7] end local rshift = bit.rshift function rnd(x) return (rshift(ffiC.krand(), 8) >= (255-x)) end -- Legacy operators function _rand(x) return rshift(ffiC.krand()*(x+1), 16) end function _displayrand(x) return rshift(math.random(0, 32767)*(x+1), 15) end function _div(a,b) if (b==0) then error("divide by zero", 2) end -- NOTE: don't confuse with math.modf! return (a - math.fmod(a,b))/b end function _mod(a,b) if (b==0) then error("mod by zero", 2) end return (math.fmod(a,b)) end -- Sect_ToggleInterpolation() clone function _togglesectinterp(sectnum, doset) for w in wallsofsect(sectnum) do ffiC.G_ToggleWallInterpolation(w, doset) local nw = wall[w].nextwall if (nw >= 0) then ffiC.G_ToggleWallInterpolation(nw, doset) ffiC.G_ToggleWallInterpolation(wall[nw].point2, doset) end end end -- Support for translated CON code: get cached sprite, actor and player structs -- (-fcache-sap option). function _getsap(aci, pli) return (aci>=0) and sprite[aci], (aci>=0) and actor[aci], (pli>=0) and player[pli] end --- player/actor/sprite searching functions --- local xmath = require("xmath") local abs = math.abs local dist, ldist = xmath.dist, xmath.ldist local function A_FP_ManhattanDist(ps, spr) local distvec = ps.pos - spr^(28*256) return distvec:touniform():mhlen() end -- Returns: player index, distance -- TODO_MP 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 (xmath.ldist(s1, s2) < d) end, d3 = function(s1, s2, d) return (xmath.dist(s1, s2) < d) end, z = function(s1, s2, d, zd) return (xmath.ldist(s1, s2) < d and abs(s1.z-s2.z) < zd) end, } function _findnear(spritenum, allspritesp, distkind, picnum, maxdist, maxzdist) local statnums = FN_STATNUMS[allspritesp] local distfunc = FN_DISTFUNC[distkind] local spr = sprite[spritenum] for _,st in ipairs(statnums) do for i in spritesofstat(st) do if (i ~= spritenum and sprite[i].picnum==picnum) then if (distfunc(spr, sprite[i], maxdist, maxzdist)) then return i end end end end return -1 end ---=== Weapon stuff ===--- --- Helper functions (might be exported later) --- local function have_ammo_at_max(ps, weap) return (ps.ammo_amount[weap] >= ps.max_ammo_amount[weap]) end local function P_AddAmmo(ps, weap, amount) if (not have_ammo_at_max(ps, weap)) then local curamount = ps.ammo_amount[weap] local maxamount = ps.max_ammo_amount[weap] -- NOTE: no clamping towards the bottom ps.ammo_amount[weap] = math.min(curamount+amount, maxamount) end end local function P_AddWeaponAmmoCommon(ps, weap, amount) P_AddAmmo(ps, weap, amount) if (ps.curr_weapon==ffiC.KNEE_WEAPON and ps:have_weapon(weap)) then CF.P_AddWeaponMaybeSwitchI(ps.weapon._p, weap); end end --- Functions that must be exported because they are used by LunaCON generated code, --- but which are off limits to users. (That is, we need to think about how to --- expose the functionality in a better fashion than merely giving access to --- the C functions.) -- quotes local REALMAXQUOTES = con_lang.REALMAXQUOTES local MAXQUOTELEN = con_lang.MAXQUOTELEN -- CON redefinequote command function _definequote(qnum, quotestr) -- NOTE: this is more permissive than C-CON: we allow to redefine quotes -- that were not previously defined. bcheck.quote_idx(qnum, true) assert(type(quotestr)=="string") ffiC.C_DefineQuote(qnum, quotestr) return (#quotestr >= MAXQUOTELEN) end function _quote(pli, qnum) bcheck.quote_idx(qnum) check_player_idx(pli) ffiC.P_DoQuote(qnum+REALMAXQUOTES, ffiC.g_player[pli].ps) end function _echo(qnum) local cstr = bcheck.quote_idx(qnum) ffiC.OSD_Printf("%s\n", cstr) end function _userquote(qnum) local cstr = bcheck.quote_idx(qnum) -- NOTE: G_AddUserQuote strcpy's the string ffiC.G_AddUserQuote(cstr) end local function strlen(cstr) for i=0,math.huge do if (cstr[i]==0) then return i end end assert(false) end -- NOTE: dst==src is OK (effectively a no-op) local function strcpy(dst, src) local i=-1 repeat i = i+1 dst[i] = src[i] until (src[i]==0) end function _qstrlen(qnum) return strlen(bcheck.quote_idx(qnum)) end function _qstrcpy(qdst, qsrc) local cstr_dst = bcheck.quote_idx(qdst) local cstr_src = bcheck.quote_idx(qsrc) strcpy(cstr_dst, cstr_src) end -- NOTE: qdst==qsrc is OK (duplicates the quote) function _qstrcat(qdst, qsrc) local cstr_dst = bcheck.quote_idx(qdst) local cstr_src = bcheck.quote_idx(qsrc) if (cstr_src[0]==0) then return end if (cstr_dst[0]==0) then return strcpy(cstr_dst, cstr_src) end -- From here on: destination and source quote (potentially aliased) are -- nonempty. local slen_dst = strlen(cstr_dst) assert(slen_dst <= MAXQUOTELEN-1) if (slen_dst == MAXQUOTELEN-1) then return end local i = slen_dst local j = 0 repeat -- NOTE: don't copy the first char yet, so that the qdst==qsrc case -- works correctly. i = i+1 j = j+1 cstr_dst[i] = cstr_src[j] until (i >= MAXQUOTELEN-1 or cstr_src[j]==0) -- Now copy the first char! cstr_dst[slen_dst] = cstr_src[0] cstr_dst[i] = 0 end local buf = ffi.new("char [?]", MAXQUOTELEN) function _qsprintf(qdst, qsrc, ...) -- NOTE: more permissive than C-CON, see _definequote if (bcheck.quote_idx(qdst, true) == nil) then ffiC.C_DefineQuote(qdst, "") -- allocate quote end local dst = bcheck.quote_idx(qdst) local src = bcheck.quote_idx(qsrc) local vals = {...} local i, j, vi = 0, 0, 1 while (true) do local ch = src[j] local didfmt = false if (ch==0) then break end if (ch==byte'%') then local nch = src[j+1] if (nch==byte'd' or (nch==byte'l' and src[j+2]==byte'd')) then -- number didfmt = true if (vi > #vals) then break end local numstr = tostring(vals[vi]) assert(type(numstr)=="string") vi = vi+1 local ncopied = math.min(#numstr, MAXQUOTELEN-1-i) ffi.copy(buf+i, numstr, ncopied) i = i+ncopied j = j+1+(nch==byte'd' and 1 or 2) elseif (nch==byte's') then -- string didfmt = true if (vi > #vals) then break end local k = -1 local tmpsrc = bcheck.quote_idx(vals[vi]) vi = vi+1 i = i-1 repeat i = i+1 k = k+1 buf[i] = tmpsrc[k] until (i >= MAXQUOTELEN-1 or tmpsrc[k]==0) j = j+2 end end if (not didfmt) then buf[i] = src[j] i = i+1 j = j+1 end if (i >= MAXQUOTELEN-1) then break end end buf[i] = 0 strcpy(dst, buf) end function _getkeyname(qdst, gfuncnum, which) local cstr_dst = bcheck.quote_idx(qdst) if (gfuncnum >= ffiC.NUMGAMEFUNCTIONS+0ULL) then error("invalid game function number "..gfuncnum, 2) end if (which >= 3+0ULL) then error("third argument to getkeyname must be 0, 1 or 2", 2) end local cstr_src for i = (which==2 and 0 or which), (which==2 and 1 or which) do local scancode = ffiC.ud.config.KeyboardKeys[gfuncnum][i] cstr_src = ffiC.KB_ScanCodeToString(scancode) if (cstr_src[0] ~= 0) then break end end if (cstr_src[0] ~= 0) then -- All key names are short, no problem strcpy'ing them strcpy(cstr_dst, cstr_src) end end local EDUKE32_VERSION_STR = "EDuke32 2.0.0devel "..ffi.string(ffiC.s_buildRev) local function quote_strcpy(dst, src) local i=-1 repeat i = i+1 dst[i] = src[i] until (src[i]==0 or i==MAXQUOTELEN-1) dst[i] = 0 end function _qgetsysstr(qdst, what, pli) local dst = bcheck.quote_idx(qdst) local idx = ffiC.ud.volume_number*con_lang.MAXLEVELS + ffiC.ud.level_number local MAXIDX = ffi.sizeof(ffiC.MapInfo) / ffi.sizeof(ffiC.MapInfo[0]) if (what == ffiC.STR_MAPNAME) then assert(not (idx >= MAXIDX+0ULL)) local src = ffiC.MapInfo[idx].name assert(src ~= nil) quote_strcpy(dst, src) elseif (what == ffiC.STR_MAPFILENAME) then assert(not (idx >= MAXIDX+0ULL)) local src = ffiC.MapInfo[idx].filename assert(src ~= nil) quote_strcpy(dst, src) elseif (what == ffiC.STR_PLAYERNAME) then ffi.copy(dst, ffiC.g_player[pli].user_name, ffi.sizeof(ffiC.g_player[0].user_name)) elseif (what == ffiC.STR_VERSION) then ffi.copy(dst, EDUKE32_VERSION_STR) elseif (what == ffiC.STR_GAMETYPE) then ffi.copy(dst, "multiplayer not yet implemented") -- TODO_MP elseif (what == ffiC.STR_VOLUMENAME) then local vol = ffiC.ud.volume_number assert(vol+0ULL < con_lang.MAXVOLUMES) 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,dist) local func = swtab[testval] or swtab.default if (func) then func(aci, pli, dist) end end -- text rendering function _minitext(x, y, qnum, shade, pal) local cstr = bcheck.quote_idx(qnum) ffiC.minitext_(x, y, cstr, shade, pal, 2+8+16) end function _digitalnumber(tilenum, x, y, num, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) if (tilenum >= ffiC.MAXTILES-9+0ULL) then error("invalid base tile number "..tilenum, 2) end ffiC.G_DrawTXDigiNumZ(tilenum, x, y, num, shade, pal, orientation, cx1, cy1, cx2, cy2, zoom) end local function text_check_common(tilenum, orientation) if (tilenum >= ffiC.MAXTILES-255+0ULL) then error("invalid base tile number "..tilenum, 3) end return bit.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, 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 ffiC.G_ShowView(x, y, z, a, horiz, sect, x1, y1, x2, y2, unbiasedp); end ---=== DEFINED LABELS ===--- -- Will contain [