connected_chests/init.lua
HybridDog 044a91fcd5 Do not crash with tenplus1's hopper mod
There are multiple hopper mods and not all of them support the set_extra_container_info function,
so the mod crashes now if it is used together with an incompatible hopper mod.

To prevent the crash, execute the code for hopper compatibility if hopper and hopper.set_extra_container_info exist and not if a mod named hopper is installed.
2024-08-25 09:48:03 +02:00

718 lines
21 KiB
Lua

-- for translation
local S = minetest.get_translator("connected_chests")
-- param_tab maps the x and z offset to a param2 value
local param_tab = {
["-1 0"] = 0,
["1 0"] = 2,
["0 -1"] = 3,
["0 1"] = 1,
}
-- param_tab2 maps the other way round
local param_tab2 = {}
for n,i in pairs(param_tab) do
param_tab2[i] = n:split" "
end
local function return_remove_next(allowed_name, add_open)
local function remove_next(pos, oldnode)
-- if the left node had an unexpected rotation, the right one can't be
-- found, in this case simply do nothing
if oldnode.param2 > 3 then
return
end
-- remove the right one if there is one
-- (the left one is already removed)
local x, z = unpack(param_tab2[oldnode.param2])
pos.x = pos.x-x
pos.z = pos.z-z
local right_n = minetest.get_node(pos).name
if right_n == allowed_name
or (add_open and right_n == allowed_name .. "_open") then
minetest.remove_node(pos)
end
end
return remove_next
end
-- used when constructing the left node
local function return_add_next(right_name)
local function add_next(pos, node)
node = node or minetest.get_node(pos)
local par = node.param2
-- if the left node is set with an unexpected rotation, put the chest
-- with default rotation
if par > 3 then
minetest.log("action",
node.name .. " with invalid param2 found, pos: " ..
minetest.pos_to_string(pos) .. ", param2: " .. par)
node.param2 = 0
minetest.set_node(pos, node)
return
end
-- put the right chest if possible
local x, z = unpack(param_tab2[par])
pos.x = pos.x-x
pos.z = pos.z-z
if minetest.get_node(pos).name == "air" then
minetest.set_node(pos, {name=right_name, param2=par})
end
end
return add_next
end
-- gives information about the positions and param to place the nodes
local function get_pointed_info(pt, name)
if not pt then
return
end
local pu = minetest.get_pointed_thing_position(pt)
local pa = minetest.get_pointed_thing_position(pt, true)
if not pu
or not pa
or pu.y ~= pa.y then
return
end
local nd_u = minetest.get_node(pu)
if nd_u.name ~= name then
return
end
return pu, pa, nd_u.param2
end
local pars = {[0]=2, 3, 0, 1}
local chestdata = {}
-- executed when connecting the chests
local function connect_chests(pu, pa, old_param2, data)
local metatable = minetest.get_meta(pu):to_table()
local par = param_tab[pu.x-pa.x.." "..pu.z-pa.z]
local par_inverted = pars[par]
if old_param2 == par_inverted then
pu, pa = pa, pu
par = par_inverted
end
data.on_connect(pu, pa, par, metatable)
end
local tube_to_left, tube_to_left_locked, tube_update, tube_groups
if minetest.global_exists"pipeworks" then
tube_to_left_locked = {
insert_object = function(pos, node, stack)
local x, z = unpack(param_tab2[node.param2])
return minetest.get_meta{x=pos.x+x, y=pos.y, z=pos.z+z
}:get_inventory():add_item("main", stack)
end,
can_insert = function(pos, node, stack)
local x, z = unpack(param_tab2[node.param2])
return minetest.get_meta{x=pos.x+x, y=pos.y, z=pos.z+z
}:get_inventory():room_for_item("main", stack)
end,
connect_sides = {right = 1, back = 1, front = 1, bottom = 1, top = 1}
}
tube_to_left = table.copy(tube_to_left_locked)
tube_to_left.input_inventory = "main"
tube_update = pipeworks.scan_for_tube_objects
tube_groups = {tubedevice=1, tubedevice_receiver=1}
else
function tube_update() end
end
connected_chests = {chestdata = chestdata}
--[[
connected_chests.register_chest(<original_node>, {
description = <string>, -- The name of the connected chest as shown to the
-- player, i.e. the node metadata infotext
get_formspec = function(metatable, pos)
return <formspec_of_big>
end,
lock = true, -- indicates whether a lock should be added to the texture
-- and has an impact on the tube function
front = <keyhole_texture>, -- if present, this texture is added to the chest
-- front
on_rightclick = <func>, -- sets an on_rightclick (some chests need this)
})
]]
function connected_chests.register_chest(fromname, data)
chestdata[fromname] = data
--~ local mod, name = fromname:split":"
local name_left = fromname .. "_connected_left"
local name_right = fromname .. "_connected_right"
data.left = name_left
data.right = name_right
local description = data.description
if not description then
minetest.log("deprecated",
"Missing connected chest description for " .. fromname)
description = "Big " .. minetest.registered_nodes[fromname].description
end
-- executed when connecting the chest
data.on_connect = function(pu, pa, par, metatable)
minetest.add_node(pu, {name=name_left, param2=par})
minetest.add_node(pa, {name=name_right, param2=par})
if not data.add_open_chest then
metatable.fields.formspec = data.get_formspec(metatable, pu)
end
metatable.fields.infotext = description
local meta = minetest.get_meta(pu)
meta:from_table(metatable)
local inv = meta:get_inventory()
inv:set_size("main", 65)
end
-- override the original node to support connecting
local place_chest = minetest.registered_nodes[fromname].on_place
local creative_mode = minetest.settings:get_bool"creative_mode"
minetest.override_item(fromname, {
on_place = function(itemstack, placer, pointed_thing)
if not placer
or not placer:get_player_control().sneak then
return place_chest(itemstack, placer, pointed_thing)
end
local pu, pa, par2 = get_pointed_info(pointed_thing, fromname)
if not pu then
return place_chest(itemstack, placer, pointed_thing)
end
if minetest.is_protected(pa, placer:get_player_name()) then
return
end
connect_chests(pu, pa, par2, data)
if not creative_mode then
itemstack:take_item()
return itemstack
end
end
})
-- Adds the big chest nodes
-- the left one contains inventory
local chest = {}
local origdef = minetest.registered_nodes[fromname]
for i in pairs(origdef) do
chest[i] = rawget(origdef, i)
end
local top = chest.tiles[1]
local side = chest.tiles[4]
local top_texture = top .. "^([combine:16x16:5,0=" .. top ..
"^connected_chests_frame.png^[makealpha:255,126,126)"
local side_texture = side .. "^([combine:16x16:5,0=" .. side ..
"^connected_chests_frame.png^[makealpha:255,126,126)"
local inside_texture
chest.description = description
chest.groups = table.copy(chest.groups)
chest.groups.not_in_creative_inventory = 1
chest.legacy_facedir_simple = nil
chest.after_place_node = nil
chest.on_receive_fields = nil
if data.on_rightclick then
chest.on_rightclick = data.on_rightclick
end
-- disallow rotating a connected chest using a screwdriver
function chest.on_rotate()
return false
end
-- copy pipeworks tube data (if requisite)
if chest.tube then
chest.tube = table.copy(chest.tube)
chest.tube.connect_sides = {left = 1, -- no connection to the right.
back = 1, front = 1, bottom = 1, top = 1}
end
if not data.front then
data.front = "connected_chests_front.png"
if data.lock then
data.front = data.front .. "^connected_chests_lock.png"
end
end
chest.tiles = {top_texture, top_texture, "default_obsidian_glass.png",
side, side_texture.."^[transformFX", side_texture.."^" .. data.front}
chest.drop = (chest.drop or fromname) .. " 2"
chest.selection_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.5, 1.5, 0.5, 0.5},
},
}
chest.on_construct = return_add_next(name_right)
chest.after_destruct = return_remove_next(name_right, data.add_open_chest)
if data.add_open_chest then
-- mostly copied from default
local def_opened = table.copy(chest)
def_opened.mesh = "connected_chest_open.obj"
def_opened.drawtype = "mesh"
def_opened.paramtype = "light"
for i = 1, #def_opened.tiles do
if type(def_opened.tiles[i]) == "string" then
def_opened.tiles[i] =
{name = def_opened.tiles[i], backface_culling = true}
elseif def_opened.tiles[i].backface_culling == nil then
def_opened.tiles[i].backface_culling = true
end
end
def_opened.selection_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 1.5, 3/16, 0.5},
}
def_opened.diggable = false
def_opened.on_blast = function() end
inside_texture = "default_chest_inside.png^([combine:16x32:5,0=" ..
"default_chest_inside.png^connected_chests_inside_frame.png^[" ..
"makealpha:255,126,126)"
-- TODO, see right chest
-- here 3 and 4 are swapped and no mirroring is needed…
def_opened.tiles[3] = def_opened.tiles[4]
def_opened.tiles[3].name = def_opened.tiles[3].name
def_opened.tiles[5] = def_opened.tiles[6]
def_opened.tiles[6] = inside_texture
minetest.register_node(":" .. name_left.. "_open", def_opened)
end
--~ minetest.register_node("connected_chests:chest_left", chest)
minetest.register_node(":" .. name_left, chest)
-- the right one is the deco one
local tiles = {top_texture.."^[transformFX", top_texture.."^[transformFX",
side, "default_obsidian_glass.png", side_texture, side_texture
.. "^" .. data.front .. "^[transformFX"}
local right_def = {
tiles = tiles,
paramtype2 = "facedir",
drop = "",
pointable = false,
diggable = false,
on_construct = function(pos)
local node = minetest.get_node(pos)
-- if the right node has an unexpected rotation, try to set it with
-- a valid one
if node.param2 > 3 then
node.param2 = node.param2 % 4
minetest.set_node(pos, node)
return
end
-- remove it if the left node can't be found
local x, z = unpack(param_tab2[node.param2])
local node_left = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
if node_left.name ~= name_left
or node_left.param2 ~= node.param2 then
minetest.remove_node(pos)
return
end
-- connect pipework tubes if there are any
tube_update(pos)
end,
after_destruct = function(pos, oldnode)
-- simply remove the right node if it has an unexpected rotation
if oldnode.param2 > 3 then
return
end
-- add it back if the left node is still there
local x, z = unpack(param_tab2[oldnode.param2])
local node_left = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
if node_left.name == name_left
and node_left.param2 == oldnode.param2
and minetest.get_node(pos).name == "air" then
minetest.set_node(pos, oldnode)
return
end
-- disconnect pipework tubes if there are any
tube_update(pos)
end,
tube = data.lock and tube_to_left_locked or tube_to_left,
groups = tube_groups,
}
if data.add_open_chest then
local def_opened = table.copy(right_def)
def_opened.mesh = "connected_chest_open.obj"
def_opened.drawtype = "mesh"
def_opened.paramtype = "light"
for i = 1, #def_opened.tiles do
if type(def_opened.tiles[i]) == "string" then
def_opened.tiles[i] =
{name = def_opened.tiles[i], backface_culling = true}
elseif def_opened.tiles[i].backface_culling == nil then
def_opened.tiles[i].backface_culling = true
end
end
-- fix right side, somehow
def_opened.tiles[4] = def_opened.tiles[3]
def_opened.tiles[4].name = def_opened.tiles[4].name .. "^[transformFX"
-- fix front side
def_opened.tiles[5] = def_opened.tiles[6]
-- add inside
def_opened.tiles[6] = inside_texture .. "^[transformFX"
-- TODO: back side looks like right side
minetest.register_node(":" .. name_right .. "_open", def_opened)
end
minetest.register_node(":" .. name_right, right_def)
-- LBMs to fix half chests if they occur for some reason
minetest.register_lbm{
label = "Connected Chest fixer " .. name_right,
name = ":" .. name_right .. "_reconnect_lbm",
nodenames = {name_right},
run_at_every_load = true,
action = function(pos, node)
if node.param2 > 3 then
node.param2 = node.param2%4
minetest.set_node(pos, node)
return
end
local x, z = unpack(param_tab2[node.param2])
local left_node = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
if left_node.name ~= name_left
or left_node.param2 ~= node.param2 then
minetest.remove_node(pos)
end
end,
}
minetest.register_lbm{
label = "Connected Chest fixer " .. name_left,
name = ":" .. name_left .. "_reconnect_lbm",
nodenames = {name_left},
run_at_every_load = true,
action = return_add_next(name_right),
}
end
local function get_chest_formspec(pos)
local spos = pos.x .. "," .. pos.y .. "," .. pos.z
local formspec =
"size[13,9]" ..
"list[nodemeta:" .. spos .. ";main;0,0;13,5;]" ..
"list[current_player;main;2.5,5.2;8,4;]" ..
"listring[nodemeta:" .. spos .. ";main]" ..
"listring[current_player;main]"
return formspec
end
local open_chests = {} -- counter for players viewing the chest
local accessed_chests = {} -- position of the chest a player views
-- a hacky way to close open connected default chests
-- vi: vector index of the position of the left chest
local function close_chest(vi)
local pos = minetest.get_position_from_hash(vi)
local node = minetest.get_node(pos)
local is_locked = node.name == "default:chest_locked_connected_left_open"
if node.name ~= "default:chest_connected_left_open"
and not is_locked then
return
end
if is_locked then
node.name = "default:chest_locked_connected_left"
else
node.name = "default:chest_connected_left"
end
minetest.swap_node(pos, node)
-- close the right chest
-- TODO: test for valid rotation
local x, z = unpack(param_tab2[node.param2])
pos.x = pos.x-x
pos.z = pos.z-z
node = minetest.get_node(pos)
if is_locked then
if node.name == "default:chest_locked_connected_right_open" then
node.name = "default:chest_locked_connected_right"
minetest.swap_node(pos, node)
end
else
if node.name == "default:chest_connected_right_open" then
node.name = "default:chest_connected_right"
minetest.swap_node(pos, node)
end
end
pos.x = pos.x + x * 0.5
pos.z = pos.z + z * 0.5
minetest.sound_play("default_chest_close",
{gain = 0.3, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)
end
-- close all remaining open chest on shutdown
minetest.register_on_shutdown(function()
for vi in pairs(open_chests) do
close_chest(vi)
end
open_chests = nil
end)
-- close open chests when the last player exits formspec
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "default:chest_connected"
and formname ~= "default:chest_locked_connected" then
return
end
if not player
or not fields.quit then
return
end
local pn = player:get_player_name()
local vi = accessed_chests[pn]
if not vi then
minetest.log("warning", pn .. " opened a chest without lid?")
return
end
accessed_chests[pn] = nil
local cnt = open_chests[vi]
if cnt == 1 then
close_chest(vi)
open_chests[vi] = nil
else
open_chests[vi] = cnt-1
end
return true
end)
local chest_lid_obstructed = default.chest
and default.chest.chest_lid_obstructed
connected_chests.register_chest("default:chest", {
description = S("Big Chest"),
add_open_chest = true,
on_rightclick = function(pos, _, player)
minetest.sound_play("default_chest_open",
{gain = 0.3, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)
local vi = minetest.hash_node_position(pos)
if not open_chests[vi]
and not chest_lid_obstructed(pos) then
local left_param2 = minetest.get_node(pos).param2
-- TODO: test for invalid param2 values
local x, z = unpack(param_tab2[left_param2])
local pos_right = {x=pos.x-x, y=pos.y, z=pos.z-z}
local node = minetest.get_node(pos_right)
if node.name == "default:chest_connected_right"
and not chest_lid_obstructed(pos_right) then
minetest.swap_node(pos, {
name = "default:chest_connected_left_open",
param2 = left_param2})
minetest.swap_node(pos_right, {
name = "default:chest_connected_right_open",
param2 = node.param2})
end
end
local pname = player:get_player_name()
local spec = get_chest_formspec(pos)
minetest.after(0.2, minetest.show_formspec, pname,
"default:chest_connected", spec)
if not accessed_chests[pname] then
open_chests[vi] = open_chests[vi] or 0
open_chests[vi] = open_chests[vi]+1
accessed_chests[pname] = vi
end
end
})
connected_chests.register_chest("default:chest_locked", {
description = S("Big Locked Chest"),
lock = true,
add_open_chest = true,
on_rightclick = function(pos, _, player)
if not default.can_interact_with_node(player, pos) then
minetest.sound_play("default_chest_locked", {pos = pos}, true)
return
end
minetest.sound_play("default_chest_open",
{gain = 0.32, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)
local vi = minetest.hash_node_position(pos)
-- TODO: somehow avoid using the chest node names here
if not open_chests[vi]
and not chest_lid_obstructed(pos) then
local left_param2 = minetest.get_node(pos).param2
-- TODO: test for invalid param2 values
local x, z = unpack(param_tab2[left_param2])
local pos_right = {x=pos.x-x, y=pos.y, z=pos.z-z}
local node = minetest.get_node(pos_right)
if node.name == "default:chest_locked_connected_right"
and not chest_lid_obstructed(pos_right) then
minetest.swap_node(pos_right, {
name = "default:chest_locked_connected_right_open",
param2 = node.param2})
minetest.swap_node(pos, {
name = "default:chest_locked_connected_left_open",
param2 = left_param2})
end
end
local pname = player:get_player_name()
local spec = get_chest_formspec(pos)
minetest.after(0.2, minetest.show_formspec, pname,
"default:chest_locked_connected", spec)
if not accessed_chests[pname] then
open_chests[vi] = open_chests[vi] or 0
open_chests[vi] = open_chests[vi]+1
accessed_chests[pname] = vi
end
end
})
-- legacy
-- the default chest lid obstruction function wasn't exposed in minetest 0.4.16
if not chest_lid_obstructed then
-- copied from default's nodes.lua
function chest_lid_obstructed(pos)
local above = {x = pos.x, y = pos.y + 1, z = pos.z}
local def = minetest.registered_nodes[minetest.get_node(above).name]
-- allow ladders, signs, wallmounted things and torches to not obstruct
if def and
(def.drawtype == "airlike" or
def.drawtype == "signlike" or
def.drawtype == "torchlike" or
(def.drawtype == "nodebox"
and def.paramtype2 == "wallmounted")) then
return false
end
return true
end
end
-- once the connected_chests mod supported only default chests and used
-- different node names
minetest.register_alias("connected_chests:chest_left",
"default:chest_connected_left")
minetest.register_alias("connected_chests:chest_right",
"default:chest_connected_right")
minetest.register_alias("connected_chests:chest_left_locked",
"default:chest_locked_connected_left")
minetest.register_alias("connected_chests:chest_right_locked",
"default:chest_locked_connected_right")
minetest.register_alias("connected_chests:chest_locked_left",
"default:chest_locked_connected_left")
minetest.register_alias("connected_chests:chest_locked_right",
"default:chest_locked_connected_right")
if minetest.global_exists("hopper") and hopper.set_extra_container_info then
local function get_inventory(chest_right_pos)
local node_right = minetest.get_node(chest_right_pos)
if node_right.param2 > 3 then
-- The right connected chest node has an invalid param2 value
-- Cannot determine the inventory
return
end
local x, z = unpack(param_tab2[node_right.param2])
local chest_left_pos = {x=chest_right_pos.x+x, y=chest_right_pos.y,
z=chest_right_pos.z+z}
local node_left = minetest.get_node(chest_left_pos)
if node_left.name ~= "default:chest_connected_left"
and node_left.name ~= "default:chest_connected_left_open" then
minetest.log("error","The left chest is not a chest: " ..
node_left.name .. " at " .. vector.to_string(chest_left_pos))
return
end
if node_left.param2 ~= node_right.param2 then
minetest.log("error", "The chests are pointing in different " ..
"directions: node_left.param2:" .. node_left.param2 ..
", node_right.param2:" .. node_right.param2)
return
end
return minetest.get_meta(chest_left_pos):get_inventory()
end
hopper:add_container({
{"top", "default:chest_connected_left", "main"},
{"bottom", "default:chest_connected_left", "main"},
{"side", "default:chest_connected_left", "main"},
{"top", "default:chest_connected_right", "main",
get_inventory = get_inventory},
{"bottom", "default:chest_connected_right", "main",
get_inventory = get_inventory},
{"side", "default:chest_connected_right", "main",
get_inventory = get_inventory},
{"top", "default:chest_connected_left_open", "main"},
{"bottom", "default:chest_connected_left_open", "main"},
{"side", "default:chest_connected_left_open", "main"},
{"top", "default:chest_connected_right_open", "main",
get_inventory = get_inventory},
{"bottom", "default:chest_connected_right_open", "main",
get_inventory = get_inventory},
{"side", "default:chest_connected_right_open", "main",
get_inventory = get_inventory},
})
local function set_hopper_param2(hopper_pos, chest_left_pos)
local param2_by_offset = {
[vector.new(-1, 0, 0):to_string()] = 0,
[vector.new( 0, 0, 1):to_string()] = 1,
[vector.new( 1, 0, 0):to_string()] = 2,
[vector.new( 0, 0,-1):to_string()] = 3,
}
local hopper_param2 = param2_by_offset[
(chest_left_pos - hopper_pos):to_string()]
if hopper_param2 then
return hopper_param2
end
local x, z = unpack(
param_tab2[minetest.get_node(chest_left_pos).param2])
local chest_right_pos = {x=chest_left_pos.x-x, y=chest_left_pos.y,
z=chest_left_pos.z-z}
return param2_by_offset[(chest_right_pos - hopper_pos):to_string()]
end
hopper:set_extra_container_info({
{"default:chest_connected_left", set_hopper_param2 = set_hopper_param2},
})
end