diff --git a/.github/workflows/busted.yml b/.github/workflows/busted.yml index 862ac55..1ba6394 100644 --- a/.github/workflows/busted.yml +++ b/.github/workflows/busted.yml @@ -4,7 +4,6 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest steps: @@ -15,6 +14,11 @@ jobs: run: sudo apt-get install -y luarocks - name: busted install run: luarocks install --local busted - - name: busted run + + - name: busted run metatool working-directory: ./metatool run: $HOME/.luarocks/bin/busted + + - name: busted run containertool + working-directory: ./containertool + run: $HOME/.luarocks/bin/busted diff --git a/.luacheckrc b/.luacheckrc index 626538d..ec572c3 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -10,11 +10,14 @@ globals = { } read_globals = { + -- Engine "minetest", - "default", - "pipeworks", "vector", "ItemStack", "Settings", "dump", + -- Mods + "default", + "pipeworks", + "technic", } diff --git a/containertool/README.md b/containertool/README.md new file mode 100644 index 0000000..02bdf88 --- /dev/null +++ b/containertool/README.md @@ -0,0 +1,39 @@ +## Container tool basics + +Container tool is made available for copying container settings from one node to another. +For example it can be used to copy settings from one container to another. + +#### Copy settings from compatible nodes + +Hold tool in your hand and point node that you want to copy settings from, hold special or sneak button and left click on node to copy settings. +Chat will display confirmation message when settings is copied into tool memory. + +#### Apply copied settings to compatible nodes + +Hold tool containing desired settings in you hand and point node that you want apply settings to. +Left click with tool to apply new settings, chat will display confirmation message when settings is applied to pointed node. + +## Nodes compatible with Container tool + +r = ability to read +w = ability to write + +* technic chests (r/w) +* technic self contained injector (r/w) +* technic machines with inventory (r/w) +* default wooden chests (r/w) +* more_chests:shared (r/w) +* digilines:chest (r/w) + +## Minetest protection checks (default settings) + +Protection checks are done automatically for all tool uses, node registration does not need any protection checks. +Tool can be used to read settings from protected nodes but it cannot be used to write settings to protected nodes. + +## Configuration + +Container tool configuration keys with default values (where * is any containertool node): + +``` +metatool:containertool:nodes:*:protection_bypass_read = interact +``` diff --git a/containertool/init.lua b/containertool/init.lua new file mode 100644 index 0000000..931b865 --- /dev/null +++ b/containertool/init.lua @@ -0,0 +1,122 @@ +-- +-- metatool:containertool is in game tool that allows cloning container configuration +-- + +local tool = metatool:register_tool('containertool', { + description = 'Container tool', + name = 'Container tool', + texture = 'containertool.png', + recipe = { + { '', '', 'default:mese_crystal' }, + { '', 'default:chest', '' }, + { 'default:skeleton_key', '', '' } + }, + settings = { + copy_key_lock_secret = true, + copy_digiline_channel = false, + }, +}) + +local function has_digiline(name) + local nodedef = minetest.registered_nodes[name] + return nodedef and nodedef.digiline and nodedef.digiline.receptor +end + +local function has_key_lock(name) + local nodedef = minetest.registered_nodes[name] + return nodedef and type(nodedef.on_skeleton_key_use) == "function" +end + +local function is_tubedevice(name, pos) + local nodedef = minetest.registered_nodes[name] + if nodedef and nodedef.groups and nodedef.groups.tubedevice_receiver then + if nodedef.tube and (not pos or nodedef.tube.input_inventory) then + return true + elseif pos then + local formspec = minetest.get_meta(pos):get("formspec") + return formspec and formspec:find("fs_helpers_cycling:%d+:splitstacks") + end + end +end + +local function description(meta, node, pos) + local nicename = meta:get("infotext") or minetest.registered_nodes[node.name].description or node.name + return ("%s at %s"):format(nicename, minetest.pos_to_string(pos)) +end + +local function get_digiline_channel(meta, node) + if tool.settings.copy_digiline_channel and has_digiline(node.name) then + return meta:get_string("channel") + end +end + +local function get_key_lock_secret(meta, player, owner) + if tool.settings.copy_key_lock_secret and player:get_player_name() == owner then + return meta:get("key_lock_secret") + end +end + +local function get_splitstacks(meta, node, pos) + return is_tubedevice(node.name, pos) and meta:get_int("splitstacks") +end + +tool:ns({ + description = description, + is_tubedevice = is_tubedevice, + has_digiline = has_digiline, + get_digiline_channel = get_digiline_channel, + get_common_attributes = function(meta, node, pos, player) + local owner = meta:get("owner") + return { + description = description(meta, node, pos), + owner = owner, + key_lock_secret = get_key_lock_secret(meta, player, owner), + channel = get_digiline_channel(meta, node), + splitstacks = get_splitstacks(meta, node), + } + end, + set_digiline_meta = function(meta, data, node) + if has_digiline(node.name) then + for key, value in pairs(data) do + if key ~= "channel" or tool.settings.copy_digiline_channel then + if type(value) == "string" then + meta:set_string(key, value) + elseif type(value) == "number" then + meta:set_int(key, value) + end + end + end + end + end, + set_key_lock_secret = function(meta, data, node) + if tool.settings.copy_key_lock_secret and data.key_lock_secret and has_key_lock(node.name) then + meta:set_string("key_lock_secret", data.key_lock_secret) + end + end, + set_splitstacks = function(meta, data, node, pos) + if type(data.splitstacks) == "number" and is_tubedevice(node.name, pos) then + meta:set_int("splitstacks", data.splitstacks) + end + end, + get_int = function(meta, key) + local value = meta:get(key) + return value and tonumber(value) + end, + set_int = function(meta, key, value) + if value then meta:set_int(key, value) end + end, + set_string = function (meta, key, value) + if value then meta:set_string(key, value) end + end, +}) + +-- nodes +local modpath = minetest.get_modpath('containertool') +tool:load_node_definition(dofile(modpath .. '/nodes/technic_chests.lua')) +tool:load_node_definition(dofile(modpath .. '/nodes/more_chests_shared.lua')) +tool:load_node_definition(dofile(modpath .. '/nodes/digilines_chest.lua')) + +-- Register after everything else, default behavior for nodes that seems to be compatible +minetest.register_on_mods_loaded(function() + tool:load_node_definition(dofile(modpath .. '/nodes/common_defaults.lua')) +end) diff --git a/containertool/mod.conf b/containertool/mod.conf new file mode 100644 index 0000000..78f041d --- /dev/null +++ b/containertool/mod.conf @@ -0,0 +1,4 @@ +name=containertool +description=Provides metatool:containertool to copy/paste container settings +depends=metatool +optional_depends=default,technic_chests,more_chests,digilines diff --git a/containertool/nodes/common_defaults.lua b/containertool/nodes/common_defaults.lua new file mode 100644 index 0000000..cebf455 --- /dev/null +++ b/containertool/nodes/common_defaults.lua @@ -0,0 +1,82 @@ +-- +-- Register rest of compatible nodes for Container tool +-- + +local ns = metatool.ns('containertool') + +-- Node feature checker +local is_tubedevice = ns.is_tubedevice +-- Base metadata reader +local get_common_attributes = ns.get_common_attributes +-- Special metadata setters +local set_key_lock_secret = ns.set_key_lock_secret +local set_digiline_meta = ns.set_digiline_meta +local set_splitstacks = ns.set_splitstacks + +-- Blacklist some nodes +local tubedevice_blacklist = { + "^technic:.*_battery_box", + "^technic:.*tool_workshop", + "^pipeworks:dispenser", + "^pipeworks:nodebreaker", + "^pipeworks:deployer", + "^digtron:", + "^jumpdrive:", + "^vacuum:", +} +local function blacklisted(name) + for _,value in ipairs(tubedevice_blacklist) do + if name:find(value) then return true end + end +end + +-- Collect nodes and on_receive_fields callback functions +local nodes = {} +local on_receive_fields = {} +for nodename, nodedef in pairs(minetest.registered_nodes) do print(nodename) + if is_tubedevice(nodename) and not blacklisted(nodename) then + -- Match found, add to registration list + table.insert(nodes, nodename) + if nodedef.on_receive_fields then + on_receive_fields[nodename] = nodedef.on_receive_fields + end + end +end + +local definition = { + name = 'common_container', + nodes = nodes, + group = 'container', + protection_bypass_read = "interact", +} + +function definition:before_write(pos, player) + -- Stay safe and check both owner and protection for unknown nodes + local meta = minetest.get_meta(pos) + local owner = meta:get("owner") + local owner_check = owner == nil or owner == player:get_player_name() + if not owner_check then + minetest.record_protection_violation(pos, player:get_player_name()) + end + return owner_check and metatool.before_write(self, pos, player) +end + +function definition:copy(node, pos, player) + -- Read common data like owner, splitstacks, channel etc. + return get_common_attributes(minetest.get_meta(pos), node, pos, player) +end + +function definition:paste(node, pos, player, data) + local meta = minetest.get_meta(pos) + set_key_lock_secret(meta, data, node) + set_splitstacks(meta, data, node, pos) + set_digiline_meta(meta, {channel = data.channel}, node) + -- Yeah, sorry... everyone just keeps their internal stuff "protected" + if on_receive_fields[node.name] then + if not pcall(function()on_receive_fields[node.name](pos, "", {}, player)end) then + pcall(function()on_receive_fields[node.name](pos, "", {quit=1}, player)end) + end + end +end + +return definition diff --git a/containertool/nodes/digilines_chest.lua b/containertool/nodes/digilines_chest.lua new file mode 100644 index 0000000..23cbc77 --- /dev/null +++ b/containertool/nodes/digilines_chest.lua @@ -0,0 +1,34 @@ +-- +-- Register digilines chest for containertool +-- + +local ns = metatool.ns('containertool') +local description = ns.description +local get_digiline_channel = ns.get_digiline_channel + +local definition = { + name = 'digilines_chest', + nodes = "digilines:chest", + group = 'container', + protection_bypass_read = "interact", +} + +function definition:copy(node, pos, player) + local meta = minetest.get_meta(pos) + local channel = get_digiline_channel(meta, node) + if channel then + return { + description = description(meta, node, pos), + channel = channel, + } + end +end + +function definition:paste(node, pos, player, data) + if data.channel and metatool.settings("containertool", "copy_digiline_channel") then + local nodedef = minetest.registered_nodes[node.name] + nodedef.on_receive_fields(pos, "", {channel=data.channel}, player) + end +end + +return definition diff --git a/containertool/nodes/more_chests_shared.lua b/containertool/nodes/more_chests_shared.lua new file mode 100644 index 0000000..5fefeb9 --- /dev/null +++ b/containertool/nodes/more_chests_shared.lua @@ -0,0 +1,28 @@ +-- +-- Register shared chest for containertool +-- + +local ns = metatool.ns('containertool') +local description = ns.description + +local definition = { + name = 'shared_chest', + nodes = "more_chests:shared", + group = 'container', + protection_bypass_read = "interact", +} + +function definition:copy(node, pos, player) + local meta = minetest.get_meta(pos) + return { + description = description(meta, node, pos), + shared_with = meta:get("shared"), + } +end + +function definition:paste(node, pos, player, data) + local nodedef = minetest.registered_nodes[node.name] + nodedef.on_receive_fields(pos, "", {shared=data.shared_with}, player) +end + +return definition diff --git a/containertool/nodes/technic_chests.lua b/containertool/nodes/technic_chests.lua new file mode 100644 index 0000000..df626dd --- /dev/null +++ b/containertool/nodes/technic_chests.lua @@ -0,0 +1,130 @@ +-- +-- Register technic chests for Container tool +-- + +-- Collect nodes and on_receive_fields callback functions (no API available) +local nodes = {} +local on_receive_fields = {} +for nodename, nodedef in pairs(minetest.registered_nodes) do + if nodedef.groups and nodedef.groups.technic_chest then + -- Match found, add to registration list + table.insert(nodes, nodename) + on_receive_fields[nodename] = nodedef.on_receive_fields + end +end + +-- Collect lookup data for colored variants (no API available) +local colornode2basenode = {} +local basenode2colornode = {} +for _, nodename in ipairs(nodes) do + for i,colordef in ipairs(technic.chests.colors) do + local color_nodename = nodename .. "_" .. colordef[1] + local nodedef = minetest.registered_nodes[color_nodename] + if nodedef and nodedef.groups and nodedef.groups.technic_chest then + colornode2basenode[color_nodename] = nodename + if not basenode2colornode[nodename] then basenode2colornode[nodename] = {} end + -- This can leave holes depending on what colors chest actually uses, always use `pairs` to iterate + basenode2colornode[nodename][i] = color_nodename + end + end +end + +local ns = metatool.ns('containertool') + +-- Helpers +local has_digiline = ns.has_digiline +-- Base metadata reader +local get_common_attributes = ns.get_common_attributes +-- Special metadata setters +local set_key_lock_secret = ns.set_key_lock_secret +local set_digiline_meta = ns.set_digiline_meta +local set_splitstacks = ns.set_splitstacks +-- Common metadata setters/getters +local get_int = ns.get_int +local set_int = ns.set_int +local set_string = ns.set_string + +local function set_color(meta, node, pos, color) + if color then + local is_color = not not technic.chests.colors[color] + local newname + if is_color then + -- Set color + newname = basenode2colornode[node.name] and basenode2colornode[node.name][color] + if not newname then + local basenode = colornode2basenode[node.name] + newname = basenode2colornode[basenode] and basenode2colornode[basenode][color] + end + else + -- Remove color + newname = colornode2basenode[node.name] + end + if newname and newname ~= node.name then + node.name = newname + minetest.swap_node(pos, node) + set_string(meta, "color", is_color and color or "") + end + end +end + +local definition = { + name = 'technic_chest', + nodes = nodes, + group = 'container', + protection_bypass_read = "interact", + settings = { + copy_color = true, + }, +} + +function definition:before_write(pos, player) + return technic.chests.change_allowed(pos, player, true, true) +end + +function definition:copy(node, pos, player) + local meta = minetest.get_meta(pos) + local has_color = not not (basenode2colornode[node.name] or colornode2basenode[node.name]) + -- Read common data like owner, splitstacks, channel etc. + local data = get_common_attributes(meta, node, pos, player) + -- Information/interface + data.color = self.settings.copy_color and (get_int(meta, "color") or has_color) + data.sort_mode = get_int(meta, "sort_mode") + data.autosort = get_int(meta, "autosort") + -- Digilines + if has_digiline(node.name) then + -- Chests seems to be clearing unchecked meta so we do the same + data.technic_chest_put = get_int(meta, "send_put") or "" + data.technic_chest_take = get_int(meta, "send_take") or "" + data.technic_chest_inject = get_int(meta, "send_inject") or "" + data.technic_chest_pull = get_int(meta, "send_pull") or "" + data.technic_chest_overflow = get_int(meta, "send_overflow") or "" + end + -- Return collected data + return data +end + +function definition:paste(node, pos, player, data) + local meta = minetest.get_meta(pos) + -- Information/interface + set_color(meta, node, pos, data.color) + set_int(meta, "sort_mode", data.sort_mode) + set_int(meta, "autosort", data.autosort) + -- Security + set_key_lock_secret(meta, data, node) + -- Pipeworks + set_splitstacks(meta, data, node, pos) + -- Digilines + local digiline_data = { + channel = data.channel, + send_put = data.technic_chest_put, + send_take = data.technic_chest_take, + send_inject = data.technic_chest_inject, + send_pull = data.technic_chest_pull, + send_overflow = data.technic_chest_overflow, + } + set_digiline_meta(meta, digiline_data, node) + -- Update formspec + on_receive_fields[node.name](pos, nil, {quit=1}, player) +end + +return definition diff --git a/containertool/spec/fixtures/metatool.cfg b/containertool/spec/fixtures/metatool.cfg new file mode 100644 index 0000000..b9c0555 --- /dev/null +++ b/containertool/spec/fixtures/metatool.cfg @@ -0,0 +1 @@ +metatool:containertool:machine_use_priv = interact diff --git a/containertool/spec/fixtures/nodes.lua b/containertool/spec/fixtures/nodes.lua new file mode 100644 index 0000000..758a2a6 --- /dev/null +++ b/containertool/spec/fixtures/nodes.lua @@ -0,0 +1,39 @@ +minetest.register_node("technic:test_chest", { + description = "Technic chest", + groups = { + snappy = 2, choppy = 2, oddly_breakable_by_hand = 2, + tubedevice = 1, tubedevice_receiver = 1, technic_chest = 1, + }, + on_receive_fields = function(...) print(...) end, + on_skeleton_key_use = function(...) print(...) end, + tube = { + input_inventory = "main", + }, +}) + +local function set_injector_formspec(pos) + local meta = minetest.get_meta(pos) + local formspec = "fs_helpers_cycling:1:splitstacks" + formspec = formspec.."button[0,1;4,1;mode_stack;"..S("Itemwise").."]" + formspec = formspec.."button[4,1;4,1;enable;"..S("Disabled").."]" + meta:set_string("formspec", formspec) +end + +minetest.register_node("technic:injector", { + description = "Self-Contained Injector", + groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2, tubedevice=1, tubedevice_receiver=1}, + tube = { + can_insert = function(...)end, + insert_object = function(...)end, + connect_sides = {left=1, right=1, back=1, top=1, bottom=1}, + }, + on_construct = function(pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Self-Contained Injector") + meta:set_string("mode", "single items") + --meta:get_inventory():set_size("main", 16) + --minetest.get_node_timer(pos):start(1) + set_injector_formspec(pos) + end, + on_receive_fields = function(...) print(...) end, +}) diff --git a/containertool/spec/fixtures/technic.lua b/containertool/spec/fixtures/technic.lua new file mode 100644 index 0000000..df1a05f --- /dev/null +++ b/containertool/spec/fixtures/technic.lua @@ -0,0 +1,35 @@ + +mineunit:set_modpath("technic", "spec/fixtures") + +technic = {} +technic.chests = {} +technic.chests.colors = { + {"black", S("Black")}, + {"blue", S("Blue")}, + {"brown", S("Brown")}, + {"cyan", S("Cyan")}, + {"dark_green", S("Dark Green")}, + {"dark_grey", S("Dark Grey")}, + {"green", S("Green")}, + {"grey", S("Grey")}, + {"magenta", S("Magenta")}, + {"orange", S("Orange")}, + {"pink", S("Pink")}, + {"red", S("Red")}, + {"violet", S("Violet")}, + {"white", S("White")}, + {"yellow", S("Yellow")}, +} + +function technic.chests.change_allowed(pos, player, owned, protected) + if owned then + if minetest.is_player(player) and not default.can_interact_with_node(player, pos) then + return false + end + elseif protected then + if minetest.is_protected(pos, player:get_player_name()) then + return false + end + end + return true +end diff --git a/containertool/spec/tool_spec.lua b/containertool/spec/tool_spec.lua new file mode 100644 index 0000000..b1029ba --- /dev/null +++ b/containertool/spec/tool_spec.lua @@ -0,0 +1,376 @@ +--[[ + Regression tests for container tool +--]] +dofile("../spec/mineunit/init.lua") + +mineunit:set_modpath("containertool", ".") + +mineunit("core") +mineunit("player") +mineunit("protection") +mineunit("default/functions") + +_G.S = _G.S or function(s) return s end + +fixture("metatool") +fixture("technic") +fixture("nodes") + +sourcefile("../metatool/init") +sourcefile("init") + +mineunit:mods_loaded() + +local TOOL_NAME = "metatool:containertool" + +local P = { + protected_chest = {x=0, y=0, z=0}, + unprotected_chest = {x=0, y=1, z=0}, + protected_injector = {x=0, y=2, z=0}, + unprotected_injector = {x=0, y=3, z=0}, + owned_chest = {x=0, y=4, z=0}, +} + +world.layout({ + {P.protected_chest, "technic:test_chest"}, + {P.unprotected_chest, "technic:test_chest"}, + {P.protected_injector, "technic:injector"}, + {P.unprotected_injector, "technic:injector"}, + {P.owned_chest, "technic:test_chest"}, +}) +mineunit:protect(P.protected_chest, "dummy") +mineunit:protect(P.protected_injector, "dummy") + +local player = Player("SX", {interact=1}) +local player2 = Player("dummy", {interact=1}) + +local function get_pointed_thing(pos) + return { + type = "node", + above = {x=pos.x,y=pos.y+1,z=pos.z}, -- Pointing from above to downwards, + under = {x=pos.x,y=pos.y,z=pos.z}, -- crosshair at protected node surface + } +end + +local function get_tool_itemstack(name, data) + local stack = ItemStack(name) + if data then + metatool.write_data(stack, data) + end + return stack +end + +describe("Tool behavior", function() + + local tooldata + before_each(function() + local worldmeta_node0 = minetest.get_meta(P.protected_chest) + worldmeta_node0:set_string("owner", "dummy") + worldmeta_node0:set_string("mineunit_test_meta", "not changed 0") + worldmeta_node0:set_string("key_lock_secret", "key_lock_secret 0") + worldmeta_node0:set_int("splitstacks", 100) + local worldmeta_node1 = minetest.get_meta(P.unprotected_chest) + worldmeta_node1:set_string("owner", "SX") + worldmeta_node1:set_string("mineunit_test_meta", "not changed 1") + worldmeta_node1:set_string("key_lock_secret", "key_lock_secret 1") + worldmeta_node1:set_int("splitstacks", 101) + local worldmeta_node2 = minetest.get_meta(P.protected_injector) + worldmeta_node2:set_string("owner", "dummy") + worldmeta_node2:set_string("mineunit_test_meta", "not changed 2") + worldmeta_node2:set_int("splitstacks", 102) + local worldmeta_node3 = minetest.get_meta(P.unprotected_injector) + worldmeta_node3:set_string("owner", "SX") + worldmeta_node3:set_string("mineunit_test_meta", "not changed 3") + worldmeta_node3:set_int("splitstacks", 103) + local worldmeta_node4 = minetest.get_meta(P.owned_chest) + worldmeta_node4:set_string("owner", "dummy") + worldmeta_node4:set_string("mineunit_test_meta", "not changed 4") + worldmeta_node4:set_string("key_lock_secret", "key_lock_secret 4") + worldmeta_node4:set_int("splitstacks", 104) + tooldata = { + data = { + key_lock_secret = "test value", + splitstacks = 42 + }, + group="container" + } + end) + + describe("node write operation", function() + + it("protects nodes from write", function() + local tool_stack = get_tool_itemstack(TOOL_NAME, tooldata) + local count = tool_stack:get_count() + local target = table.copy(P.protected_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to write metadata + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + + -- Verify that returned stack is not modified + assert.equals(true, return_stack == nil or (return_stack == tool_stack and count == return_stack:get_count())) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 0", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 0", worldmeta:get("key_lock_secret")) + assert.equals(100, worldmeta:get_int("splitstacks")) + end) + + it("writes unprotected nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME, tooldata) + local count = tool_stack:get_count() + local target = table.copy(P.unprotected_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to write metadata + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + + -- Verify that returned stack is not modified + assert.equals(true, return_stack == nil or (return_stack == tool_stack and count == return_stack:get_count())) + local meta = tool_stack:get_meta() + assert.not_nil(meta) + + -- Check if world metadata was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 1", worldmeta:get("mineunit_test_meta")) + assert.equals("test value", worldmeta:get("key_lock_secret")) + assert.equals(42, worldmeta:get_int("splitstacks")) + end) + + it("protects owned nodes from write", function() + local tool_stack = get_tool_itemstack(TOOL_NAME, tooldata) + local count = tool_stack:get_count() + local target = table.copy(P.owned_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to write metadata + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + + -- Verify that returned stack is not modified + assert.equals(true, return_stack == nil or (return_stack == tool_stack and count == return_stack:get_count())) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 4", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 4", worldmeta:get("key_lock_secret")) + assert.equals(104, worldmeta:get_int("splitstacks")) + end) + + it("writes owned nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME, tooldata) + local count = tool_stack:get_count() + local target = table.copy(P.owned_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to write metadata + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player2, pointed_thing) + + -- Verify that returned stack is not modified + assert.equals(true, return_stack == nil or (return_stack == tool_stack and count == return_stack:get_count())) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 4", worldmeta:get("mineunit_test_meta")) + assert.equals("test value", worldmeta:get("key_lock_secret")) + assert.equals(42, worldmeta:get_int("splitstacks")) + end) + + end) + + describe("node read operation", function() + + it("reads protected nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.protected_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Technic chest at (0,0,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + -- Protected value + assert.is_nil(data.data.key_lock_secret) + -- Unprotected values + assert.equals("dummy", data.data.owner) + assert.equals(100, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 0", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 0", worldmeta:get("key_lock_secret")) + assert.equals(100, worldmeta:get_int("splitstacks")) + end) + + it("reads protected common_defaults nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.protected_injector) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Self-Contained Injector at (0,2,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + -- Protected value + assert.is_nil(data.data.key_lock_secret) + -- Unprotected values + assert.equals("dummy", data.data.owner) + assert.equals(102, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 2", worldmeta:get("mineunit_test_meta")) + assert.is_nil(worldmeta:get("key_lock_secret")) + assert.equals(102, worldmeta:get_int("splitstacks")) + end) + + it("reads unprotected nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.unprotected_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Technic chest at (0,1,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + assert.equals("key_lock_secret 1", data.data.key_lock_secret) + assert.equals(101, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 1", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 1", worldmeta:get("key_lock_secret")) + assert.equals(101, worldmeta:get_int("splitstacks")) + end) + + it("reads unprotected common_defaults nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.unprotected_injector) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Self-Contained Injector at (0,3,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + assert.is_nil(data.data.key_lock_secret) + assert.equals(103, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 3", worldmeta:get("mineunit_test_meta")) + assert.is_nil(worldmeta:get("key_lock_secret")) + assert.equals(103, worldmeta:get_int("splitstacks")) + end) + + it("reads owned nodes protecting private data", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.owned_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Technic chest at (0,4,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + -- Protected value + assert.is_nil(data.data.key_lock_secret) + -- Unprotected values + assert.equals("dummy", data.data.owner) + assert.equals(104, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 4", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 4", worldmeta:get("key_lock_secret")) + assert.equals(104, worldmeta:get_int("splitstacks")) + end) + + it("reads owned nodes", function() + local tool_stack = get_tool_itemstack(TOOL_NAME) + local target = table.copy(P.owned_chest) + local pointed_thing = get_pointed_thing(target) + + -- Use tool to copy metadata from pointed node + player2:_set_player_control_state("aux1", true) + local return_stack = metatool:on_use(TOOL_NAME, tool_stack, player2, pointed_thing) + player2:_reset_player_controls() + + -- Check returned tool stack + assert.not_nil(return_stack) + assert.equals("Technic chest at (0,4,0)", return_stack:get_description()) + + -- Check tool data + local data = return_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + -- Unprotected values + assert.equals("key_lock_secret 4", data.data.key_lock_secret) + assert.equals("dummy", data.data.owner) + assert.equals(104, data.data.splitstacks) + + -- Check if world metada was written + local worldmeta = minetest.get_meta(target) + assert.equals("not changed 4", worldmeta:get("mineunit_test_meta")) + assert.equals("key_lock_secret 4", worldmeta:get("key_lock_secret")) + assert.equals(104, worldmeta:get_int("splitstacks")) + end) + + end) + +end) diff --git a/containertool/textures/containertool.png b/containertool/textures/containertool.png new file mode 100644 index 0000000..35a44f5 Binary files /dev/null and b/containertool/textures/containertool.png differ diff --git a/metatool/init.lua b/metatool/init.lua index c106dd3..c31a459 100644 --- a/metatool/init.lua +++ b/metatool/init.lua @@ -14,7 +14,7 @@ -- initialize namespace and core functions metatool = { configuration_file = minetest.get_worldpath() .. '/metatool.cfg', - export_default_config = minetest.settings:get_bool("metatool_export_default_config", false), + export_default_config = minetest.settings:get_bool("metatool_export_default_config", true), modpath = minetest.get_modpath('metatool'), S = string.format } diff --git a/metatool/settings.lua b/metatool/settings.lua index 3234d85..39e441f 100644 --- a/metatool/settings.lua +++ b/metatool/settings.lua @@ -110,14 +110,18 @@ local update_setting = function(target, name, key, value) if target[key] == nil then -- If key is not set use provided value and export it if asked to target[key] = value + -- Export default configuration to settings file + if metatool.export_default_config then + if type(value) == "boolean" then + settings:set_bool(makekey(name, key), value) + else + settings:set(makekey(name, key), value) + end + end else -- If key is set convert configuration value to type of default setting target[key] = convert(target[key], type(value)) end - -- Export default configuration to settings file - if metatool.export_default_config then - settings:set(makekey(name, key), value) - end end local node_specials = { "protection_bypass_info", "protection_bypass_read", "protection_bypass_write" } diff --git a/metatool/spec/api_spec.lua b/metatool/spec/api_spec.lua index 3faf9cc..af2f61e 100644 --- a/metatool/spec/api_spec.lua +++ b/metatool/spec/api_spec.lua @@ -20,25 +20,29 @@ sourcefile("settings") sourcefile("command") sourcefile("api") +local UnprotectedPos = {x=-123,y=-123,z=-123} +local ProtectedPos = {x=123,y=123,z=123} +mineunit:protect(ProtectedPos, "dummy") + describe("Metatool API protection", function() it("metatool.is_protected bypass privileges", function() - local value = metatool.is_protected(ProtectedPos(), Player(), "test_priv", true) + local value = metatool.is_protected(ProtectedPos, Player(), "test_priv", true) assert.equals(false, value) end) it("metatool.is_protected no bypass privileges", function() - local value = metatool.is_protected(ProtectedPos(), Player(), "test_priv2", true) + local value = metatool.is_protected(ProtectedPos, Player(), "test_priv2", true) assert.equals(true, value) end) it("metatool.is_protected bypass privileges, unprotected", function() - local value = metatool.is_protected(UnprotectedPos(), Player(), "test_priv", true) + local value = metatool.is_protected(UnprotectedPos, Player(), "test_priv", true) assert.equals(false, value) end) it("metatool.is_protected no bypass privileges, unprotected", function() - local value = metatool.is_protected(UnprotectedPos(), Player(), "test_priv2", true) + local value = metatool.is_protected(UnprotectedPos, Player(), "test_priv2", true) assert.equals(false, value) end) @@ -227,14 +231,20 @@ describe("Metatool API node registration", function() "testnode2", "nonexistent2", }, - group = 'test node', + group = 'test node 2', protection_bypass_write = "default_bypass_write_priv", } function definition:copy(node, pos, player) - print("nodedef copy callback executed") + print("testnode2 copy callback executed") + local meta = minetest.get_meta(pos) + local value = meta:get_string("test") + --assert.equals("node2meta", value) + return { description = "after copy description", testvalue = value } end function definition:paste(node, pos, player, data) - print("nodedef paste callback executed") + print("testnode2 paste callback executed") + local meta = minetest.get_meta(pos) + meta:set_string("test", data.testvalue) end tool:load_node_definition(definition) @@ -275,7 +285,7 @@ describe("Metatool API node registration", function() local definition = { name = 'testnode3', nodes = "testnode3", - group = 'test node', + group = 'test node 3', protection_bypass_read = "default_bypass_read_priv", settings = { allow_doing_x = true, @@ -288,10 +298,14 @@ describe("Metatool API node registration", function() }, } function definition:copy(node, pos, player) - print("nodedef copy callback executed") + print("testnode3 copy callback executed") + local meta = minetest.get_meta(pos) + return { description = "after copy description", testvalue = meta:get("test") } end function definition:paste(node, pos, player, data) - print("nodedef paste callback executed") + print("testnode3 paste callback executed") + local meta = minetest.get_meta(pos) + meta:set_string("test", data.testvalue) end tool:load_node_definition(definition) @@ -337,11 +351,16 @@ describe("Tool behavior", function() {{x=123,y=123,z=123}, "testnode1"}, {{x=123,y=124,z=123}, "testnode1"}, }) + local worldmeta_node1 = minetest.get_meta({x=123,y=123,z=123}) + local worldmeta_node2 = minetest.get_meta({x=123,y=124,z=123}) + local player = Player("SX", {server=1,test_testtool2_privs=1,test_priv=1}) describe("node write operation", function() it("protects nodes from write", function() + worldmeta_node1:set_string("test", "node1meta") + worldmeta_node2:set_string("test", "node2meta") local use_stack = ItemStack("metatool:testtool2") local count = use_stack:get_count() local pointed_thing = { @@ -355,14 +374,48 @@ describe("Tool behavior", function() end) it("writes unprotected nodes", function() + worldmeta_node1:set_string("test", "node1meta") + worldmeta_node2:set_string("test", "node2meta") + local use_stack = ItemStack("metatool:testtool2") + metatool.write_data(use_stack,{data={testvalue="write test"},group="test node"}) + local pointed_thing = { + type = "node", + above = {x=123,y=125,z=123}, -- Pointing from above to downwards, + under = {x=123,y=124,z=123}, -- crosshair at protected node surface + } + local out_stack = metatool:on_use("metatool:testtool2", use_stack, player, pointed_thing) + -- Verify that returned stack is not modified + assert.equals(true, out_stack == nil or (out_stack == use_stack and count == out_stack:get_count())) + local meta = use_stack:get_meta() + assert.not_nil(meta) + local worldmeta = minetest.get_meta({x=123,y=124,z=123}) + assert.equals("write test", worldmeta:get("test")) + end) + + it("reads unprotected nodes", function() + worldmeta_node1:set_string("test", "node1meta") + worldmeta_node2:set_string("test", "node2meta") local use_stack = ItemStack("metatool:testtool2") - metatool.write_data(use_stack,{data={},group="test node"}) local pointed_thing = { type = "node", above = {x=123,y=125,z=123}, -- Pointing from above to downwards, under = {x=123,y=124,z=123}, -- crosshair at protected node surface } + + -- Use tool to copy metadata from pointed node + player:_set_player_control_state("aux1", true) local out_stack = metatool:on_use("metatool:testtool2", use_stack, player, pointed_thing) + player:_reset_player_controls() + + -- Check results + assert.not_nil(out_stack) + assert.equals("on_read_node description", out_stack:get_description("description")) + local data = out_stack:get_meta():get("data") + assert.is_string(data) + data = minetest.deserialize(data) + assert.is_table(data) + assert.is_table(data.data) + assert.equals("node2meta", data.data.testvalue) -- TODO: Check if data was written, currently this only verifies no crash end) diff --git a/spec/fixtures/metatool.lua b/spec/fixtures/metatool.lua index 4c9dc42..003fc0d 100644 --- a/spec/fixtures/metatool.lua +++ b/spec/fixtures/metatool.lua @@ -1,4 +1,6 @@ +mineunit:set_modpath("metatool", "../metatool") + _G.metatool = {} _G.metatool.S = string.format _G.metatool.configuration_file = fixture_path("metatool.cfg") diff --git a/spec/mineunit b/spec/mineunit index e3c28ae..f03b7e7 160000 --- a/spec/mineunit +++ b/spec/mineunit @@ -1 +1 @@ -Subproject commit e3c28ae0a9440980fb78ddcbf603a4023d0a325f +Subproject commit f03b7e7585d64617776ed8921cc25c4517df1015