Initial commit

This commit is contained in:
luk3yx 2021-03-09 20:35:49 +13:00
commit 2e12f34c0d
7 changed files with 414 additions and 0 deletions

12
.luacheckrc Normal file
View File

@ -0,0 +1,12 @@
max_line_length = 80
globals = {
'formspec_ast',
'fs51',
'minetest',
}
read_globals = {
string = {fields = {'split', 'trim'}},
table = {fields = {'copy'}}
}

22
LICENSE.md Normal file
View File

@ -0,0 +1,22 @@
# The MIT License (MIT)
Copyright © 2019-2021 by luk3yx.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# fs51
A compatibility layer that makes formspec_version 3 (and later) formspecs
render more correctly in Minetest 5.1.0 and earlier.
This will work with most mods without any additional configuration. If you want
to disable automatic formspec translation, add
`fs51.disable_monkey_patching = true` to minetest.conf.
## Why?
Minetest 5.1.0 introduced changes to formspecs that made them much less painful
to create and work with. However, formspecs are interpreted client-side and to
take advantage of these changes you would normally need to force everyone to
upgrade Minetest. This mod detects these older clients and modifies formspecs
sent to them to try and make sure they are at least usable.
## How to use
1. Install the mod
2. Hope it works properly and doesn't break anything
## Troubleshooting
- If your mod stores `minetest.show_formspec` during load time, you'll need to
add `fs51` as an optional dependency to `mod.conf` so it can use the patched
show_formspec code.
## Dependencies
This mod depends on my [formspec_ast] library.
## API functions
You probably don't need to use these unless you're embedding fs51 outside of
Minetest or are using node formspecs.
- `fs51.backport(tree)`: Applies backports to a [formspec_ast] tree and
returns the modified tree. This does not modify the existing tree in place.
- `fs51.backport_string(formspec)`: Similar to
`formspec_ast.unparse(fs51.backport(formspec_ast.parse(formspec)))`.
*Unlike the automatic backporting, these functions will preserve newer elements
such as hypertext and background9 so the formspec will still work properly with
newer clients.*
[formspec_ast]: https://git.minetest.land/luk3yx/formspec_ast

1
depends.txt Normal file
View File

@ -0,0 +1 @@
formspec_ast

210
init.lua Normal file
View File

