hopper-cd2025/init.lua

875 lines
22 KiB
Lua

-- define global
hopper = {version = "20230815"}
-- Translation support
local S = minetest.get_translator("hopper")
-- creative check
local creative_mode_cache = minetest.settings:get_bool("creative_mode")
local function check_creative(name)
return creative_mode_cache or minetest.check_player_privs(name, {creative = true})
end
-- screwdriver mod
local mod_screwdriver = minetest.get_modpath("screwdriver")
-- containers ( { where, node, inventory, run_callbacks })
-- run_callbacks is false to suppress logging during transfer. (be quiet like pipeworks)
local containers = {
{"top", "hopper:hopper", "main", true},
{"bottom", "hopper:hopper", "main", true},
{"side", "hopper:hopper", "main", true},
{"side", "hopper:hopper_side", "main", true},
{"void", "hopper:hopper", "main", true},
{"void", "hopper:hopper_side", "main", true},
{"void", "hopper:hopper_void", "main", true},
}
-- default behavior for *_metadata_inventory_* callbacks
-- useful to prevent logging during transfer between nodes
-- cb_default: { true | false }
-- true: runs callbacks, except the nodes from the blacklist
-- false: only performs callbacks for the nodes in the whitelist
local cb_default = true
local cb_nodes = {}
if cb_default == false then
-- a whitelist follows
-- *_metadata_inventory_* or timer:start may be necessary to start processing
cb_nodes = {
-- callbacks without transfer logging
"^bones:bones", -- drop empty bones
"^default:furnace", -- start timer
"^wine:wine_barrel", -- start timer
-- crazy autocrafter:
-- allow_metadata_inventory_* is needed to start processing,
-- when 'main' was empty before.
-- But don't call get_node_timer(pos):start(1) when disabled.
"^pipeworks:autocrafter",
--[[ notes
-- no prozessing, no transfer logging
"^pipeworks:deployer_",
"^pipeworks:dispenser_",
"^pipeworks:nodebreaker_",
-- checks upgrade slots, no transfer logging
"^technic:[lmh]v_",
"^technic:coal_alloy_furnace", -- abm driven
"^technic:constructor_mk",
"^technic:injector",
"^technic:tool_workshop",
-- fixed: no transfer logging of fake players
"^hopper:hopper",
--]]
}
elseif cb_default == true then
-- a blacklist follows
-- *_metadata_inventory_* or timer:start is not needed to start processing
cb_nodes = {
-- callbacks opens the formspec
"^df_underworld_items:puzzle_seal",
-- callbacks disabled to prevent logging during transfer between nodes
"[:_]chest",
"^castle_storage:",
"^crafting_bench:workbench",
"^darkage:box",
"^darkage:wood_shelves",
"^homedecor:",
--[[ notes
-- no prozessing, logs action in on_metadata_inventory_*
"^digilines:chest",
"^homedecor:nightstand_",
"^homedecor:kitchen_cabinet_",
"^homedecor:refrigerator_",
"^homedecor:cardboard_box",
"^homedecor:desk",
"^homedecor:filing_cabine",
"^homedecor:wardrobe",
"^more_chests:",
"^nanotech:carbon_chest",
"^protector:chest",
"^technic:.*_chest",
-- abm driven, logs action in on_metadata_inventory_*
"^homedecor:microwave_oven",
"^homedecor:oven",
--]]
}
end
-- global function to add new containers
function hopper:add_container(list)
for n = 1, #list do
local cols = table.copy(list[n])
cols[4] = cb_default
for _, p in pairs(cb_nodes) do
if string.find(cols[2], p) then
cols[4] = not cb_nodes
break
end
end
table.insert(containers, cols)
end
end
-- default containers
hopper:add_container({
{"top", "default:chest", "main"},
{"bottom", "default:chest", "main"},
{"side", "default:chest", "main"},
{"top", "default:furnace", "dst"},
{"bottom", "default:furnace", "src"},
{"side", "default:furnace", "fuel"},
{"top", "default:furnace_active", "dst"},
{"bottom", "default:furnace_active", "src"},
{"side", "default:furnace_active", "fuel"},
{"top", "default:chest_locked", "main"}, -- checks owner before taking items
{"bottom", "default:chest_locked", "main"},
{"side", "default:chest_locked", "main"},
{"top", "default:chest_open", "main"}, -- new animated chests
{"bottom", "default:chest_open", "main"},
{"side", "default:chest_open", "main"},
{"top", "default:chest_locked_open", "main"}, -- checks owner before taking items
{"bottom", "default:chest_locked_open", "main"},
{"side", "default:chest_locked_open", "main"},
{"void", "default:chest", "main"},
{"void", "default:chest_open", "main"},
{"void", "default:furnace", "src"},
{"void", "default:furnace_active", "src"}
})
-- protector redo mod support
if minetest.get_modpath("protector") then
hopper:add_container({
{"top", "protector:chest", "main"},
{"bottom", "protector:chest", "main"},
{"side", "protector:chest", "main"},
{"void", "protector:chest", "main"}
})
end
-- wine mod support
if minetest.get_modpath("wine") then
hopper:add_container({
{"top", "wine:wine_barrel", "dst"},
{"bottom", "wine:wine_barrel", "src"},
{"side", "wine:wine_barrel", "src"},
{"void", "wine:wine_barrel", "src"}
})
end
-- formspec
local function get_hopper_formspec(pos)
local spos = pos.x .. "," .. pos.y .. "," .. pos.z
local formspec =
"size[8,9]"
.. default.gui_bg
.. default.gui_bg_img
.. default.gui_slots
.. "list[nodemeta:" .. spos .. ";main;0,0.3;8,4;]"
.. "list[current_player;main;0,4.85;8,1;]"
.. "list[current_player;main;0,6.08;8,3;8]"
.. "listring[nodemeta:" .. spos .. ";main]"
.. "listring[current_player;main]"
return formspec
end
-- only log actions of real players
local function log_action(player, pos, message)
if player and not player.is_fake_player and player:is_player() then
minetest.log("action", player:get_player_name() .. " "
.. message .. " at " .. minetest.pos_to_string(pos))
end
end
-- check where pointing and set normal or side-hopper
local hopper_place = function(itemstack, placer, pointed_thing)
local pos = pointed_thing.above
local x = pointed_thing.under.x - pos.x
local z = pointed_thing.under.z - pos.z
local name = placer:get_player_name() or ""
if minetest.is_protected(pos, name) then
minetest.record_protection_violation(pos, name)
return itemstack
end
-- make sure we aren't replacing something we shouldnt
local node = minetest.get_node_or_nil(pos)
local def = node and minetest.registered_nodes[node.name]
if def and not def.buildable_to then
return itemstack
end
if pointed_thing.type == "node"
and placer and not placer:get_player_control().sneak then
local nn = minetest.get_node(pointed_thing.under).name
if minetest.registered_nodes[nn]
and minetest.registered_nodes[nn].on_rightclick then
return minetest.item_place(itemstack, placer, pointed_thing)
end
end
if x == -1 then
minetest.set_node(pos, {name = "hopper:hopper_side", param2 = 0})
elseif x == 1 then
minetest.set_node(pos, {name = "hopper:hopper_side", param2 = 2})
elseif z == -1 then
minetest.set_node(pos, {name = "hopper:hopper_side", param2 = 3})
elseif z == 1 then
minetest.set_node(pos, {name = "hopper:hopper_side", param2 = 1})
else
minetest.set_node(pos, {name = "hopper:hopper"})
end
if not check_creative(placer:get_player_name()) then
itemstack:take_item()
end
-- set metadata
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size("main", 4*4)
meta:set_string("owner", name)
return itemstack
end
-- hopper
minetest.register_node("hopper:hopper", {
description = S("Hopper (Place onto sides for side-hopper)"),
groups = {cracky = 3},
drawtype = "nodebox",
paramtype = "light",
use_texture_alpha = "clip",
tiles = {"hopper_top.png", "hopper_top.png", "hopper_front.png"},
inventory_image = "hopper_inv.png",
node_box = {
type = "fixed",
fixed = {
--funnel walls
{-0.5, 0.0, 0.4, 0.5, 0.5, 0.5},
{0.4, 0.0, -0.5, 0.5, 0.5, 0.5},
{-0.5, 0.0, -0.5, -0.4, 0.5, 0.5},
{-0.5, 0.0, -0.5, 0.5, 0.5, -0.4},
--funnel base
{-0.5, 0.0, -0.5, 0.5, 0.1, 0.5},
--spout
{-0.3, -0.3, -0.3, 0.3, 0.0, 0.3},
{-0.15, -0.3, -0.15, 0.15, -0.5, 0.15}
}
},
on_place = hopper_place,
can_dig = function(pos, player)
local inv = minetest.get_meta(pos):get_inventory()
return inv:is_empty("main")
end,
on_rightclick = function(pos, node, clicker, itemstack)
if not minetest.get_meta(pos)
or minetest.is_protected(pos, clicker:get_player_name()) then
return itemstack
end
minetest.show_formspec(clicker:get_player_name(),
"hopper:hopper", get_hopper_formspec(pos))
end,
on_metadata_inventory_move = function(
pos, from_list, from_index, to_list, to_index, count, player)
log_action(player, pos, "moves stuff in hopper")
end,
on_metadata_inventory_put = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff to hopper")
end,
on_metadata_inventory_take = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff from hopper")
end,
on_rotate = mod_screwdriver and screwdriver.disallow,
on_blast = function() end
})
-- side hopper
minetest.register_node("hopper:hopper_side", {
description = S("Side Hopper (Place into crafting to return normal Hopper)"),
groups = {cracky = 3, not_in_creative_inventory = 1},
drawtype = "nodebox",
paramtype = "light",
use_texture_alpha = "clip",
paramtype2 = "facedir",
tiles = {
"hopper_top.png", "hopper_top.png", "hopper_back.png",
"hopper_side.png", "hopper_back.png", "hopper_back.png"
},
inventory_image = "hopper_side_inv.png",
drop = "hopper:hopper",
node_box = {
type = "fixed",
fixed = {
--funnel walls
{-0.5, 0.0, 0.4, 0.5, 0.5, 0.5},
{0.4, 0.0, -0.5, 0.5, 0.5, 0.5},
{-0.5, 0.0, -0.5, -0.4, 0.5, 0.5},
{-0.5, 0.0, -0.5, 0.5, 0.5, -0.4},
--funnel base
{-0.5, 0.0, -0.5, 0.5, 0.1, 0.5},
--spout
{-0.3, -0.3, -0.3, 0.3, 0.0, 0.3},
{-0.7, -0.3, -0.15, 0.15, 0.0, 0.15}
}
},
on_place = hopper_place,
can_dig = function(pos, player)
local inv = minetest.get_meta(pos):get_inventory()
return inv:is_empty("main")
end,
on_rightclick = function(pos, node, clicker, itemstack)
if not minetest.get_meta(pos)
or minetest.is_protected(pos, clicker:get_player_name()) then
return itemstack
end
minetest.show_formspec(clicker:get_player_name(),
"hopper:hopper_side", get_hopper_formspec(pos))
end,
on_metadata_inventory_move = function(
pos, from_list, from_index, to_list, to_index, count, player)
log_action(player, pos, "moves stuff in side hopper")
end,
on_metadata_inventory_put = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff to side hopper")
end,
on_metadata_inventory_take = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff from side hopper")
end,
on_rotate = mod_screwdriver and screwdriver.rotate_simple,
on_blast = function() end
})
local player_void = {}
-- void hopper
minetest.register_node("hopper:hopper_void", {
description = S("Void Hopper (Use first to set destination container)"),
groups = {cracky = 3},
drawtype = "nodebox",
paramtype = "light",
use_texture_alpha = "clip",
tiles = {"hopper_top.png", "hopper_top.png", "hopper_front.png"},
inventory_image = "default_obsidian.png^hopper_inv.png",
node_box = {
type = "fixed",
fixed = {
--funnel walls
{-0.5, 0.0, 0.4, 0.5, 0.5, 0.5},
{0.4, 0.0, -0.5, 0.5, 0.5, 0.5},
{-0.5, 0.0, -0.5, -0.4, 0.5, 0.5},
{-0.5, 0.0, -0.5, 0.5, 0.5, -0.4},
--funnel base
{-0.5, 0.0, -0.5, 0.5, 0.1, 0.5}
}
},
on_use = function(itemstack, player, pointed_thing)
if pointed_thing.type ~= "node" then
return
end
local pos = pointed_thing.under
local name = player:get_player_name()
local node = minetest.get_node(pos).name
local ok
if minetest.is_protected(pos, name) then
minetest.record_protection_violation(pos, name)
return itemstack
end
for _ = 1, #containers do
if node == containers[_][2] then
ok = true
end
end
if ok then
minetest.chat_send_player(name, S("Output container set")
.. " " .. minetest.pos_to_string(pos))
player_void[name] = pos
else
minetest.chat_send_player(name, S("Not a registered container!"))
player_void[name] = nil
end
end,
on_place = function(itemstack, placer, pointed_thing)
local pos = pointed_thing.above
local name = placer:get_player_name() or ""
if pointed_thing.type == "node"
and placer and not placer:get_player_control().sneak then
local nn = minetest.get_node(pointed_thing.under).name
if minetest.registered_nodes[nn]
and minetest.registered_nodes[nn].on_rightclick then
return minetest.item_place(itemstack, placer, pointed_thing)
end
end
if not player_void[name] then
minetest.chat_send_player(name, S("No container position set!"))
return itemstack
end
if minetest.is_protected(pos, name) then
minetest.record_protection_violation(pos, name)
return itemstack
end
-- make sure we aren't replacing something we shouldnt
local node = minetest.get_node_or_nil(pos)
local def = node and minetest.registered_nodes[node.name]
if def and not def.buildable_to then
return itemstack
end
if not check_creative(placer:get_player_name()) then
itemstack:take_item()
end
minetest.set_node(pos, {name = "hopper:hopper_void", param2 = 0})
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size("main", 4*4)
meta:set_string("owner", name)
meta:set_string("void", minetest.pos_to_string(player_void[name]))
meta:set_string("infotext", "Void Hopper\nConnected to " ..
minetest.pos_to_string(player_void[name]))
return itemstack
end,
can_dig = function(pos, player)
local inv = minetest.get_meta(pos):get_inventory()
return inv:is_empty("main")
end,
on_rightclick = function(pos, node, clicker, itemstack)
if not minetest.get_meta(pos)
or minetest.is_protected(pos, clicker:get_player_name()) then
return itemstack
end
minetest.show_formspec(clicker:get_player_name(),
"hopper:hopper", get_hopper_formspec(pos))
end,
on_metadata_inventory_move = function(
pos, from_list, from_index, to_list, to_index, count, player)
log_action(player, pos, "moves stuff in void hopper")
end,
on_metadata_inventory_put = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff into void hopper")
end,
on_metadata_inventory_take = function(pos, listname, index, stack, player)
log_action(player, pos, "moves stuff from void hopper")
end,
on_rotate = mod_screwdriver and screwdriver.disallow,
on_blast = function() end
})
-- transfer function
local transfer = function(src, srcpos, dst, dstpos, allowed, finished)
-- source inventory
local inv = minetest.get_meta(srcpos):get_inventory()
-- destination inventory
local inv2 = minetest.get_meta(dstpos):get_inventory()
-- check for empty source or no inventory
if not inv or not inv2 or inv:is_empty(src) == true then
return
end
local stack, item, max
-- transfer item
for i = 1, inv:get_size(src) do
stack = inv:get_stack(src, i)
item = stack:get_name()
-- if slot not empty and room for item in destination
if item ~= ""
and inv2:room_for_item(dst, item) then
local take = stack:take_item(1)
if allowed(i, take) then
inv2:add_item(dst, take)
inv:set_stack(src, i, stack)
finished(i, take)
end
return
end
end
end
local lazy = minetest.settings:get_bool("lazy_container_support")
local function add_container_lazy(meta, where, node_name, inv_names)
if not meta then return end
local inv = meta:get_inventory()
for _, inv_name in pairs(inv_names) do
if inv:get_size(inv_name) > 0 then
hopper:add_container({{where, node_name, inv_name}})
--print("hopper: add_container_lazy ["..#containers.."] "..where.." '"..node_name.."' "..inv_name .. " " .. (containers[#containers][4] and "true" or "false"))
return
end
end
end
-- hopper workings
minetest.register_abm({
label = "Hopper suction and transfer",
nodenames = {"hopper:hopper", "hopper:hopper_side", "hopper:hopper_void"},
interval = 1,
chance = 1,
catch_up = false,
action = function(pos, node, active_object_count, active_object_count_wider)
local inv = minetest.get_meta(pos):get_inventory()
for _,object in pairs(minetest.get_objects_inside_radius(pos, 1)) do
if not object:is_player()
and object:get_luaentity()
and object:get_luaentity().name == "__builtin:item"
and inv
and inv:room_for_item("main",
ItemStack(object:get_luaentity().itemstring)) then
if object:get_pos().y - pos.y > 0.25 then
inv:add_item("main",
ItemStack(object:get_luaentity().itemstring))
object:get_luaentity().itemstring = ""
object:remove()
end
end
end
local dst_pos
-- if side hopper check which way spout is facing
if node.name == "hopper:hopper_side" then
local face = node.param2
if face == 0 then
dst_pos = {x = pos.x - 1, y = pos.y, z = pos.z}
elseif face == 1 then
dst_pos = {x = pos.x, y = pos.y, z = pos.z + 1}
elseif face == 2 then
dst_pos = {x = pos.x + 1, y = pos.y, z = pos.z}
elseif face == 3 then
dst_pos = {x = pos.x, y = pos.y, z = pos.z - 1}
else
return
end
elseif node.name == "hopper:hopper_void" then
local meta = minetest.get_meta(pos)
if not meta then return end
dst_pos = minetest.string_to_pos(meta:get_string("void"))
elseif node.name == "hopper:hopper" then
-- otherwise normal hopper, output downwards
dst_pos = {x = pos.x, y = pos.y - 1, z = pos.z}
else
return
end
-- get node above hopper
local src_pos = {x = pos.x, y = pos.y + 1, z = pos.z}
local src_name = minetest.get_node(src_pos).name
-- get node at other end of spout
local dst_name = minetest.get_node(dst_pos).name
-- hopper owner
local owner = minetest.get_meta(pos):get_string("owner")
-- the hopper itself interacts as fake player
local player = {
is_player = function() return false end,
get_player_name = function() return owner end,
is_fake_player = ":hopper",
get_wielded_item = function() return ItemStack(nil) end
}
if minetest.check_player_privs(owner, "protection_bypass") then
owner = ""
end
local to
if node.name == "hopper:hopper" then
to = "bottom"
elseif node.name == "hopper:hopper_side" then
to = "side"
elseif node.name == "hopper:hopper_void" then
to = "void"
end
local where, name, inv, run_cb, src_inv, dst_inv, src_cb, dst_cb
-- do for loop here for api check
for n = 1, #containers do
where = containers[n][1]
name = containers[n][2]
inv = containers[n][3]
run_cb = containers[n][4]
if where == "top" and src_name == name then
src_inv = inv -- from hopper into destionation container
src_cb = run_cb
elseif where == to and dst_name == name then
dst_inv = inv
dst_cb = run_cb
end
end
-- get container owner
local c_owner = minetest.get_meta(src_pos):get_string("owner") or ""
-- if protection_bypass or actual owner or container not owned
if owner == "" or owner == c_owner or c_owner == "" then
if src_inv then
-- run callbacks from source node or not
local src_def = src_cb and minetest.registered_nodes[src_name]
local allowed = function(i, stack)
return not src_def
or not src_def.allow_metadata_inventory_take
or src_def.allow_metadata_inventory_take(src_pos, src_inv, i,
stack, player) > 0
end
local finished = function(i, stack)
return not src_def
or not src_def.on_metadata_inventory_take
or src_def.on_metadata_inventory_take(src_pos, src_inv, i, stack,
player)
end
transfer(src_inv, src_pos, "main", pos, allowed, finished)
elseif src_name ~= "ignore"
and lazy and not string.find(src_name, "^hopper:") then
local meta = minetest.get_meta(src_pos)
add_container_lazy(meta, "top", src_name, {"main", "dst", "out"})
end
end
c_owner = minetest.get_meta(dst_pos):get_string("owner") or ""
if owner == "" or owner == c_owner or c_owner == "" then
if dst_inv then
-- run callbacks from destionation node or not
local dst_def = dst_cb and minetest.registered_nodes[dst_name]
local allowed = function(i, stack)
return not dst_def
or not dst_def.allow_metadata_inventory_put
or dst_def.allow_metadata_inventory_put(dst_pos, dst_inv, i, stack,
player) > 0
end
local finished = function(i, stack)
return not dst_def
or not dst_def.on_metadata_inventory_put
or dst_def.on_metadata_inventory_put(dst_pos, dst_inv, i, stack,
player)
end
transfer("main", pos, dst_inv, dst_pos, allowed, finished)
elseif dst_name ~= "ignore" and lazy
and not string.find(dst_name, "^hopper:") then
local meta = minetest.get_meta(dst_pos)
if to == "side" then
add_container_lazy(meta, to, dst_name, {"fuel", "main", "src", "in"})
else
add_container_lazy(meta, to, dst_name, {"main", "src", "in"})
end
end
end
end
})
-- hopper recipe
minetest.register_craft({
output = "hopper:hopper",
recipe = {
{"default:steel_ingot", "default:chest", "default:steel_ingot"},
{"", "default:steel_ingot", ""}
}
})
-- side hopper to hopper recipe
minetest.register_craft({
output = "hopper:hopper",
recipe = {{"hopper:hopper_side"}}
})
-- void hopper recipe
if minetest.get_modpath("teleport_potion") then
minetest.register_craft({
output = "hopper:hopper_void",
recipe = {
{"default:steel_ingot", "default:chest", "default:steel_ingot"},
{"teleport_potion:potion", "default:steel_ingot", "teleport_potion:potion"}
}
})
else
minetest.register_craft({
output = "hopper:hopper_void",
recipe = {
{"default:steel_ingot", "default:chest", "default:steel_ingot"},
{"default:diamondblock", "default:steel_ingot", "default:mese"}
}
})
end
-- add lucky blocks
if minetest.get_modpath("lucky_block") then
lucky_block:add_blocks({
{"dro", {"hopper:hopper"}, 3},
{"nod", "default:lava_source", 1}
})
end
print ("[MOD] Hopper loaded")