From 2e12f34c0d774d9243e819f0f0247dc684fa831c Mon Sep 17 00:00:00 2001 From: luk3yx Date: Tue, 9 Mar 2021 20:35:49 +1300 Subject: [PATCH] Initial commit --- .luacheckrc | 12 +++ LICENSE.md | 22 +++++ README.md | 47 ++++++++++ depends.txt | 1 + init.lua | 210 ++++++++++++++++++++++++++++++++++++++++++++ mod.conf | 2 + monkey_patching.lua | 120 +++++++++++++++++++++++++ 7 files changed, 414 insertions(+) create mode 100644 .luacheckrc create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 depends.txt create mode 100644 init.lua create mode 100644 mod.conf create mode 100644 monkey_patching.lua diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..06f46fb --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,12 @@ +max_line_length = 80 + +globals = { + 'formspec_ast', + 'fs51', + 'minetest', +} + +read_globals = { + string = {fields = {'split', 'trim'}}, + table = {fields = {'copy'}} +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..69f97cc --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ff2f6b --- /dev/null +++ b/README.md @@ -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 diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..da687a2 --- /dev/null +++ b/depends.txt @@ -0,0 +1 @@ +formspec_ast diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..6c46c32 --- /dev/null +++ b/init.lua @@ -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 diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..2992f09 --- /dev/null +++ b/mod.conf @@ -0,0 +1,2 @@ +name = fs51 +depends = formspec_ast diff --git a/monkey_patching.lua b/monkey_patching.lua new file mode 100644 index 0000000..95147fd --- /dev/null +++ b/monkey_patching.lua @@ -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