-- LunaCON CON to Lunatic translator -- requires LPeg, http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html local require = require local lpeg = require("lpeg") local bit local math = require("math") local string = require("string") local table = require("table") local arg = arg local assert = assert local error = error local ipairs = ipairs local loadstring = loadstring local pairs = pairs local pcall = pcall local print = print local setmetatable = setmetatable local tonumber = tonumber local tostring = tostring local type = type local unpack = unpack -- non-nil if running from EDuke32 -- (read_into_string~=nil iff string.dump==nil) local read_into_string = read_into_string local ffi, ffiC if (string.dump) then -- running stand-alone local ljp = pcall(function() require("ffi") end) -- "lbit" is the same module as LuaJIT's "bit" (LuaBitOp: -- http://bitop.luajit.org/), but under a different name for (IMO) less -- confusion. Useful for running with Rio Lua for cross-checking. bit = ljp and require("bit") or require("lbit") require("strict") else bit = require("bit") ffi = require("ffi") ffiC = ffi.C end module("lunacon") -- I think that the "too many pending calls/choices" is unavoidable in general. -- This limit is of course still arbitrary, but writing long if/else cascades -- in CON isn't pretty either (though sometimes necessary because nested switches -- don't work?) -- See also: http://lua-users.org/lists/lua-l/2010-03/msg00086.html lpeg.setmaxstack(1024); local Pat, Set, Range, Var = lpeg.P, lpeg.S, lpeg.R, lpeg.V local POS, Cc, Ctab = lpeg.Cp, lpeg.Cc, lpeg.Ct -- CON language definitions (among other things, all keywords pattern). local conl = require("con_lang") local function match_until(matchsp, untilsp) -- (!untilsp matchsp)* in PEG -- sp: string or pattern return (matchsp - Pat(untilsp))^0 end local format = string.format --[[ format = function(fmt, ...) local ok, res = pcall(string.format, fmt, ...) if (not ok) then error(string.format("FAILED format(%q, ...) | message: %s", fmt, res)) end return res end --]] local function printf(fmt, ...) print(format(fmt, ...)) end --- some constants local C = { -- These two are not used except for predefined labels. -- NOTE: in-game, MAXSPRITES may be 4096 for a V7 build! MAXSTATUS = ffiC and ffiC.MAXSTATUS or 1024, MAXSPRITES = ffiC and ffiC.MAXSPRITES or 16384, MAXTILES = ffiC and ffiC.MAXTILES or 30720, MAX_WEAPONS = ffiC and ffiC.MAX_WEAPONS or 12, } ---=== semantic action functions ===--- local inf = 1/0 local NaN = 0/0 -- Last keyword position, for error diagnosis. local g_lastkwpos = nil local g_lastkw = nil local g_badids = {} -- maps bad id strings to 'true' local g_recurslevel = -1 -- 0: base CON file, >0 included local g_filename = "???" local g_directory = "" -- with trailing slash if not empty local g_maxerrors = 20 local g_numerrors = 0 -- Default directory to search for GAME.CON etc. -- Stand-alone LunaCON only. local g_defaultDir = nil -- Warning options. Key names are the same as cmdline options, e.g. -- -Wno-bad-identifier for disabling the "bad identifier" warning. local g_warn = { ["not-redefined"]=true, ["bad-identifier"]=false, ["number-conversion"]=true, ["system-gamevar"]=true, ["error-bad-getactorvar"]=false, ["chained-loadactor"]=true, } -- Code generation and output options. local g_cgopt = { ["no"]=false, ["debug-lineinfo"]=false, ["gendir"]=nil, ["cache-sap"]=false, ["error-nostate"]=true, ["playervar"]=true, ["trapv"]=false, ["wrapv"]=false, } local function csapp() return g_cgopt["cache-sap"] end local function handle_cmdline_arg(str) if (str:sub(1,1)=="-") then if (#str == 1) then printf("Warning: input from stdin not supported") else local ok = false local kind = str:sub(2,2) -- -W(no-)*: warnings if (kind=="W" and #str >= 3) then local val = true local warnstr = str:sub(3) if (warnstr == "all") then -- Enable all warnings. for wopt in pairs(g_warn) do g_warn[wopt] = true end ok = true else -- Enable or disable a particular warning. if (warnstr:sub(1,3)=="no-") then val = false warnstr = warnstr:sub(4) end if (type(g_warn[warnstr])=="boolean") then g_warn[warnstr] = val ok = true end end -- -fno* special handling elseif (str:sub(2)=="fno") then -- Disable printing code entirely. g_cgopt["no"] = true ok = true elseif (str:sub(2)=="fno=onlycheck") then -- Disable printing code, only do syntax check of gen'd code. g_cgopt["no"] = "onlycheck" ok = true -- -fgendir=: specify directory for generated code elseif (str:sub(2,9)=="fgendir=" and #str >= 10) then g_cgopt["gendir"] = str:sub(10) ok = true -- -f(no-)*: code generation options elseif (kind=="f" and #str >= 3) then local val = true local cgstr = str:sub(3) if (cgstr:sub(1,3)=="no-") then val = false cgstr = cgstr:sub(4) end if (type(g_cgopt[cgstr])=="boolean") then g_cgopt[cgstr] = val ok = true end -- -I: default search directory (only ONCE, not search path) elseif (kind=="I" and #str >= 3) then g_defaultDir = str:sub(3) ok = true end if (not ffi and not ok) then printf("Warning: Unrecognized option %s", str) end end return true end end -- Handle command line arguments. Has to happen before pattern construction, -- because some of them depend on codegen options (specifically, -ftrapv, -- -fwrapv). if (string.dump) then -- running stand-alone local i = 1 while (arg[i]) do if (handle_cmdline_arg(arg[i])) then table.remove(arg, i) -- remove processed cmdline arg else i = i+1 end end else -- running from EDuke32 local i=0 while (ffiC.g_argv[i] ~= nil) do handle_cmdline_arg(ffi.string(ffiC.g_argv[i])) i = i+1 end end -- Stack with *true* on top if the innermost block is a "whilevar*n". local g_isWhile = {} -- Sequence number of 'while' statements, used to implement CON "break" inside -- whilevar*n, which really behaves like what sane languages call "continue"... local g_whilenum = 0 ---=== Code generation ===--- local GVFLAG = { PERPLAYER=1, PERACTOR=2, PERX_MASK=3, SYSTEM = 0x00000800, READONLY = 0x00001000, NODEFAULT = 0x00000400, -- don't reset on actor spawn NORESET = 0x00020000, -- don't reset when restoring map state CON_PERPLAYER = 0x40000000, -- LunaCON internal } -- NOTE: This differs from enum GamevarFlags_t's GAMEVAR_USER_MASK GVFLAG.USER_MASK = GVFLAG.PERX_MASK + GVFLAG.NODEFAULT + GVFLAG.NORESET -- CON --> mangled Lua function name, also existence check: local g_funcname = {} -- while parsing a block, it is a table of "gencode" tables: local g_switchCode = nil -- Global number of switch statements: local g_switchCount = 0 -- Number of session gamevars: local g_numSessionVars = 0 -- [identifier] = { name=, flags= } local g_gamevar = {} -- [identifier] = { name=, size= } local g_gamearray = {} -- * nil if dynamic tile remapping disabled -- * {} if enabled but no remappings made -- * else, a nonempty table { [name]= } local g_dyntilei = nil -- Analogously for sounds. local g_dynsoundi = nil local g_have_file = {} -- [filename]=true local g_curcode = nil -- a table of string pieces or other "gencode" tables -- will be a table, see reset.codegen() local g_code = nil local function ACS(s) return (csapp() and "_a" or "actor[_aci]")..s end local function SPS(s) return (csapp() and "_spr" or "sprite[_aci]")..s end local function PLS(s) return (csapp() and "_ps" or "player[_pli]")..s end local function PLSX(s) return "player[_pli]"..s end local function getlinecol(pos) end -- fwd-decl local function new_initial_codetab() -- NOTE: Keep this one line per line to not confuse the Lua->CON line -- mapping system. return { -- Requires. "local require=require", "local _con, _bit, _math = require'con', require'bit', require'math'", "local _xmath = require'xmath'", -- Cache globals into locals. "local sector, sprite, wall, spriteext, _atsprite = sector, sprite, wall, spriteext, _atsprite", "local actor, player, projectile, g_tile = actor, player, projectile, g_tile", "local gameactor, gameevent, _gv = gameactor, gameevent, gv", "local updatesector, updatesectorz, cansee = updatesector, updatesectorz, cansee", "local print, printf = print, printf", -- Cache a couple of often-used functions. "local _div, _mod, _mulTR, _mulWR = _con._div, _con._mod, _con._mulTR, _con._mulWR", "local _band, _bor, _bxor = _bit.band, _bit.bor, _bit.bxor", "local _lsh, _rsh, _arsh = _bit.lshift, _bit.rshift, _bit.arshift", "local _setsprite,_ssp = _con._setsprite,_con._ssp", -- * CON "states" (subroutines) and -- * Switch function table, indexed by global switch sequence number: "local _F,_SW = {},{}", -- CON gamevars and gamearrays (see mangle_name()), set up for -- restoration from savegames. "module(...)", "_V,_A={},{}", "-- NOTE to the reader: This require's result is Lunatic-private API! DO NOT USE!", "local _dummy,_S=require'end_gamevars'", -- XXX: Currently commented out because of gamevar restoration from loadmapstate. -- "local _V,_A=_V,_A", "local _C,_M,_I={},{},{}", -- actions, moves, ais -- Static ivec3s so that no allocations need to be made. "local _IVEC = { _xmath.ivec3(), _xmath.ivec3() }", "local function _IV(num, x, y, z)", " local v=_IVEC[num]; v.x=x; v.y=y; v.z=z; return v;", "end", } end -- CON global system gamevar local function CSV(var) return "_gv._csv"..var end -- Creates the table of predefined game variables. -- KEEPINSYNC gamevars.c: Gv_AddSystemVars() local function new_initial_gvartab() local wmembers = conl.wdata_members local function GamevarCreationFunc(addflags) return function(varname) return { name=varname, flags=GVFLAG.SYSTEM+addflags } end end local RW = GamevarCreationFunc(0) local RO = GamevarCreationFunc(GVFLAG.READONLY) local PRW = GamevarCreationFunc(GVFLAG.PERPLAYER) local PRO = GamevarCreationFunc(GVFLAG.READONLY+GVFLAG.PERPLAYER) local gamevar = { -- NOTE: THISACTOR can mean different things in some contexts. THISACTOR = RO "_aci", RETURN = RW "_gv.RETURN", HITAG = RW(CSV".HITAG"), LOTAG = RW(CSV".LOTAG"), TEXTURE = RW(CSV".TEXTURE"), -- This will warn when defining from CON, but it's the most -- straightforward implementation. LOGO_FLAGS = RW "_gv.g_logoFlags", xdim = RO "_gv.xdim", ydim = RO "_gv.ydim", windowx1 = RO "_gv.windowx1", windowy1 = RO "_gv.windowy1", windowx2 = RO "_gv.windowx2", windowy2 = RO "_gv.windowy2", yxaspect = RO "_gv._get_yxaspect()", viewingrange = RO "_gv._get_viewingrange()", -- TODO: gravitationalconstant, gametype_flags numsectors = RO "_gv.numsectors", NUMSECTORS = RO "_gv.numsectors", NUMWALLS = RO "_gv.numwalls", -- TODO: Numsprites randomseed = RW "_gv.randomseed", totalclock = RO "_gv.totalclock", framerate = RO "_gv._currentFramerate()", current_menu = RO "_gv._currentMenu()", rendmode = RO "_gv.rendmode", screenpeek = RO "_gv.screenpeek", camerax = RW "_gv.cam.pos.x", cameray = RW "_gv.cam.pos.y", cameraz = RW "_gv.cam.pos.z", cameraang = RW "_gv.cam.ang", camerahoriz = RW "_gv.cam.horiz", camerasect = RW "_gv.cam.sect", cameradist = RW "_gv.cam.dist", cameraclock = RW "_gv.cam.clock", -- HUD weapon gamevars currentweapon = RW "_gv.hudweap.cur", weaponcount = RW "_gv.hudweap.count", weapon_xoffset = RW "_gv.hudweap.gunposx", looking_angSR1 = RW "_gv.hudweap.lookhalfang", gun_pos = RW "_gv.hudweap.gunposy", looking_arc = RW "_gv.hudweap.lookhoriz", gs = RW "_gv.hudweap.shade", -- Some per-player gamevars ZRANGE = PRW(PLSX".zrange"), ANGRANGE = PRW(PLSX".angrange"), AUTOAIMANGLE = PRW(PLSX".autoaimang"), PIPEBOMB_CONTROL = PRW(PLSX".pipebombControl"), GRENADE_LIFETIME = PRW(PLSX".pipebombLifetime"), GRENADE_LIFETIME_VAR = PRW(PLSX".pipebombLifetimeVar"), TRIPBOMB_CONTROL = PRW(PLSX".tripbombControl"), STICKYBOMB_LIFETIME = PRW(PLSX".tripbombLifetime"), STICKYBOMB_LIFETIME_VAR = PRW(PLSX".tripbombLifetimeVar"), -- Some *writable* system gamevars relating to multiplayer. -- TODO_MP. RESPAWN_MONSTERS = RO "0", RESPAWN_ITEMS = RO "0", RESPAWN_INVENTORY = RO "0", MONSTERS_OFF = RO "0", MARKER = RO "0", -- These are not 100% authentic (they're only updated in certain -- circumstances, see player.c: P_SetWeaponGamevars()). But IMO it's -- more useful like this. WEAPON = PRO(PLSX".curr_weapon"), WORKSLIKE = PRO(format(PLSX".weapon[%s].workslike", PLSX".curr_weapon")), VOLUME = RO "_gv._ud.volume_number", LEVEL = RO "_gv._ud.level_number", } -- Reserved bits gamevar.LOGO_FLAGS.rbits = bit.bnot(65535) for w=0,C.MAX_WEAPONS-1 do for i=1,#wmembers do local member = wmembers[i]:gsub(".*_t ","") -- strip e.g. "const int32_t " :gsub("^_","") -- strip potentially leading underscore local name = format("WEAPON%d_%s", w, member:upper()) gamevar[name] = PRW(format(PLSX".weapon[%d].%s", w, member)) if (member=="flags") then gamevar[name].rbits = bit.bnot(0x1ffff) end end end return gamevar end local reset = {} function reset.codegen() g_funcname = {} g_switchCode = nil g_switchCount = 0 g_numSessionVars = 0 g_gamevar = new_initial_gvartab() g_gamearray = { -- SYSTEM_GAMEARRAY tilesizx = { name="g_tile.sizx", size=C.MAXTILES, sysp=true }, tilesizy = { name="g_tile.sizy", size=C.MAXTILES, sysp=true }, } g_dyntilei = nil g_dynsoundi = nil g_have_file = {} g_curcode = new_initial_codetab() -- [{actor, event, actor}num]=gencode_table g_code = { actor={}, event={}, loadactor={} } g_recurslevel = -1 g_numerrors = 0 end -- Is SYSTEM_GAMEARRAY? local function issysgar(str) return str:match("^g_tile.siz[xy]") end local function addcode(x) assert(type(x)=="string" or type(x)=="table") g_curcode[#g_curcode+1] = x end local function addcodef(fmt, ...) addcode(format(fmt, ...)) end local function paddcodef(pos, fmt, ...) addcodef(fmt.."--"..getlinecol(pos), ...) end local function add_code_and_end(codetab, endstr) assert(type(codetab)=="table") addcode(codetab) addcode(endstr) end local function get_cache_sap_code() return csapp() and "local _spr,_a,_ps=_con._getsap(_aci,_pli)" or "" end -- fwd-decls local warnprintf, errprintf, pwarnprintf, perrprintf, contprintf local on = {} -- Map from CON actor usertype to SFLAGs. local MAP_ACTOR_FLAGS = { [0] = 0, [1] = conl.SFLAG.SFLAG_BADGUY, [2] = conl.SFLAG.SFLAG_BADGUY + conl.SFLAG.SFLAG_BADGUYSTAYPUT, [3] = conl.SFLAG.SFLAG_BADGUY + conl.SFLAG.SFLAG_BADGUYSTAYPUT, } for i=4,7 do MAP_ACTOR_FLAGS[i] = MAP_ACTOR_FLAGS[i-4] + conl.SFLAG.SFLAG_ROTFIXED end function on.actor_end(pos, usertype, tsamm, codetab) local tilenum = tsamm[1] local flags = 0 if (usertype ~= nil) then -- useractor if (not (bit.band(usertype, bit.bnot(7)) == 0)) then perrprintf(pos, "invalid usertype: must be bitwise OR of 1, 2 and/or 4") else flags = MAP_ACTOR_FLAGS[usertype] end end -- 0x10000000: actor.FLAGS.replace_soft flags = bit.bor(flags, 0x10000000) local str = flags.."," for i=2,math.min(#tsamm,4) do str = str .. tostring(tsamm[i]).."," end if (#tsamm >= 5) then local movflags = bit.bor(unpack(tsamm, 5)) str = str .. movflags.."," end paddcodef(pos, "gameactor{%d,%sfunction(_aci,_pli,_dist)", tilenum, str) addcode(get_cache_sap_code()) add_code_and_end(codetab, "end}") if (g_code.actor[tilenum] ~= nil) then pwarnprintf(pos, "redefined actor %d", tilenum) end g_code.actor[tilenum] = codetab end -- NOTE: in C-CON, the slash and backslash can also be part of an identifier, -- but this is likely to support file names in other places. local BAD_ID_CHARS0 = "_*?" -- allowed 1st identifier chars local BAD_ID_CHARS1 = "_*-+?" -- allowed following identifier chars local function truetab(tab) local ttab = {} for i=1,#tab do ttab[tab[i]] = true end return ttab end -- Lua 5.2 keywords. Not 5.1 because we use "goto" for codegen. local LUA_KEYW = truetab { "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while" } -- Return the Lua code by which the CON object is referenced in the -- translated code. local function mangle_name(name, prefix) if (name:match("^[A-Za-z_][A-Za-z_0-9]*$") and not LUA_KEYW[name]) then return format("_%s.%s", prefix, name) else return format("_%s[%q]", prefix, name) end end function on.state_begin_Cmt(_subj, _pos, statename) -- We must register the state name early (Cmt) because otherwise, it won't -- be found in a recursive state. XXX: The real issue seems to be the use -- of "Cmt"s in other places, which messes up the sequence of running the -- semantic actions. local ourname = mangle_name(statename, "F") g_funcname[statename] = ourname return true, ourname end function on.state_end(pos, funcname, codetab) paddcodef(pos, "%s=function(_aci,_pli,_dist)", funcname) addcode(get_cache_sap_code()) add_code_and_end(codetab, "end") end function on.event_end(pos, eventidx, codetab) assert(type(codetab)=="table") -- 0x20000000: actor.FLAGS.chain_beg paddcodef(pos, "gameevent{%d,0x20000000,function (_aci,_pli,_dist)", eventidx) addcode(get_cache_sap_code()) addcode(codetab) addcode("end}") g_code.event[eventidx] = codetab end function on.eventloadactor_end(pos, tilenum, codetab) -- Translate eventloadactor into a chained EVENT_LOADACTOR block paddcodef(pos, "gameevent{'LOADACTOR', function (_aci,_pli,_dist)") addcode(get_cache_sap_code()) addcodef("if (%s==%d) then", SPS".picnum", tilenum) addcode(codetab) addcode("end") addcode("end}") if (g_code.loadactor[tilenum] ~= nil and g_warn["chained-loadactor"]) then -- NOTE: C-CON redefines loadactor code if encountered multiple times. pwarnprintf(pos, "chained additional loadactor %d code", tilenum) end g_code.loadactor[tilenum] = codetab end ---------- local function linecolstr(pos) local line, col = getlinecol(pos) return format("%d:%d", line, col) end local function increment_numerrors() g_numerrors = g_numerrors+1 if (g_numerrors == g_maxerrors) then g_numerrors = inf printf("Too many errors (%d), aborting...", g_maxerrors) end end function perrprintf(pos, fmt, ...) printf("%s %s: error: "..fmt, g_filename, pos and linecolstr(pos) or "???", ...) increment_numerrors() end function errprintf(fmt, ...) perrprintf(g_lastkwpos, fmt, ...) end function pwarnprintf(pos, fmt, ...) printf("%s %s: warning: "..fmt, g_filename, pos and linecolstr(pos) or "???", ...) end function warnprintf(fmt, ...) pwarnprintf(g_lastkwpos, fmt, ...) end -- Print a continuation line to an error or warning. function contprintf(iserr, fmt, ...) printf("%s %s: %s "..fmt, g_filename, g_lastkwpos and linecolstr(g_lastkwpos) or "???", iserr and " " or " ", ...) end local function parse_number(pos, numstr) -- is a full number string, potentially prefixed with a minus sign. local num = tonumber((numstr:gsub("h$", ""))) -- local onum = num local hex = numstr:match("0[xX]([^h]*)h?") -- get hex digits, if any -- num==nil for Rio Lua, which doesn't handle large hex literals. if (num==nil or not (num >= -0x80000000 and num <= 0xffffffff)) then -- number is UINT32_MAX or NaN if (hex and #hex>8 and hex:sub(1,#hex-8):match("^[fF]$")) then -- Too many hex digits, but they're all Fs. pwarnprintf(pos, "number %s truncated to 32 bits", numstr) num = bit.tobit(num) else perrprintf(pos, "number %s out of the range of a 32-bit integer", numstr) -- Be careful not to write bound checks like -- "if (iHIGHBOUND) then error('...') end": num = NaN end elseif (num >= 0x80000000) then if (not hex and g_warn["number-conversion"]) then pwarnprintf(pos, "number %s converted to a negative one", numstr) end num = bit.tobit(num) end -- printf("numstr:%s, num=%d (0x%s) '%s', resnum=%d (0x%s)", -- numstr, onum, bit.tohex(onum), hex, num, bit.tohex(num)) return num end -- Bound checking functions that generate a compilation error on failure. local check = {} function check.tile_idx(tilenum) if (not (tilenum >= 0 and tilenum < C.MAXTILES)) then errprintf("invalid tile number %d", tilenum) return false end return true end function check.sound_idx(sidx) if (not (sidx >= 0 and sidx < conl.MAXSOUNDS)) then errprintf("invalid sound number %d", sidx) return false end return true end -- Mapping of various "define" types to the respective number of members and -- vice versa local LABEL = { MOVE=2, AI=3, ACTION=5, [2]="move", [3]="ai", [5]="action", NUMBER=1, [1]="number" } -- Function names in the 'con' module: local LABEL_FUNCNAME = { [2]="move", [3]="ai", [5]="action" } local LABEL_PREFIX = { [2]="M", [3]="I", [5]="C" } -- _C, _M, _I in the gen'd code local g_labeldef = {} -- Lua numbers for numbers, strings for composites local g_labeltype = {} local g_labelspecial = {} -- [