fs51/monkey_patching.lua

193 lines
6.6 KiB
Lua

--
-- fs51 - Compatibility layer for Minetest formspecs
--
-- Copyright © 2021 by luk3yx.
--
local fixers = ...
local get_player_information, type = minetest.get_player_information, type
local function remove_hypertext(text)
-- If the text doesn't contain backslashes use gsub for performance
if not text:find('\\', 1, true) then
return text:gsub('<[^>]+>', '')
end
-- Otherwise iterate over it
local res = ''
local escaping, ignoring
for i = 1, #text do
local char = text:sub(i, i)
if ignoring then
ignoring = char ~= '>'
elseif escaping then
res = res .. char
escaping = false
elseif char == '<' then
ignoring = true
elseif char == '\\' then
escaping = true
else
res = res .. char
end
end
return res
end
-- Backport index_event by modifying the dropdown items so that they all start
-- with \x1b(fs51@idx_<N>). This is then parsed out here if the player has been
-- shown any formspec with a dropdown that has index_event. The extra check is
-- done to prevent trying to parse every single field for every single player.
local dropdown_hack_enabled = {}
minetest.after(0, minetest.register_on_player_receive_fields,
function(player, _, fields)
if dropdown_hack_enabled[player:get_player_name()] then
local to_update = {}
for field, raw_value in pairs(fields) do
if field:sub(1, 6) == "\1fs51\1" then
to_update[field] = raw_value:match("^\27%(fs51@idx_([0-9]+)%)")
end
end
for field, value in pairs(to_update) do
fields[field] = nil
fields[field:sub(7)] = value
end
end
end)
minetest.register_on_leaveplayer(function(player)
dropdown_hack_enabled[player:get_player_name()] = nil
end)
local function backport_for(name, formspec)
local info = get_player_information(name)
local formspec_version = info and info.formspec_version or 1
if formspec_version >= 4 then return formspec end
local tree, err = formspec_ast.parse(formspec)
if not tree then
minetest.log('warning', '[fs51] Error parsing formspec (in ' ..
'monkey_patching.lua): ' .. tostring(err))
return formspec
end
-- Add some placeholders
local modified
for node in formspec_ast.walk(tree) do
local node_type = node.type
if node_type == "dropdown" and node.index_event and node.items then
-- Enable the dropdown hack for this player
dropdown_hack_enabled[name] = true
modified = true
node.name = "\1fs51\1" .. node.name
for i, item in ipairs(node.items) do
node.items[i] = "\27(fs51@idx_" .. i .. ")" .. item
end
node.index_event = nil
elseif formspec_version == 3 then -- luacheck: ignore 542
-- Don't do anything else
elseif formspec_version == 1 and node_type == 'background9' then
-- No need to set modified here
node.type = 'background'
node.middle_x, node.middle_y = nil, nil
node.middle_x2, node.middle_y2 = nil, nil
elseif node_type == 'animated_image' then
modified = true
node.type = 'image'
local frame_start = node.frame_start or 1
node.texture_name = ('(%s)^[verticalframe:%d:%d'):format(
node.texture_name, node.frame_count, frame_start - 1)
elseif node_type == 'model' and node.textures[1] then
modified = true
node.type = 'image'
node.texture_name = node.textures[1]
elseif node_type == 'hypertext' then
-- Convert hypertext elements to regular textareas
modified = true
node.type = 'textarea'
node.name = ''
node.label = ''
node.default = remove_hypertext(node.text)
node.text = nil
elseif node_type == 'scroll_container' then
modified = true
node.type = 'container'
-- Scroll containers are always going to be broken on older clients
for i = #node, 1, -1 do
local inner_node = node[i]
if inner_node.x and inner_node.y and
(inner_node.x >= node.w or inner_node.y >= node.h) then
table.remove(node, i)
end
end
elseif formspec_version == 1 and node_type == 'tabheader' then
node.w, node.h = nil, nil
elseif formspec_version == 2 and node_type == 'bgcolor' then
modified = true
fixers.bgcolor(node)
end
end
if formspec_version == 1 then
modified = true
tree = fs51.backport(tree)
end
if modified then
return assert(formspec_ast.unparse(tree))
end
return formspec
end
-- Patch minetest.show_formspec()
local show_formspec = minetest.show_formspec
function minetest.show_formspec(pname, formname, formspec)
return show_formspec(pname, formname, backport_for(pname, formspec))
end
-- Patch player:set_inventory_formspec()
local old_set_inventory_formspec
local function new_set_inventory_formspec(self, formspec, ...)
return old_set_inventory_formspec(self,
backport_for(self:get_player_name(), formspec), ...)
end
minetest.register_on_joinplayer(function(player)
if old_set_inventory_formspec == nil then
assert(type(player) == 'userdata', 'Fake player object?')
local cls = getmetatable(player)
old_set_inventory_formspec = cls.set_inventory_formspec
cls.set_inventory_formspec = new_set_inventory_formspec
-- In case the inventory formspec has been set in the meantime
player:set_inventory_formspec(player:get_inventory_formspec())
end
end)
if minetest.settings:get_bool('fs51.disable_meta_override', true) then
return
end
-- Patch minetest.get_meta()
-- Inspired by https://gitlab.com/sztest/nodecore/-/blob/master/mods/nc_api
local old_nodemeta_set_string
local function new_nodemeta_set_string(self, k, v)
if k == 'formspec' and type(v) == 'string' then
v = fs51.backport_string(v) or v
end
return old_nodemeta_set_string(self, k, v)
end
local get_meta = minetest.get_meta
function minetest.get_meta(...)
local meta = get_meta(...)
if old_nodemeta_set_string == nil and type(meta) == 'userdata' then
minetest.get_meta = get_meta
local cls = getmetatable(meta)
old_nodemeta_set_string = cls.set_string
cls.set_string = new_nodemeta_set_string
end
return meta
end