Refactor texmod:read to pass reader around

This commit is contained in:
Lars Mueller 2023-05-24 22:27:25 +02:00
parent fe7fb6aeec
commit 95159c48e8

@ -12,316 +12,319 @@ local transforms = {
{flip_axis = "y", rotation_deg = 90}, {flip_axis = "y", rotation_deg = 90},
} }
return function(read_char) -- Generator readers
-- TODO this currently uses closures rather than passing around a "reader" object,
-- which is inconsistent with the writer and harder to port to more static languages
local level = 0
local invcube = false
local eof = false
local escapes, character local gr = {}
local function peek() function gr.png(r)
if eof then return end r:expect":"
local expected_escapes = 0 local base64 = r:match_str"[a-zA-Z0-9+/=]"
if level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, level)
end
if character:match"[&^:]" then
if escapes == expected_escapes then return character end
elseif escapes <= expected_escapes then
return character
elseif escapes >= 2*expected_escapes then
return "\\"
end
end
local function popchar()
escapes = 0
while true do
character = read_char()
if character ~= "\\" then break end
escapes = escapes + 1
end
if character == nil then
assert(escapes == 0, "end of texmod expected")
eof = true
end
end
popchar()
local function pop()
local expected_escapes = 0
if level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, level)
end
if escapes > 0 and escapes >= 2*expected_escapes then
escapes = escapes - 2*expected_escapes
return
end
return popchar()
end
local function match(char)
if peek() == char then
pop()
return true
end
end
local function expect(char)
if not match(char) then
error(("%q expected"):format(char))
end
end
local function hat()
return match(invcube and "&" or "^")
end
local function match_charset(set)
local char = peek()
if char and char:match(set) then
pop()
return char
end
end
local function match_str(set)
local c = match_charset(set)
if not c then error ("character in " .. set .. " expected") end
local t = {c}
while true do
c = match_charset(set)
if not c then break end
table.insert(t, c)
end
return table.concat(t)
end
local function int()
local sign = 1
if match"-" then sign = -1 end
return sign * tonumber(match_str"%d")
end
local texp
local function subtexp()
level = level + 1
local res = texp()
level = level - 1
return res
end
local read_base = {
png = function()
expect":"
local base64 = match_str"[a-zA-Z0-9+/=]"
return assert(minetest.decode_base64(base64), "invalid base64") return assert(minetest.decode_base64(base64), "invalid base64")
end, end
inventorycube = function()
local function read_side() function gr.inventorycube(r)
assert(not invcube, "can't nest inventorycube") local top = r:invcubeside()
invcube = true local left = r:invcubeside()
assert(match"{", "'{' expected") local right = r:invcubeside()
local res = texp()
invcube = false
return res
end
local top = read_side()
local left = read_side()
local right = read_side()
return top, left, right return top, left, right
end, end
combine = function()
expect":" function gr.combine(r)
local w = int() r:expect":"
expect"x" local w = r:int()
local h = int() r:expect"x"
local h = r:int()
local blits = {} local blits = {}
while match":" do while r:match":" do
if eof then break end -- we can just end with `:`, right? if r.eof then break end -- we can just end with `:`, right?
local x = int() local x = r:int()
expect"," r:expect","
local y = int() local y = r:int()
expect"=" r:expect"="
level = level + 1 table.insert(blits, {x = x, y = y, texture = r:subtexp()})
local t = texp()
level = level - 1
table.insert(blits, {x = x, y = y, texture = t})
end end
return w, h, blits return w, h, blits
end, end
}
local function fname() -- Parameter readers
-- This is overly permissive, as is Minetest;
-- we just allow arbitrary characters up until a character which may terminate the name.
-- Inside an inventorycube, `&` also terminates names.
return match_str(invcube and "[^:^&){]" or "[^:^){]")
end
local function basexp() local pr = {}
if match"(" then
local res = texp()
expect")"
return res
end
if match"[" then
local name = match_str"[a-z]"
local reader = read_base[name]
if not reader then
error("invalid texture modifier: " .. name)
end
return texmod[name](reader())
end
return texmod.file(fname())
end
local function pcolorspec() function pr.brighten() end
-- Leave exact validation up to colorspec, only do a rough greedy charset matching
return assert(colorspec.from_string(match_str"[#%xa-z]"))
end
local function crack() function pr.noalpha() end
expect":"
local framecount = int()
expect":"
local frame = int()
if match":" then
return framecount, frame, int()
end
return framecount, frame
end
local param_readers = { function pr.resize(r)
brighten = function()end, r:expect":"
noalpha = function()end, local w = r:int()
resize = function() r:expect"x"
expect":" local h = r:int()
local w = int()
expect"x"
local h = int()
return w, h return w, h
end, end
makealpha = function()
expect":" function pr.makealpha(r)
local r = int() r:expect":"
expect"," local red = r:int()
local g = int() r:expect","
expect"," local green = r:int()
local b = int() r:expect","
return r, g, b local blue = r:int()
end, return red, green, blue
opacity = function() end
expect":"
local ratio = int() function pr.opacity(r)
r:expect":"
local ratio = r:int()
return ratio return ratio
end, end
invert = function()
expect":" function pr.invert(r)
r:expect":"
local channels = {} local channels = {}
while true do while true do
local c = match_charset"[rgba]" local c = r:match_charset"[rgba]"
if not c then break end if not c then break end
channels[c] = true channels[c] = true
end end
return channels return channels
end, end
transform = function()
if match"I" then function pr.transform(r)
if r:match"I" then
return return
end end
local flip_axis local flip_axis
if match"F" then if r:match"F" then
flip_axis = assert(match_charset"[XY]", "axis expected"):lower() flip_axis = assert(r:match_charset"[XY]", "axis expected"):lower()
end end
local rot_deg local rot_deg
if match"R" then if r:match"R" then
rot_deg = int() rot_deg = r:int()
end end
if flip_axis or rot_deg then if flip_axis or rot_deg then
return flip_axis, rot_deg return flip_axis, rot_deg
end end
local transform = assert(transforms[int()], "out of range") local transform = assert(transforms[r:int()], "out of range")
return transform.flip_axis, transform.rotation_deg return transform.flip_axis, transform.rotation_deg
end, end
verticalframe = function()
expect":" function pr.verticalframe(r)
local framecount = int() r:expect":"
expect":" local framecount = r:int()
local frame = int() r:expect":"
local frame = r:int()
return framecount, frame return framecount, frame
end, end
crack = crack,
cracko = crack, function pr.crack(r)
sheet = function() r:expect":"
expect":" local framecount = r:int()
local w = int() r:expect":"
expect"x" local frame = r:int()
local h = int() if r:match":" then
expect":" return framecount, frame, r:int()
local x = int() end
expect"," return framecount, frame
local y = int() end
pr.cracko = pr.crack
function pr.sheet(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
local x = r:int()
r:expect","
local y = r:int()
return w, h, x, y return w, h, x, y
end, end
multiply = function()
expect":" function pr.multiply(r)
return pcolorspec() r:expect":"
end, return r:colorspec()
colorize = function() end
expect":"
local color = pcolorspec() function pr.colorize(r)
if not match":" then r:expect":"
local color = r:colorspec()
if not r:match":" then
return color return color
end end
if not match"a" then if not r:match"a" then
return color, int() return color, r:int()
end end
for c in ("lpha"):gmatch"." do for c in ("lpha"):gmatch"." do
expect(c) r:expect(c)
end end
return color, "alpha" return color, "alpha"
end, end
mask = function()
expect":"
return subtexp()
end,
lowpart = function()
expect":"
local percent = int()
assert(percent)
expect":"
return percent, subtexp()
end,
}
function texp() function pr.mask(r)
local base = basexp() r:expect":"
while hat() do return r:subtexp(r)
if match"[" then end
local name = match_str"[a-z]"
local param_reader = param_readers[name] function pr.lowpart(r)
local gen_reader = read_base[name] r:expect":"
local percent = r:int()
assert(percent)
r:expect":"
return percent, r:subtexp()
end
-- Reader methods. We use `r` instead of the `self` "sugar" for consistency (and to save us some typing).
local rm = {}
function rm.peek(r)
if r.eof then return end
local expected_escapes = 0
if r.level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, r.level)
end
if r.character:match"[&^:]" then
if r.escapes == expected_escapes then return r.character end
elseif r.escapes <= expected_escapes then
return r.character
elseif r.escapes >= 2*expected_escapes then
return "\\"
end
end
function rm.popchar(r)
r.escapes = 0
while true do
r.character = r:read_char()
if r.character ~= "\\" then break end
r.escapes = r.escapes + 1
end
if r.character == nil then
assert(r.escapes == 0, "end of texmod expected")
r.eof = true
end
end
function rm.pop(r)
local expected_escapes = 0
if r.level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, r.level)
end
if r.escapes > 0 and r.escapes >= 2*expected_escapes then
r.escapes = r.escapes - 2*expected_escapes
return
end
return r:popchar()
end
function rm.match(r, char)
if r:peek() == char then
r:pop()
return true
end
end
function rm.expect(r, char)
if not r:match(char) then
error(("%q expected"):format(char))
end
end
function rm.hat(r)
return r:match(r.invcube and "&" or "^")
end
function rm.match_charset(r, set)
local char = r:peek()
if char and char:match(set) then
r:pop()
return char
end
end
function rm.match_str(r, set)
local c = r:match_charset(set)
if not c then
error(("character in %s expected"):format(set))
end
local t = {c}
while true do
c = r:match_charset(set)
if not c then break end
table.insert(t, c)
end
return table.concat(t)
end
function rm.int(r)
local sign = 1
if r:match"-" then sign = -1 end
return sign * tonumber(r:match_str"%d")
end
function rm.fname(r)
-- This is overly permissive, as is Minetest;
-- we just allow arbitrary characters up until a character which may terminate the name.
-- Inside an inventorycube, `&` also terminates names.
return r:match_str(r.invcube and "[^:^&){]" or "[^:^){]")
end
function rm.subtexp(r)
r.level = r.level + 1
local res = r:texp()
r.level = r.level - 1
return res
end
function rm.invcubeside(r)
assert(not r.invcube, "can't nest inventorycube")
r.invcube = true
assert(r:match"{", "'{' expected")
local res = r:texp()
r.invcube = false
return res
end
function rm.basexp(r)
if r:match"(" then
local res = r:texp()
r:expect")"
return res
end
if r:match"[" then
local type = r:match_str"[a-z]"
local gen_reader = gr[type]
if not gen_reader then
error("invalid texture modifier: " .. type)
end
return texmod[type](gen_reader(r))
end
return texmod.file(r:fname())
end
function rm.colorspec(r)
-- Leave exact validation up to colorspec, only do a rough greedy charset matching
return assert(colorspec.from_string(r:match_str"[#%xa-z]"))
end
function rm.texp(r)
local base = r:basexp()
while r:hat() do
if r:match"[" then
local type = r:match_str"[a-z]"
local param_reader, gen_reader = pr[type], gr[type]
if not (param_reader or gen_reader) then if not (param_reader or gen_reader) then
error("invalid texture modifier: " .. name) error("invalid texture modifier: " .. type)
end end
if param_reader then if param_reader then
base = base[name](base, param_reader()) base = base[type](base, param_reader(r))
elseif gen_reader then elseif gen_reader then
base = base:overlay(texmod[name](gen_reader())) base = base:overlay(texmod[type](gen_reader(r)))
end end
else else
base = base:overlay(basexp()) base = base:overlay(r:basexp())
end end
end end
return base return base
end end
local res = texp()
assert(eof, "eof expected") local mt = {__index = rm}
return function(read_char)
local r = setmetatable({
level = 0,
invcube = false,
eof = false,
read_char = read_char,
}, mt)
r:popchar()
local res = r:texp()
assert(r.eof, "eof expected")
return res return res
end end