-- MAPTEXT -- Lunatic: routines for reading and writing map-text. local ffi = require("ffi") local ffiC = ffi.C local pairs = pairs local pcall = pcall local print = print local setfenv = setfenv local tonumber = tonumber local type = type local readintostr = assert(string.readintostr) local io = require("io") local string = require("string") ffi.cdef[[ int32_t (*saveboard_maptext)(const char *filename, const vec3_t *dapos, int16_t daang, int16_t dacursectnum); int32_t (*loadboard_maptext)(int32_t fil, vec3_t *dapos, int16_t *daang, int16_t *dacursectnum); ]] --== COMMON ==-- local sector_members = { -- Mandatory positional members first, [pos]=. "ceilingz", "floorz", "ceilingpicnum", "floorpicnum", "ceilingshade", "floorshade"; -- If other positional members are to be added, they must be optional -- for backwards compatibility. -- Optional key/value members next. B = { "ceilingbunch", -1 }, b = { "floorbunch", -1 }, -- default: -1 F = "ceilingstat", f = "floorstat", -- default: 0 H = "ceilingheinum", h = "floorheinum", P = "ceilingpal", p = "floorpal", X = "ceilingxpanning", x = "floorxpanning", Y = "ceilingypanning", y = "floorypanning", v = "visibility", _ = "filler", o = "lotag", i = "hitag", e = { "extra", -1 } } -- Defines the order in which the members are written out. A space denotes that -- a newline should appear in the output. KEEPINSYNC with sector_members. local sector_ord = { mand="12 34 56 ", opt="Bb Ff Hh Pp Xx Yy v _ oie" } -- KEEPINSYNC with sector_members. local sector_default = ffi.new("const sectortype", { ceilingbunch=-1, floorbunch=-1, extra=-1 }) local wall_members = { -- mandatory "point2", -- special: 0, 1 or 2 in map-text "x", "y", "nextwall", "picnum", "shade", "xrepeat", "yrepeat", "xpanning", "ypanning"; -- optional f = "cstat", m = "overpicnum", p = "pal", w = { "upwall", -1 }, W = { "dnwall", -1 }, o = "lotag", i = "hitag", e = { "extra", -1 } } local wall_ord = { mand="1 23 4 5 6 78 90 ", opt="f m p wW oie" } local wall_default = ffi.new("const walltype", { extra = -1, upwall=-1, dnwall=-1 }) local sprite_members = { -- mandatory "x", "y", "z", "ang", "sectnum", "picnum", "cstat", "shade", "xrepeat", "yrepeat", -- optional p = "pal", c = { "clipdist", 32 }, _ = "filler", x = "xoffset", y = "yoffset", s = "statnum", w = { "owner", -1 }, X = "xvel", Y = "yvel", Z = "zvel", o = "lotag", i = "hitag", e = { "extra", -1 } } local sprite_ord = { mand="123 4 5 6 7 8 90 ", opt="p c _ xy s w XYZ oie" } local sprite_default = ffi.new("const spritetype", { clipdist=32, owner=-1, extra=-1 }) --== SAVING ==-- local function write_struct(f, struct, members, ord) -- Write mandatory members first. local str = ord.mand:gsub(".", function(s) local num = (s=="0") and 10 or tonumber(s) return (s==" ") and "\n" or (struct[members[num]]..",") end) f:write("{"..str) local havesth = false -- Write optional members next. str = ord.opt:gsub(".", function(s) if (s==" ") then local ohavesth = havesth havesth = false return ohavesth and "\n" or "" end local memb = members[s] local mname = (type(memb)=="table") and memb[1] or memb local mdefault = (type(memb)=="table") and memb[2] or 0 local val = struct[mname] if (val~=mdefault) then havesth = true return s.."="..val.."," else return "" end end) local neednl = (#str>0 and str:sub(-1)~="\n") f:write(str..(neednl and "\n" or "").."},\n") end -- common local function check_bad_point2() local lastloopstart = 0 for i=0,ffiC.numwalls-1 do local p2 = ffiC.wall[i].point2 if (not (p2 == i+1 or (p2 ~= i and p2 == lastloopstart))) then -- If we hit this, the map is seriously corrupted! print(string.format("INTERNAL ERROR: wall[%d].point2=%d invalid", i, p2)) return true end if (p2 ~= i+1) then lastloopstart = i+1 end end end local function lastwallofsect(s) return ffiC.sector[s].wallptr + ffiC.sector[s].wallnum - 1 end -- In map-text, instead of saving wall[].point2, we store whether a particular -- wall is the last one in its loop instead: the on-disk wall[i].point2 is -- * 2 if wall i is last of its sector (no need to save sector's .wallnum), -- * 1 if wall i is last of its loop, -- * 0 otherwise. -- This function prepares saving to map-text by tweaking the wall[].point2 -- members in-place. local function save_tweak_point2() -- Check first. if (check_bad_point2()) then return true end -- Do it for real. local lastloopstart = 0 local cursect, curlastwall = 0, lastwallofsect(0) for i=0,ffiC.numwalls-1 do local wal = ffiC.wall[i] if (wal.point2 == i+1) then wal.point2 = 0 else -- Wall i is last point in loop. if (i==curlastwall) then -- ... and also last wall of sector. cursect = cursect+1 curlastwall = lastwallofsect(cursect) wal.point2 = 2 else wal.point2 = 1 end lastloopstart = i+1 end end end -- Common: restore tweaked point2 members to actual wall indices. -- If is true, also set sector's .wallptr and .wallnum members. local function restore_point2(alsosectorp) local lastloopstart = 0 local cursect, curfirstwall = 0, 0 for i=0,ffiC.numwalls-1 do local wal = ffiC.wall[i] local islast = (wal.point2~=0) if (not islast) then wal.point2 = i+1 else -- Wall i is last point in loop. if (alsosectorp and wal.point2 == 2) then -- ... and also last wall of sector. if (cursect==ffiC.MAXSECTORS) then return true -- Too many sectors. end ffiC.sector[cursect].wallptr = curfirstwall ffiC.sector[cursect].wallnum = i-curfirstwall+1 cursect = cursect+1 curfirstwall = i+1 end wal.point2 = lastloopstart lastloopstart = i+1 end end end local function saveboard_maptext(filename, pos, ang, cursectnum) assert(ffiC.numsectors > 0) -- First, temporarily tweak wall[].point2. if (save_tweak_point2()) then return -1 end -- We open in binary mode so that newlines get written out as one byte even -- on Windows. local f, msg = io.open(ffi.string(filename), "wb") if (f == nil) then print(string.format("Couldn't open \"%s\" for writing: %s\n", filename, msg)) restore_point2(false) return -1 end -- Write header. f:write(string.format("--EDuke32 map\n".. "return {\n".. "version=10,\n\n".. "pos={%d,%d,%d},\n".. "sectnum=%d,\n".. "ang=%d,\n\n", pos.x, pos.y, pos.z, cursectnum, ang)) -- Sectors. f:write("sector={\n") for i=0,ffiC.numsectors-1 do write_struct(f, ffiC.sector[i], sector_members, sector_ord) end f:write("},\n\n") -- Walls. f:write("wall={\n") for i=0,ffiC.numwalls-1 do write_struct(f, ffiC.wall[i], wall_members, wall_ord) end f:write("},\n\n") -- Sprites. f:write("sprite={\n") for i=0,ffiC.MAXSPRITES-1 do if (ffiC.sprite[i].statnum ~= ffiC.MAXSTATUS) then write_struct(f, ffiC.sprite[i], sprite_members, sprite_ord) end end f:write("},\n\n") f:write("}\n"); -- Done. f:close() restore_point2(false) return 0 end --== LOADING ==-- local function isnum(v) return (type(v)=="number") end local function istab(v) return (type(v)=="table") end -- Checks whether is a table all values of are of type . local function allxtab(tab, extype) if (not istab(tab)) then return false end for _,val in pairs(tab) do if (type(val) ~= extype) then return false end end return true end -- Is table of all numbers? local function allnumtab(tab) return allxtab(tab, "number") end -- Is table of all tables? local function alltabtab(tab) return allxtab(tab, "table") end -- Is table of tables of all numbers? Additionally, each must contain exactly -- as many mandatory positional entries as given by the table. local function tabofnumtabs(tab, members) for i=1,#tab do if (not allnumtab(tab[i])) then return false end local nummand = #members -- number of mandatory entries if (#tab[i] ~= nummand) then return false end end return true end -- Read data from Lua table into C struct , using the struct -- description . -- Returns true on error. local function read_struct(cs, stab, members, defaults) -- Clear struct to default values. ffi.copy(cs, defaults, ffi.sizeof(defaults)) -- Read mandatory positional members. for i=1,#members do cs[members[i]] = stab[i] end -- Read optional key/value members. for k,val in pairs(stab) do if (members[k]==nil) then -- No such member abbreviation for the given struct. return true end local memb = istab(members[k]) and members[k][1] or members[k] cs[memb] = val end end local RETERR = -4 local function loadboard_maptext(fil, posptr, angptr, cursectnumptr) -- Read the whole map-text as string. local str = readintostr(fil) if (str == nil) then return RETERR end -- Strip all one-line comments (currently, only the header). str = str:gsub("%-%-.-\n", "") --- Preliminary (pseudo-syntactical) validation --- -- Whitelist approach: map-text may only contain certain characters. This -- excludes various potentially 'bad' operations (such as calling a -- function) in one blow. Also, this assures (by exclusion) that the Lua -- code contains no long comments, strings, or function calls. if (not str:find("^[ A-Za-z_0-9{},%-\n=]+$")) then return RETERR-1 end -- The map-text code must return a single table. if (not str:find("^return %b{}\n$")) then return RETERR-2 end local func, errmsg = loadstring(str, "maptext") if (func == nil) then print("Error preloading map-text Lua code: "..errmsg) return RETERR-3 end -- Completely empty the function's environment as an additional safety -- measure, then run the chunk protected! (XXX: currently a bit pointless -- because of the asserts below.) local ok, map = pcall(setfenv(func, {})) if (not ok) then print("Error executing map-text Lua code: "..map) return RETERR-4 end assert(istab(map)) -- OK, now 'map' contains the map data. --- Structural validation --- -- Check types. if (not isnum(map.version) or not allnumtab(map.pos) or #map.pos~=3 or not isnum(map.sectnum) or not isnum(map.ang)) then return RETERR-5 end if (not (map.version <= 10)) then return RETERR-6 end local msector, mwall, msprite = map.sector, map.wall, map.sprite if (not alltabtab(msector) or not alltabtab(mwall) or not alltabtab(msprite)) then return RETERR-7 end if (not tabofnumtabs(msector, sector_members) or not tabofnumtabs(mwall, wall_members) or not tabofnumtabs(msprite, sprite_members)) then return RETERR-8 end local numsectors, numwalls, numsprites = #msector, #mwall, #msprite local sector, wall, sprite = ffiC.sector, ffiC.wall, ffiC.sprite if (numsectors == 0 or numsectors > ffiC.MAXSECTORS or numwalls > ffiC.MAXWALLS or numsprites > ffiC.MAXSPRITES) then return RETERR-9 end --- From here on, start filling out C structures. --- ffiC.numsectors = numsectors ffiC.numwalls = numwalls -- Header. posptr.x = map.pos[1] posptr.y = map.pos[2] posptr.z = map.pos[3] angptr[0] = map.ang cursectnumptr[0] = map.sectnum -- Sectors. for i=0,numsectors-1 do if (read_struct(sector[i], msector[i+1], sector_members, sector_default)) then return RETERR-10 end end -- Walls. for i=0,numwalls-1 do if (read_struct(wall[i], mwall[i+1], wall_members, wall_default)) then return RETERR-11 end end -- Sprites. for i=0,numsprites-1 do if (read_struct(sprite[i], msprite[i+1], sprite_members, sprite_default)) then return RETERR-12 end end -- XXX: need to consistency-check much more here! Basically, all of -- astub.c's CheckMapCorruption() for corruption level >=4? -- See NOTNICE below. --- Tweakery: mostly setting dependent members. --- -- sector[]: .wallptr calculated from .wallnum. local numw = 0 for i=0,numsectors-1 do assert(numw >= 0 and numw < numwalls) -- NOTNICE, cheap check instead of real one. sector[i].wallptr = numw numw = numw + sector[i].wallnum end -- .point2 in {0, 1} --> wall index, sector[].wallptr/.wallnum if (restore_point2(true)) then return RETERR-13 end -- Check .point2 at least. if (check_bad_point2()) then return RETERR-14 end -- wall[]: .nextsector calculated by using engine's sectorofwall_noquick() for i=0,numwalls-1 do local nw = wall[i].nextwall if (nw >= 0) then assert(nw >= 0 and nw < numwalls) -- NOTNICE wall[i].nextsector = ffiC.sectorofwall_noquick(nw) else wall[i].nextsector = -1 end end -- All OK, return the number of sprites for further engine loading code. return numsprites end -- Register our Lua functions as callbacks from C. ffiC.saveboard_maptext = saveboard_maptext ffiC.loadboard_maptext = loadboard_maptext