@ -0,0 +1,210 @@
--
-- fs51 - Compatibility layer for Minetest formspecs
--
-- Copyright © 2019-2021 by luk3yx.
--
fs51 = {}
local fs51 = fs51
local padding, spacing_x, spacing_y = 3/8, 5/4, 15/13
-- Random offsets
local random_offsets = {
-- box = {{0, 0}, {0.2, 0.125}},
label = {{0, 0.3}},
field = {{-padding, -0.33}, {-0.25, -0.2}},
pwdfield = {{-padding, -0.33}, {-0.25, 0}},
-- textarea = {{-0.3, -0.33}, {-0.2, 0}},
textarea = {{-padding, 0}, {-0.25, -padding}},
dropdown = {{0, 0}, {-0.25, 0}},
checkbox = {{0, 0.5}},
background = {{(1 - spacing_x) / 2, (1 - spacing_y) / 2}},
tabheader = {{-padding, -padding}},
}
local fixers = {}
local function fix_pos(elem, random_offset)
if type(elem.x) == 'number' and type(elem.y) == 'number' then
if random_offset then
elem.x = elem.x - random_offset[1][1]
elem.y = elem.y - random_offset[1][2]
end
elem.x = (elem.x - padding) / spacing_x
elem.y = (elem.y - padding) / spacing_y
end
end
local function default_fixer(elem)
local random_offset = random_offsets[elem.type]
fix_pos(elem, random_offset)
if type(elem.w) == 'number' then
if random_offset and random_offset[2] then
elem.w = elem.w - random_offset[2][1]
end
elem.w = elem.w / spacing_x
end
if type(elem.h) == 'number' then
if random_offset and random_offset[2] then
elem.h = elem.h - random_offset[2][2]
end
elem.h = elem.h / spacing_y
end
end
-- Other fixers
function fixers.image_button(elem)
fix_pos(elem, random_offsets[elem.type])
elem.w = elem.w * 0.8 + 0.205
elem.h = elem.h * 0.866 + 0.134
end
fixers.item_image_button = fixers.image_button
fixers.image_button_exit = fixers.image_button
function fixers.textarea(elem)
local h = elem.h
default_fixer(elem)
elem.h = h + 0.15
end
fixers.image = fix_pos
fixers.item_image = fixers.image
function fixers.button(elem)
elem.type = 'image_' .. elem.type
elem.texture_name = 'blank.png'
return fixers.image_button(elem)
end
fixers.button_exit = fixers.button
function fixers.size(elem)
elem.w = elem.w / spacing_x - padding * 2 + 0.36
elem.h = elem.h / spacing_y - padding * 2
end
function fixers.list(elem, next_elem)
fix_pos(elem)
if elem.h < 2 then return end
-- Split the list[] into multiple list[]s.
local start = math.max(elem.starting_item_index or 0, 0)
for row = 1, elem.h do
local r = row - 1
elem[row] = {
type = 'list',
inventory_location = elem.inventory_location,
list_name = elem.list_name,
x = 0,
y = (r * 1.25) / spacing_y,
w = elem.w,
h = 1,
starting_item_index = start + (elem.w * r),
}
-- Swap the second element and any listring[]
if row == 2 and next_elem and next_elem.type == 'listring' and
not next_elem.inventory_location then
for k, v in pairs(elem[2]) do
next_elem[k] = v
end
next_elem.x = elem.x
next_elem.y = elem.y + next_elem.y
elem[2] = {type = 'listring'}
end
end
-- Convert the base element to a container
for k, _ in pairs(elem) do
if type(k) ~= 'number' and k ~= 'x' and k ~= 'y' then
elem[k] = nil
end
end
elem.type = 'container'
end
-- Remove the "height" attribute on dropdowns.
function fixers.dropdown(elem)
fix_pos(elem)
elem.w = elem.w / spacing_y
elem.h = nil
elem.index_event = nil
end
--
local pre_types = {size = true, position = true, anchor = true,
no_prepend = true}
local xywh = {'x', 'y', 'w', 'h'}
function fs51.backport(tree)
-- Flatten the tree (this will also copy it).
tree = formspec_ast.flatten(tree)
local real_coordinates = type(tree.formspec_version) == 'number' and
tree.formspec_version >= 2
tree.formspec_version = 1
-- Check for an initial real_coordinates[].
if not real_coordinates then
for _, elem in ipairs(tree) do
if elem.type == 'real_coordinates' then
real_coordinates = elem.bool
break
elseif not pre_types[elem.type] then
break
end
end
end
-- Allow deletion of real_coordinates[]
local i = 1
while tree[i] ~= nil do
local elem = tree[i]
if elem.type == 'real_coordinates' then
real_coordinates = elem.bool
table.remove(tree, i)
i = i - 1
elseif elem.type == 'list' and real_coordinates then
fixers.list(elem, tree[i + 1])
-- Flatten containers
if elem.type == 'container' and elem[1] then
formspec_ast.apply_offset(elem, elem.x, elem.y)
tree[i] = elem[1]
for j = 2, #elem do
i = i + 1
table.insert(tree, i, elem[j])
end
end
elseif real_coordinates then
(fixers[elem.type] or default_fixer)(elem)
for _, n in ipairs(xywh) do
if elem[n] then
elem[n] = math.floor(elem[n] * 1000) / 1000
end
end
end
i = i + 1
end
return tree
end
local minetest_log = rawget(_G, 'minetest') and minetest.log or print
function fs51.backport_string(formspec)
local fs, err = formspec_ast.parse(formspec)
if not fs then
minetest_log('warning', '[fs51] Error parsing formspec: ' ..
tostring(err))
return nil, err
end
return formspec_ast.unparse(fs51.backport(fs))
end
-- Monkey patch Minetest's code
if rawget(_G, 'minetest') and minetest.register_on_player_receive_fields and
not minetest.settings:get_bool('fs51.disable_monkey_patching') then
dofile(minetest.get_modpath('fs51') .. '/monkey_patching.lua')
end

2
mod.conf Normal file
View File

@ -0,0 +1,2 @@
name = fs51
depends = formspec_ast

120
monkey_patching.lua Normal file
View File

@ -0,0 +1,120 @@
--
-- fs51 - Compatibility layer for Minetest formspecs
--
-- Copyright © 2021 by luk3yx.
--
local get_player_information = minetest.get_player_information
local type = type
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 >= 3 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 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 = node.text:gsub('<[^>]+>', '')
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
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)
-- 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 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