mirror of
https://github.com/DrBeef/Raze.git
synced 2024-11-16 09:21:12 +00:00
1296f9ba55
git-svn-id: https://svn.eduke32.com/eduke32@3751 1a8010ca-5511-0410-912e-c29ae57300e0
530 lines
14 KiB
Lua
530 lines
14 KiB
Lua
-- 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]=<name>.
|
|
"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 <alsosectorp> 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)
|
|
-- 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 <tab> is a table all values of <tab> are of type <extype>.
|
|
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 <members> 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 <stab> into C struct <cs>, using the struct
|
|
-- description <members>.
|
|
-- 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
|
|
|
|
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-6
|
|
end
|
|
|
|
if (not tabofnumtabs(msector, sector_members) or
|
|
not tabofnumtabs(mwall, wall_members) or
|
|
not tabofnumtabs(msprite, sprite_members))
|
|
then
|
|
return RETERR-7
|
|
end
|
|
|
|
local numsectors, numwalls, numsprites = #msector, #mwall, #msprite
|
|
local sector, wall, sprite = ffiC.sector, ffiC.wall, ffiC.sprite
|
|
|
|
if (numsectors+0ULL > ffiC.MAXSECTORS or numwalls+0ULL > ffiC.MAXWALLS or
|
|
numsprites+0ULL > ffiC.MAXSPRITES)
|
|
then
|
|
return RETERR-8
|
|
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-9
|
|
end
|
|
end
|
|
|
|
-- Walls.
|
|
for i=0,numwalls-1 do
|
|
if (read_struct(wall[i], mwall[i+1], wall_members, wall_default)) then
|
|
return RETERR-10
|
|
end
|
|
end
|
|
|
|
-- Sprites.
|
|
for i=0,numsprites-1 do
|
|
if (read_struct(sprite[i], msprite[i+1], sprite_members, sprite_default)) then
|
|
return RETERR-11
|
|
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-12
|
|
end
|
|
|
|
-- Check .point2 at least.
|
|
if (check_bad_point2()) then
|
|
return RETERR-13
|
|
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
|