modlib/minetest/texmod/read.lua
2023-05-24 19:39:20 +02:00

327 lines
6.8 KiB
Lua

local texmod = ...
local colorspec = modlib.minetest.colorspec
local transforms = {
[0] = {},
{rotation_deg = 90},
{rotation_deg = 180},
{rotation_deg = 270},
{flip_axis = "x"},
{flip_axis = "x", rotation_deg = 90},
{flip_axis = "y"},
{flip_axis = "y", rotation_deg = 90},
}
return function(read_char)
-- 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 function peek()
if eof then return end
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 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")
end,
inventorycube = function()
local function read_side()
assert(not invcube, "can't nest inventorycube")
invcube = true
assert(match"{", "'{' expected")
local res = texp()
invcube = false
return res
end
local top = read_side()
local left = read_side()
local right = read_side()
return top, left, right
end,
combine = function()
expect":"
local w = int()
expect"x"
local h = int()
local blits = {}
while match":" do
if eof then break end -- we can just end with `:`, right?
local x = int()
expect","
local y = int()
expect"="
level = level + 1
local t = texp()
level = level - 1
table.insert(blits, {x = x, y = y, texture = t})
end
return w, h, blits
end,
}
local function fname()
-- 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()
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()
-- 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()
expect":"
local framecount = int()
expect":"
local frame = int()
if match":" then
return framecount, frame, int()
end
return framecount, frame
end
local param_readers = {
brighten = function()end,
noalpha = function()end,
resize = function()
expect":"
local w = int()
expect"x"
local h = int()
return w, h
end,
makealpha = function()
expect":"
local r = int()
expect","
local g = int()
expect","
local b = int()
return r, g, b
end,
opacity = function()
expect":"
local ratio = int()
return ratio
end,
invert = function()
expect":"
local channels = {}
while true do
local c = match_charset"[rgba]"
if not c then break end
channels[c] = true
end
return channels
end,
transform = function()
if match"I" then
return
end
local flip_axis
if match"F" then
flip_axis = assert(match_charset"[XY]", "axis expected"):lower()
end
local rot_deg
if match"R" then
rot_deg = int()
end
if flip_axis or rot_deg then
return flip_axis, rot_deg
end
local transform = assert(transforms[int()], "out of range")
return transform.flip_axis, transform.rotation_deg
end,
verticalframe = function()
expect":"
local framecount = int()
expect":"
local frame = int()
return framecount, frame
end,
crack = crack,
cracko = crack,
sheet = function()
expect":"
local w = int()
expect"x"
local h = int()
expect":"
local x = int()
expect","
local y = int()
return w, h, x, y
end,
multiply = function()
expect":"
return pcolorspec()
end,
colorize = function()
expect":"
local color = pcolorspec()
if not match":" then
return color
end
if not match"a" then
return color, int()
end
for c in ("lpha"):gmatch"." do
expect(c)
end
return color, "alpha"
end,
mask = function()
expect":"
return subtexp()
end,
lowpart = function()
expect":"
local percent = int()
assert(percent)
expect":"
return percent, subtexp()
end,
}
function texp()
local base = basexp()
while hat() do
if match"[" then
local name = match_str"[a-z]"
local param_reader = param_readers[name]
local gen_reader = read_base[name]
if not (param_reader or gen_reader) then
error("invalid texture modifier: " .. name)
end
if param_reader then
base = base[name](base, param_reader())
elseif gen_reader then
base = base:overlay(texmod[name](gen_reader()))
end
else
base = base:overlay(basexp())
end
end
return base
end
local res = texp()
assert(eof, "eof expected")
return res
end