v1.00 Extracted from TechPack to be used as standalone mod

master
Joachim Stolberg 2018-08-09 15:00:40 +02:00
parent f3cd8fc992
commit ff39a821d2
13 changed files with 642 additions and 519 deletions

View File

@ -1,7 +1,3 @@
:boom: This Mod is now part of the Modpack ![TechPack](https://github.com/joe7575/techpack).
This repository here is no longer maintained. :boom:
```
# Tube Library
Minetest Tube Mod for item exchange via lumber tubes and wireless message communication between nodes.
@ -23,6 +19,9 @@ The mod provides:
Hints for Admins: ![manual.md](https://github.com/joe7575/Minetest-Tubelib/blob/master/manual.md)
Programmers Manual: ![api.md](https://github.com/joe7575/Minetest-Tubelib/blob/master/api.md)
This mod is part of the ModPack ![TechPack](https://github.com/joe7575/techpack)
A Tutorial to this Mod is available as ![TechPack Wiki](https://github.com/joe7575/techpack/wiki)
Browse on: ![GitHub](https://github.com/joe7575/Minetest-Tubelib)
Download: ![GitHub](https://github.com/joe7575/Minetest-Tubelib/archive/master.zip)
@ -32,7 +31,10 @@ Download: ![GitHub](https://github.com/joe7575/Minetest-Tubelib/archive/master.z
default
# License
Copyright (C) 2017 Joachim Stolberg
Copyright (C) 2017-2018 Joachim Stolberg
Code: Licensed under the GNU LGPL version 2.1 or later. See LICENSE.txt and http://www.gnu.org/licenses/lgpl-2.1.txt
Textures: CC0
```
## Dependencies
default

13
api.md
View File

@ -197,18 +197,12 @@ Because several nodes could be addressed, the function don't return any response
```LUA
tubelib.send_request(number, placer_name, clicker_name, topic, payload)
tubelib.send_request(number, topic, payload)
```
In contrast to `send_message` this functions send a message to exactly one node
referenced by `number` and returns the node response.
The message is based on the topic string (e.g. "state") and
topic related payload.
The placer and clicker names are needed to check the protection rights.
`placer_name` is the name of the player, who places the node.
`clicker_name` is the name of the player, who uses the node.
`placer_name` of sending and receiving nodes have to be the same.
If every player should be able to send a message, use nil for clicker_name.
## 4. Code Snippets
@ -279,10 +273,11 @@ and as templates for own projects:
## 6. Further information
The complete functionality is implemented in the file
![command.lua](https://github.com/joe7575/Minetest-Tubelib/blob/master/command.lua).
![command.lua](https://github.com/joe7575/techpack/blob/master/tubelib/command.lua).
This file has further helper functions and is recommended for deeper study.
## 7. History
2017-10-02 First draft
2017-10-29 Commands start/stop replaced by on/off
2017-10-29 Commands start/stop replaced by on/off
2018-03-31 Corrections for 'send_request' and 'add_node'

View File

@ -49,6 +49,7 @@ minetest.register_node("tubelib:blackhole", {
tubelib.remove_node(pos) -- <<=== tubelib
end,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
paramtype2 = "facedir",

View File

@ -52,7 +52,11 @@ local function switch_off(pos)
local own_num = meta:get_string("own_num")
local numbers = meta:get_string("numbers")
local placer_name = meta:get_string("placer_name")
tubelib.send_message(numbers, placer_name, nil, "off", own_num) -- <<=== tubelib
local clicker_name = nil
if meta:get_string("public") == "false" then
clicker_name = meta:get_string("clicker_name")
end
tubelib.send_message(numbers, placer_name, clicker_name, "off", own_num) -- <<=== tubelib
end
@ -123,6 +127,7 @@ minetest.register_node("tubelib:button", {
end
end,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
paramtype2 = "facedir",
@ -153,13 +158,15 @@ minetest.register_node("tubelib:button_active", {
end,
on_timer = switch_off,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
paramtype2 = "facedir",
groups = {crumbly=0, not_in_creative_inventory=1},
groups = {choppy=2, cracky=2, crumbly=2, not_in_creative_inventory=1},
is_ground_content = false,
sounds = default.node_sound_wood_defaults(),
drop = "tubelib:button",
})
minetest.register_craft({

View File

@ -23,13 +23,14 @@ local Version = minetest.deserialize(storage:get_string("Version")) or 1
local Number2Pos = minetest.deserialize(storage:get_string("Number2Pos")) or {}
local function update_mod_storage()
minetest.log("action", "[Tubelib] Store data...")
storage:set_string("NextNumber", minetest.serialize(NextNumber))
storage:set_string("Version", minetest.serialize(Version))
storage:set_string("Number2Pos", minetest.serialize(Number2Pos))
storage:set_string("Key2Number", nil) -- not used any more
-- store data each hour
minetest.after(60*60, update_mod_storage)
print("[Tubelib] Data stored")
minetest.log("action", "[Tubelib] Data stored")
end
minetest.register_on_shutdown(function()
@ -94,6 +95,20 @@ local function generate_Key2Number()
end
end
local function not_protected(pos, placer_name, clicker_name)
local meta = minetest.get_meta(pos)
if meta then
local cached_name = meta:get_string("tubelib_cached_name")
if placer_name and (placer_name == cached_name or not minetest.is_protected(pos, placer_name)) then
meta:set_string("tubelib_cached_name", placer_name)
if clicker_name == nil or not minetest.is_protected(pos, clicker_name) then
return true
end
end
end
return false
end
-------------------------------------------------------------------
-- API helper functions
-------------------------------------------------------------------
@ -158,7 +173,7 @@ end
-- Node construction/destruction functions
-------------------------------------------------------------------
-- Add node to the tubelib lists and update the tube surrounding.
-- Add node to the tubelib lists.
-- Function determines and returns the node position number,
-- needed for message communication.
function tubelib.add_node(pos, name)
@ -168,8 +183,6 @@ function tubelib.add_node(pos, name)
pos = pos,
name = name,
}
-- update surrounding tubes
tubelib.update_tubes(pos)
return number
end
@ -204,12 +217,17 @@ end
function tubelib.register_node(name, add_names, node_definition)
tubelib_NodeDef[name] = node_definition
-- store facedir table for all known node names
tubelib.knownNodes[name] = true
Name2Name[name] = name
for _,n in ipairs(add_names) do
tubelib.knownNodes[n] = true
Name2Name[n] = name
end
if node_definition.on_pull_item or node_definition.on_push_item or
node_definition.is_pusher then
tubelib.KnownNodes[name] = true
for _,n in ipairs(add_names) do
tubelib.KnownNodes[n] = true
end
end
end
-------------------------------------------------------------------
@ -220,13 +238,9 @@ function tubelib.send_message(numbers, placer_name, clicker_name, topic, payload
for _,num in ipairs(string_split(numbers, " ")) do
if Number2Pos[num] and Number2Pos[num].name then
local data = Number2Pos[num]
if placer_name and not minetest.is_protected(data.pos, placer_name) then
if clicker_name == nil or not minetest.is_protected(data.pos, clicker_name) then
if data and data.name then
if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then
tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload)
end
end
if not_protected(data.pos, placer_name, clicker_name) then
if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then
tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload)
end
end
end
@ -236,10 +250,8 @@ end
function tubelib.send_request(number, topic, payload)
if Number2Pos[number] and Number2Pos[number].name then
local data = Number2Pos[number]
if data and data.name then
if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then
return tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload)
end
if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then
return tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload)
end
end
return false
@ -251,6 +263,7 @@ end
function tubelib.pull_items(pos, side, player_name)
local npos, facedir = get_neighbor_pos(pos, side)
if npos == nil then return end
local nside, node = get_node_side(npos, facedir)
local name = Name2Name[node.name]
if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_pull_item then
@ -261,6 +274,7 @@ end
function tubelib.push_items(pos, side, items, player_name)
local npos, facedir = get_neighbor_pos(pos, side)
if npos == nil then return end
local nside, node = get_node_side(npos, facedir)
local name = Name2Name[node.name]
if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_push_item then
@ -274,6 +288,7 @@ end
function tubelib.unpull_items(pos, side, items, player_name)
local npos, facedir = get_neighbor_pos(pos, side)
if npos == nil then return end
local nside, node = get_node_side(npos, facedir)
local name = Name2Name[node.name]
if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_unpull_item then
@ -282,6 +297,20 @@ function tubelib.unpull_items(pos, side, items, player_name)
return false
end
function tubelib.pull_stack(pos, side, player_name)
local npos, facedir = get_neighbor_pos(pos, side)
if npos == nil then return end
local nside, node = get_node_side(npos, facedir)
local name = Name2Name[node.name]
if tubelib_NodeDef[name] then
if tubelib_NodeDef[name].on_pull_stack then
return tubelib_NodeDef[name].on_pull_stack(npos, nside, player_name)
elseif tubelib_NodeDef[name].on_pull_item then
return tubelib_NodeDef[name].on_pull_item(npos, nside, player_name)
end
end
return nil
end
-------------------------------------------------------------------
-- Server side helper functions
@ -363,6 +392,20 @@ function tubelib.get_num_items(meta, listname, num)
return nil
end
function tubelib.get_stack(meta, listname)
local inv = meta:get_inventory()
local item = tubelib.get_item(meta, listname)
if item and inv:contains_item(listname, item) then
-- try to remove a complete stack
item:set_count(98)
local taken = inv:remove_item(listname, item)
-- add the already removed
taken:set_count(taken:get_count() + 1)
return taken
end
return item
end
-- Return "full", "loaded", or "empty" depending
-- on the number of fuel stack items.
-- Function only works on fuel inventories with one stacks/99 items

View File

@ -25,7 +25,6 @@ local TICKS_TO_SLEEP = 5
local CYCLE_TIME = 2
local STOP_STATE = 0
local STANDBY_STATE = -1
local FAULT_STATE = -2
-- Return a key/value table with all items and the corresponding stack numbers
local function invlist_content_as_kvlist(list)
@ -214,6 +213,8 @@ local function keep_running(pos, elapsed)
local inv = meta:get_inventory()
local list = inv:get_list("src")
local kvSrc = invlist_content_as_kvlist(list)
local counter = minetest.deserialize(meta:get_string("item_counter")) or
{red=0, green=0, blue=0, yellow=0}
-- calculate the filter settings only once
local hash = minetest.hash_node_position(pos)
@ -242,6 +243,7 @@ local function keep_running(pos, elapsed)
if not tubelib.push_items(pos, side, item, player_name) then -- <<=== tubelib
tubelib.put_item(meta, "src", item)
else
counter[listname] = counter[listname] + 1
busy = true
end
end
@ -258,6 +260,7 @@ local function keep_running(pos, elapsed)
if not tubelib.push_items(pos, side, item, player_name) then -- <<=== tubelib
tubelib.put_item(meta, "src", item)
else
counter[listname] = counter[listname] + 1
busy = true
end
end
@ -277,6 +280,7 @@ local function keep_running(pos, elapsed)
end
end
meta:set_string("item_counter", minetest.serialize(counter))
meta:set_int("running", running)
return true
end
@ -302,7 +306,7 @@ local function on_receive_fields(pos, formname, fields, player)
filter_settings(pos)
if fields.button ~= nil then
if running > STOP_STATE or running == FAULT_STATE then
if running > STOP_STATE then
stop_the_machine(pos)
else
start_the_machine(pos)
@ -312,6 +316,24 @@ local function on_receive_fields(pos, formname, fields, player)
end
end
-- tubelib command to turn on/off filter channels
local function change_filter_settings(pos, slot, val)
local slots = {["red"] = 1, ["green"] = 2, ["blue"] = 3, ["yellow"] = 4}
local meta = minetest.get_meta(pos)
local filter = minetest.deserialize(meta:get_string("filter"))
local num = slots[slot] or 1
if num >= 1 and num <= 4 then
filter[num] = val == "on"
end
meta:set_string("filter", minetest.serialize(filter))
filter_settings(pos)
local running = meta:get_int("running")
meta:set_string("formspec", distributor_formspec(tubelib.state(running), filter))
return true
end
minetest.register_node("tubelib:distributor", {
description = "Tubelib Distributor",
tiles = {
@ -340,6 +362,7 @@ minetest.register_node("tubelib:distributor", {
inv:set_size('green', 6)
inv:set_size('red', 6)
inv:set_size('blue', 6)
meta:set_string("item_counter", minetest.serialize({red=0, green=0, blue=0, yellow=0}))
end,
on_receive_fields = on_receive_fields,
@ -361,6 +384,7 @@ minetest.register_node("tubelib:distributor", {
allow_metadata_inventory_move = allow_metadata_inventory_move,
on_timer = keep_running,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
@ -399,6 +423,7 @@ minetest.register_node("tubelib:distributor_active", {
allow_metadata_inventory_move = allow_metadata_inventory_move,
on_timer = keep_running,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
@ -441,6 +466,15 @@ tubelib.register_node("tubelib:distributor", {"tubelib:distributor_active"}, {
local meta = minetest.get_meta(pos)
local running = meta:get_int("running")
return tubelib.statestring(running)
elseif topic == "filter" then
return change_filter_settings(pos, payload.slot, payload.val)
elseif topic == "counter" then
local meta = minetest.get_meta(pos)
return minetest.deserialize(meta:get_string("item_counter")) or
{red=0, green=0, blue=0, yellow=0}
elseif topic == "clear_counter" then
local meta = minetest.get_meta(pos)
meta:set_string("item_counter", minetest.serialize({red=0, green=0, blue=0, yellow=0}))
else
return "unsupported"
end

View File

@ -3,7 +3,7 @@
Tube Library
============
Copyright (C) 2017 Joachim Stolberg
Copyright (C) 2017,2018 Joachim Stolberg
LGPLv2.1+
See LICENSE.txt for more information
@ -19,7 +19,11 @@
2017-11-02 v0.08 Data base changed, aging of node positions added
2017-11-04 v0.09 functions set_data/get_data added
2018-01-27 v0.10 WLAN Chip added, recipes reviced, Pusher state 'blocked' added,
function send_request changed
function send_request changed
2018-08-09 v1.00 Extracted from TechPack to be used as standalone mod
- new tubing algorithm
- tubelib.pull_stack()/tubelib.get_stack() added
- item counter for pusher/distributor added
]]--
@ -28,7 +32,7 @@ tubelib = {
NodeDef = {}, -- node registration info
}
tubelib.version = 0.10
tubelib.version = 1.00
--------------------------- conversion to v0.04
@ -83,7 +87,8 @@ minetest.register_craft({
})
dofile(minetest.get_modpath("tubelib") .. "/tubes.lua")
dofile(minetest.get_modpath("tubelib") .. "/tubes1.lua")
dofile(minetest.get_modpath("tubelib") .. "/tubes2.lua")
dofile(minetest.get_modpath("tubelib") .. "/command.lua")
dofile(minetest.get_modpath("tubelib") .. "/states.lua")
dofile(minetest.get_modpath("tubelib") .. "/pusher.lua")

View File

@ -1,39 +0,0 @@
# Tubelib Library
## Hints for Admins and Players
Tubelib is little useful for itself, it makes only sense with extensions such as:
- ![tubelib_addons1](https://github.com/joe7575/tubelib_addons1) with farming nodes like Harvester, Quarry, Grinder, Autocrafter, Fermenter and Reformer.
- ![tubelib_addons2](https://github.com/joe7575/tubelib_addons2) with control task nodes like Timer, Sequencer, Repeater, Gate, Door, Access Lock and Color Lamp.
However Tubelib provides the following basic nodes:
### Tubes
Tubes allow the item exchange between two nodes. Tube forks are not possible. You have to use chests
or other inventory nodes as hubs to build more complex structures.
Tubes for itself are passive. For item exchange you have to use pulling/pushing nodes in addition.
The maximum tube length is limited to 100 nodes.
### Pusher
The Pusher is able to pull one item out of one inventory node and pushing it into another inventory node directly or by means of tubes.
It the source node is empty or the destination node full, the Pusher goes into STANDBY state for some seconds.
### Distributor
The Distributor works as filter and pusher. It allows to divide and distribute incoming items into 4 tube channels.
The channels can be switched on/off and individually configured with up to 6 items. The filter passes the configured
items and restrains all others. To increase the throughput, one item can be added several times to a filter.
An unconfigured but activated filter allows to pass up to 6 remaining items.
### Button/Switch
The Button/Switch is a simple communication node for the Tubelib wireless communication.
This node can be configured as button and switch. For the button configuration different switching
times from 2 to 16 seconds are possible. The Button/Switch node has to be configured with the destination
number of the receiving node (e.g. Lamp). This node allows to address several receivers by means or their numbers.
All numbers of the receiving nodes have to be added a configuration time.
### Lamp
The Lamp is a receiving node, showing its destination/communication number via "infotext".
The Lamp can be switched on/off by means of the right mouse button (use function) or by means of messages commands
from a Button/Switch or any other command sending node.

View File

@ -88,6 +88,7 @@ local function keep_running(pos, elapsed)
local node = minetest.get_node(pos)
return goto_blocked(pos, node)
end
meta:set_int("item_counter", meta:get_int("item_counter") + 1)
if running <= 0 then
local node = minetest.get_node(pos)
return switch_on(pos, node)
@ -123,6 +124,7 @@ minetest.register_node("tubelib:pusher", {
local number = tubelib.add_node(pos, "tubelib:pusher") -- <<=== tubelib
meta:set_string("number", number)
meta:set_string("infotext", "Pusher "..number..": stopped")
meta:set_int("item_counter", 0)
end,
on_rightclick = function(pos, node, clicker)
@ -136,6 +138,7 @@ minetest.register_node("tubelib:pusher", {
end,
on_timer = keep_running,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
@ -201,6 +204,7 @@ minetest.register_node("tubelib:pusher_active", {
end,
on_timer = keep_running,
on_rotate = screwdriver.disallow,
paramtype = "light",
sunlight_propagates = true,
@ -224,6 +228,7 @@ tubelib.register_node("tubelib:pusher", {"tubelib:pusher_active"}, {
on_pull_item = nil, -- pusher has no inventory
on_push_item = nil, -- pusher has no inventory
on_unpull_item = nil, -- pusher has no inventory
is_pusher = true, -- is a pulling/pushing node
on_recv_message = function(pos, topic, payload)
local node = minetest.get_node(pos)
@ -235,6 +240,12 @@ tubelib.register_node("tubelib:pusher", {"tubelib:pusher_active"}, {
local meta = minetest.get_meta(pos)
local running = meta:get_int("running") or tubelib.STATE_STOPPED
return tubelib.statestring(running)
elseif topic == "counter" then
local meta = minetest.get_meta(pos)
return meta:get_int("item_counter")
elseif topic == "clear_counter" then
local meta = minetest.get_meta(pos)
return meta:set_int("item_counter", 0)
else
return "not supported"
end

View File

@ -29,7 +29,7 @@ tubelib.StatesImg = {
"tubelib_inv_button_standby.png",
"tubelib_inv_button_error.png"
}
-- Return state button image for the node inventory
function tubelib.state_button(state)
if state and state < 5 and state > 0 then
@ -37,7 +37,10 @@ function tubelib.state_button(state)
end
return tubelib.StatesImg[tubelib.FAULT]
end
-- State string based on button states
tubelib.states = {"stopped", "running", "standby", "fault"}
--
-- 'running' variable States

438
tubes.lua
View File

@ -1,438 +0,0 @@
--[[
Tube Library
============
Copyright (C) 2017 Joachim Stolberg
LGPLv2.1+
See LICENSE.txt for more information
History:
see init.lua
]]--
local MAX_TUBE_LENGTH = 100
local TubeTypes = {
0,0,0,0,0,0,1,3,1,3, -- 01-10
4,5,3,1,3,1,4,5,1,3, -- 11-20
1,3,4,5,3,1,3,1,4,5, -- 21-30
2,2,2,2,0,2,2,2,5,2, -- 31-40
5,0, -- 40-41
}
local TubeFacedir = {
0,0,0,0,0,0,0,2,0,1, -- 01-10
2,2,2,1,3,1,3,3,0,3, -- 11-20
0,0,0,0,1,1,0,1,1,1, -- 21-30
0,0,0,0,0,0,0,0,0,0, -- 31-40
0,0, -- 40-41
}
tubelib.knownNodes = {
["tubelib:tube1"] = true,
["tubelib:tube2"] = true,
["tubelib:tube3"] = true,
["tubelib:tube4"] = true,
["tubelib:tube5"] = true,
["tubelib:tube6"] = true,
["default:chest_locked"] = true,
["default:chest"] = true,
["default:furnace"] = true,
["default:furnace_active"] = true,
}
-- Localize functions to avoid table lookups (better performance).
local string_find = string.find
local minetest_get_meta = minetest.get_meta
local minetest_get_node = minetest.get_node
local KnownNodes = tubelib.knownNodes
local vector_add = vector.add
local vector_equals = vector.equals
-- Translate from contact side to facedir
local SideToFacedir = {B=0, R=1, F=2, L=3, D=4, U=5}
-- 6D variant of the facedir to dir conversion
local Facedir2Dir = {[0] =
{x=0, y=0, z=1},
{x=1, y=0, z=0},
{x=0, y=0, z=-1},
{x=-1, y=0, z=0},
{x=0, y=-1, z=0},
{x=0, y=1, z=0},
}
-- Determine position from and facedir to the node on the other side or the tubes.
-- Param pos: my pos
-- Param npos: neighbor tube pos
local function remote_node(pos, npos)
local dest_pos = minetest.string_to_pos(minetest.get_meta(npos):get_string("dest_pos"))
-- two possible reasons, why dest_pos == pos:
-- 1) wrong side of a single tube node
-- 2) node connected with itself. In this case "dest_pos2" is not available
if dest_pos and vector_equals(dest_pos, pos) then
local dest_pos2 = minetest.string_to_pos(minetest.get_meta(npos):get_string("dest_pos2"))
if dest_pos2 == nil then
local facedir = minetest.get_meta(npos):get_int("facedir")
return pos, facedir -- node connected with itself
else
local facedir2 = minetest.get_meta(npos):get_int("facedir2")
return dest_pos2, facedir2 -- one tube connection
end
else
local facedir = minetest.get_meta(npos):get_int("facedir")
return dest_pos, facedir -- multi tube connection
end
end
-- Calculate the facedir to the other node, based on both node positions
local function dir_to_facedir(my_pos, other_pos)
if my_pos.z ~= other_pos.z then return my_pos.z - other_pos.z + 1 end
if my_pos.x ~= other_pos.x then return my_pos.x - other_pos.x + 2 end
if my_pos.y > other_pos.y then return 5 else return 4 end
end
-------------------------------------------------------------------------------
-- API functions
-------------------------------------------------------------------------------
-- Determine neighbor position and own facedir to the node.
-- based on own pos and contact side 'B' - 'U'.
-- Function considers also tube connections.
function tubelib.get_neighbor_pos(pos, side)
local facedir = SideToFacedir[side]
if facedir < 4 then
local node = minetest_get_node(pos)
facedir = (facedir + node.param2) % 4
end
local npos = vector_add(pos, Facedir2Dir[facedir])
local node = minetest_get_node(npos)
if node and string_find(node.name, "tubelib:tube") then
npos, facedir = remote_node(pos, npos)
end
return npos, facedir
end
-- use Voxel Manipulator to read the node
function tubelib.read_node_with_vm(pos)
local vm = VoxelManip()
local MinEdge, MaxEdge = vm:read_from_map(pos, pos)
local data = vm:get_data()
local param2_data = vm:get_param2_data()
local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
return {
name=minetest.get_name_from_content_id(data[area:index(pos.x, pos.y, pos.z)]),
param2 = param2_data[area:index(pos.x, pos.y, pos.z)]
}
end
local read_node_with_vm = tubelib.read_node_with_vm
-------------------------------------------------------------------------------
-- Tube placement
-------------------------------------------------------------------------------
-- Return neighbor tubes orientation relative to the given pos.
local function get_neighbor_tubes_orientation(pos)
local orientation = 0
local Nodes = {
minetest.get_node({x=pos.x , y=pos.y , z=pos.z+1}),
minetest.get_node({x=pos.x+1, y=pos.y , z=pos.z }),
minetest.get_node({x=pos.x , y=pos.y , z=pos.z-1}),
minetest.get_node({x=pos.x-1, y=pos.y , z=pos.z }),
minetest.get_node({x=pos.x , y=pos.y-1, z=pos.z }),
minetest.get_node({x=pos.x , y=pos.y+1, z=pos.z }),
}
for side,node in ipairs(Nodes) do
if KnownNodes[node.name] then -- registered node?
orientation = orientation * 6 + side
if orientation > 6 then
break
end
end
end
return orientation
end
local function determine_tube_node(pos)
local node = minetest.get_node(pos)
if not string.find(node.name, "tubelib:tube") then
return nil
end
local orientation = get_neighbor_tubes_orientation(pos)
if orientation > 6 then
node.name = "tubelib:tube"..TubeTypes[orientation]
node.param2 = TubeFacedir[orientation]
return node
elseif orientation > 0 then
orientation = orientation * 6 + (((node.param2 + 2) % 4) + 1)
node.name = "tubelib:tube"..TubeTypes[orientation]
node.param2 = TubeFacedir[orientation]
return node
end
return nil
end
local function update_tube(pos)
local node = determine_tube_node(pos)
if node then
minetest.swap_node(pos, node)
end
end
local OffsTable = {
{2,0}, -- tube1
{4,5}, -- tube2
{2,3}, -- tube3
{2,4}, -- tube4
{2,5}, -- tube5
}
-- The tube on 'pos1' has two ends and thus two neighbor position.
-- One is the given 'pos', the other position is returned.
-- Param mpos: my node position
-- Param opos: the other tube position
-- Param node: the tube node
local function nodetype_to_pos(mpos, opos, node)
local idx = string.byte(node.name, -1) - 48
local facedir1 = OffsTable[idx][1]
local facedir2 = OffsTable[idx][2]
if facedir1 < 4 then
facedir1 = (facedir1 + node.param2) % 4
end
if facedir2 < 4 then
facedir2 = (facedir2 + node.param2) % 4
end
local dir1 = Facedir2Dir[facedir1]
local dir2 = Facedir2Dir[facedir2]
local p1 = vector.add(opos, dir1)
local p2 = vector.add(opos, dir2)
if mpos == nil then
return p1, p2
elseif vector.equals(p1, mpos) then
return p2
else
return p1
end
end
-- Walk to the other end of the tube line, starting at 'pos1'.
-- Returns: cnt - number of tube nodes
-- pos - the peer tube node
-- pos1 - the destination position, connected with 'pos'
local function walk_to_peer(pos, pos1)
local node = minetest.get_node(pos1)
local pos2
local cnt = 0
while string.find(node.name, "tubelib:tube") and cnt < MAX_TUBE_LENGTH do
pos2 = nodetype_to_pos(pos, pos1, node)
pos, pos1 = pos1, pos2
cnt = cnt + 1
node = minetest.get_node_or_nil(pos1) or read_node_with_vm(pos1)
end
return cnt, pos, pos1
end
-- Update head tubes with peer pos and facedir of the other end
-- Needed for StackItem exchange.
local function update_head_tubes(pos)
local node = minetest.get_node(pos)
if string.find(node.name, "tubelib:tube") then
local pos1, pos2 = nodetype_to_pos(nil, pos, node)
local cnt1, peer1, dest1 = walk_to_peer(pos, pos1)
local cnt2, peer2, dest2 = walk_to_peer(pos, pos2)
if cnt1 == 0 and cnt2 == 0 then -- first tube node placed?
-- we have to store both dest positions
minetest.get_meta(peer1):set_string("dest_pos", minetest.pos_to_string(dest1))
minetest.get_meta(peer1):set_int("facedir", dir_to_facedir(peer1, dest1))
minetest.get_meta(peer1):set_string("dest_pos2", minetest.pos_to_string(dest2))
minetest.get_meta(peer1):set_int("facedir2", dir_to_facedir(peer2, dest2))
else
minetest.get_meta(peer1):set_string("dest_pos", minetest.pos_to_string(dest2))
minetest.get_meta(peer1):set_int("facedir", dir_to_facedir(peer2, dest2))
minetest.get_meta(peer2):set_string("dest_pos", minetest.pos_to_string(dest1))
minetest.get_meta(peer2):set_int("facedir", dir_to_facedir(peer1, dest1))
end
-- delete meta data from old head nodes
if cnt1 > 1 then
minetest.get_meta(pos1):from_table(nil)
end
if cnt2 > 1 then
minetest.get_meta(pos2):from_table(nil)
end
return cnt1 + cnt2
end
return 0
end
-- Update all tubes arround the currently placed tube
local function update_surrounding_tubes(pos)
update_tube({x=pos.x , y=pos.y , z=pos.z+1})
update_tube({x=pos.x+1, y=pos.y , z=pos.z })
update_tube({x=pos.x , y=pos.y , z=pos.z-1})
update_tube({x=pos.x-1, y=pos.y , z=pos.z })
update_tube({x=pos.x , y=pos.y-1, z=pos.z })
update_tube({x=pos.x , y=pos.y+1, z=pos.z })
update_tube(pos)
return update_head_tubes(pos) < MAX_TUBE_LENGTH
end
-- Update tubes after a tube node is removed
local function after_tube_removed(pos, node)
local pos1, pos2 = nodetype_to_pos(nil, pos, node)
update_head_tubes(pos1)
update_head_tubes(pos2)
end
-- API function, called from all nodes, which shall be connected to tubes
function tubelib.update_tubes(pos)
update_tube( {x=pos.x , y=pos.y , z=pos.z+1})
update_head_tubes({x=pos.x , y=pos.y , z=pos.z+1})
update_tube( {x=pos.x+1, y=pos.y , z=pos.z })
update_head_tubes({x=pos.x+1, y=pos.y , z=pos.z })
update_tube( {x=pos.x , y=pos.y , z=pos.z-1})
update_head_tubes({x=pos.x , y=pos.y , z=pos.z-1})
update_tube( {x=pos.x-1, y=pos.y , z=pos.z })
update_head_tubes({x=pos.x-1, y=pos.y , z=pos.z })
update_tube( {x=pos.x , y=pos.y-1, z=pos.z })
update_head_tubes({x=pos.x , y=pos.y-1, z=pos.z })
update_tube( {x=pos.x , y=pos.y+1, z=pos.z })
update_head_tubes({x=pos.x , y=pos.y+1, z=pos.z })
end
local DefNodeboxes = {
-- x1 y1 z1 x2 y2 z2
{ -1/4, -1/4, -1/4, 1/4, 1/4, 1/4 },
{ -1/4, -1/4, -1/4, 1/4, 1/4, 1/4 },
}
local DirCorrections = {
{3, 6}, {2, 5}, -- standard tubes
{3, 1}, {3, 2}, {3, 5}, -- knees from front to..
}
local SelectBoxes = {
{ -1/4, -1/4, -1/2, 1/4, 1/4, 1/2 },
{ -1/4, -1/2, -1/4, 1/4, 1/2, 1/4 },
{ -1/2, -1/4, -1/2, 1/4, 1/4, 1/4 },
{ -1/4, -1/2, -1/2, 1/4, 1/4, 1/4 },
{ -1/4, -1/4, -1/2, 1/4, 1/2, 1/4 },
}
local TilesData = {
-- up, down, right, left, back, front
{
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png",
"tubelib_tube.png",
"tubelib_hole.png",
"tubelib_hole.png",
},
{
"tubelib_hole.png",
"tubelib_hole.png",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
},
{
"tubelib_knee.png^[transformR270",
"tubelib_knee.png^[transformR180",
"tubelib_knee2.png^[transformR270",
"tubelib_hole2.png^[transformR90",
"tubelib_knee2.png^[transformR90",
"tubelib_hole2.png^[transformR270",
},
{
"tubelib_knee2.png",
"tubelib_hole2.png^[transformR180",
"tubelib_knee.png^[transformR270",
"tubelib_knee.png",
"tubelib_knee2.png",
"tubelib_hole2.png",
},
{
"tubelib_hole2.png",
"tubelib_knee2.png^[transformR180",
"tubelib_knee.png^[transformR180",
"tubelib_knee.png^[transformR90",
"tubelib_knee2.png^[transformR180",
"tubelib_hole2.png^[transformR180",
},
}
for idx,pos in ipairs(DirCorrections) do
node_box_data = table.copy(DefNodeboxes)
node_box_data[1][pos[1]] = node_box_data[1][pos[1]] * 2
node_box_data[2][pos[2]] = node_box_data[2][pos[2]] * 2
tiles_data = TilesData[idx]
if idx == 1 then
hidden = 0
else
hidden = 1
end
minetest.register_node("tubelib:tube"..idx, {
description = "Tubelib Tube",
tiles = tiles_data,
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = node_box_data,
},
selection_box = {
type = "fixed",
fixed = SelectBoxes[idx],
},
collision_box = {
type = "fixed",
fixed = SelectBoxes[idx],
},
after_place_node = function(pos, placer, itemstack, pointed_thing)
if update_surrounding_tubes(pos) == false then
after_tube_removed(pos, minetest.get_node(pos))
minetest.remove_node(pos)
return itemstack
end
end,
after_dig_node = function(pos, oldnode, oldmetadata, digger)
after_tube_removed(pos, oldnode)
end,
paramtype2 = "facedir",
paramtype = "light",
sunlight_propagates = true,
is_ground_content = false,
groups = {choppy=2, cracky=3, stone=1, not_in_creative_inventory=hidden},
drop = "tubelib:tube1",
sounds = default.node_sound_wood_defaults(),
})
end
minetest.register_craft({
output = "tubelib:tube1 4",
recipe = {
{"default:steel_ingot", "", "group:wood"},
{"", "group:wood", ""},
{"group:wood", "", "default:tin_ingot"},
},
})

283
tubes1.lua Normal file
View File

@ -0,0 +1,283 @@
--[[
Tube Library
============
Copyright (C) 2017-2018 Joachim Stolberg
LGPLv2.1+
See LICENSE.txt for more information
History:
see init.lua
tubes1.lua: Functions to place and remove tubes
]]--
-- debugging
local P = minetest.pos_to_string
local MAX_TUBE_LENGTH = 100
-- Conversion from tube number/param2 (number*10 + param2) to tube hole dirs (view from the inside)
local TubeDirs = {
[10] = {1,3},
[11] = {2,4},
[12] = {1,3},
[13] = {2,4},
[20] = {5,6},
[21] = {5,6},
[22] = {5,6},
[23] = {5,6},
[30] = {3,4},
[31] = {1,4},
[32] = {1,2},
[33] = {2,3},
[40] = {3,5},
[41] = {4,5},
[42] = {1,5},
[43] = {2,5},
[50] = {3,6},
[51] = {4,6},
[52] = {1,6},
[53] = {2,6},
}
-- Conversion from tube dirs (dir1 * 10 + dir2) to tube number/param2
local TubeNodeAttr = {}
for k,v in pairs(TubeDirs) do
local key = v[1] * 10 + v[2]
local number = math.floor(k / 10)
local param2 = k % 10
TubeNodeAttr[key] = {number = number, param2 = param2}
end
-- Convertion of 'dir' (view from the outside to inside and vs)
local Turn180Deg = {3,4,1,2,6,5}
local Dir2Offset = {
{x=0, y=0, z=1},
{x=1, y=0, z=0},
{x=0, y=0, z=-1},
{x=-1, y=0, z=0},
{x=0, y=-1, z=0},
{x=0, y=1, z=0}
}
tubelib.TubeNames = {
["tubelib:tube1"] = true,
["tubelib:tube2"] = true,
["tubelib:tube3"] = true,
["tubelib:tube4"] = true,
["tubelib:tube5"] = true,
}
-- used for registered nodes
tubelib.KnownNodes = {
["tubelib:tube1"] = true,
["tubelib:tube2"] = true,
["tubelib:tube3"] = true,
["tubelib:tube4"] = true,
["tubelib:tube5"] = true,
}
-- use Voxel Manipulator to read the node
function tubelib.read_node_with_vm(pos)
local vm = VoxelManip()
local MinEdge, MaxEdge = vm:read_from_map(pos, pos)
local data = vm:get_data()
local param2_data = vm:get_param2_data()
local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
return {
name = minetest.get_name_from_content_id(data[area:index(pos.x, pos.y, pos.z)]),
param2 = param2_data[area:index(pos.x, pos.y, pos.z)]
}
end
local function get_tube_number_and_param2(dir1, dir2)
if dir1 == dir2 then
dir2 = Turn180Deg[dir1]
end
if dir1 > dir2 then
dir1, dir2 = dir2, dir1
end
local item = TubeNodeAttr[dir1*10 + dir2]
return item.number, item.param2
end
-- convert 6D-dir to position
local function get_tube_pos(pos, dir)
return vector.add(pos, Dir2Offset[dir])
end
-- Tube open sides
local function get_tube_dirs(pos, node)
if node == nil then
node = minetest.get_node_or_nil(pos) or tubelib.read_node_with_vm(pos)
end
if tubelib.TubeNames[node.name] then
local ttype = (string.byte(node.name, -1) - 48) * 10 + node.param2
return TubeDirs[ttype][1], TubeDirs[ttype][2]
end
return nil, nil
end
function tubelib.get_next_tube(pos, dir)
pos = get_tube_pos(pos, dir)
local dir1, dir2 = get_tube_dirs(pos)
if dir1 then
dir = Turn180Deg[dir]
if dir == dir1 then
return pos, dir2
elseif dir == dir2 then
return pos, dir1
end
end
return pos, nil
end
local function is_known_node(pos, dir)
if dir then
pos = get_tube_pos(pos, dir)
local node = minetest.get_node_or_nil(pos) or tubelib.read_node_with_vm(pos)
if tubelib.KnownNodes[node.name] and not tubelib.TubeNames[node.name] then
return true
end
end
return false
end
-- Walk to the other end of the tube line, starting at 'pos/dir'.
-- Returns: cnt - number of tube nodes
-- pos - the peer tube node
-- dir - dir to the drop position, next after 'pos'
function tubelib.walk_to_peer(pos, dir)
local cnt = 0
while cnt < MAX_TUBE_LENGTH do
local new_pos, new_dir = tubelib.get_next_tube(pos, dir)
if not new_dir then
break
end
cnt = cnt + 1
pos, dir = new_pos, new_dir
end
return cnt, pos, dir
end
-- Delete meta data of the peer node
function tubelib.delete_meta_data(pos, node)
local dir1, dir2 = get_tube_dirs(pos, node)
local cnt1 = 0
local dir
if dir1 then
cnt1, pos, dir = tubelib.walk_to_peer(pos, dir1)
-- delete meta on peer tube
if cnt1 > 0 then
minetest.get_meta(pos):from_table(nil)
end
end
local cnt2 = 0
if dir2 then
cnt2, pos, dir = tubelib.walk_to_peer(pos, dir2)
-- delete meta on peer tube
if cnt2 > 0 then
minetest.get_meta(pos):from_table(nil)
end
end
return cnt1 + cnt2
end
local function swap_node(pos, node_num, param2)
local node = minetest.get_node(pos)
node.name = "tubelib:tube"..node_num
node.param2 = param2
minetest.swap_node(pos, node)
end
local function is_connected(pos, dir)
if dir then
pos = get_tube_pos(pos, dir)
local dir1,dir2 = get_tube_dirs(pos)
-- return true if connected
dir = Turn180Deg[dir]
return dir == dir1 or dir == dir2
end
return false
end
local function is_tubelib_block(pos, dir)
if dir then
pos = get_tube_pos(pos, dir)
local dir1,dir2 = get_tube_dirs(pos)
-- return true if connected
dir = Turn180Deg[dir]
return dir == dir1 or dir == dir2
end
return false
end
local function update_next_tube(dir, pos)
-- test if tube is connected with neighbor tubes
local dir1, dir2 = get_tube_dirs(pos)
local conn1 = is_connected(pos, dir1) or is_known_node(pos, dir1)
local conn2 = is_connected(pos, dir2) or is_known_node(pos, dir2)
-- already connected or no tube arround?
if conn1 == conn2 then
return
end
if conn1 then
dir2 = Turn180Deg[dir]
else
dir1 = Turn180Deg[dir]
end
local node_num, param2 = get_tube_number_and_param2(dir1, dir2)
swap_node(pos, node_num, param2)
end
-- update new placed tube
local function update_tube(pos, dir)
local dir1 = nil
local dir2 = nil
-- search on all 6 pos for up to 2 tubes with open holes or
-- other tubelib compatible nodes
for dir = 1,6 do
if not dir1 and is_connected(pos, dir) then
dir1 = dir
elseif not dir2 and is_connected(pos, dir) then
dir2 = dir
end
end
if not dir1 or not dir2 then
for dir = 1,6 do
if not dir1 and is_known_node(pos, dir) then
dir1 = dir
elseif not dir2 and is_known_node(pos, dir) then
dir2 = dir
end
end
end
dir1 = dir1 or dir
dir2 = dir2 or Turn180Deg[dir]
local node_num, param2 = get_tube_number_and_param2(dir1, dir2)
swap_node(pos, node_num, param2)
end
function tubelib.update_tubes(pos, dir)
-- Update all tubes arround the currently placed tube
update_next_tube(1, {x=pos.x , y=pos.y , z=pos.z+1})
update_next_tube(2, {x=pos.x+1, y=pos.y , z=pos.z })
update_next_tube(3, {x=pos.x , y=pos.y , z=pos.z-1})
update_next_tube(4, {x=pos.x-1, y=pos.y , z=pos.z })
update_next_tube(5, {x=pos.x , y=pos.y-1, z=pos.z })
update_next_tube(6, {x=pos.x , y=pos.y+1, z=pos.z })
-- Update the placed tube
update_tube(pos, dir)
return tubelib.delete_meta_data(pos, minetest.get_node(pos)) < MAX_TUBE_LENGTH
end

216
tubes2.lua Normal file
View File

@ -0,0 +1,216 @@
--[[
Tube Library
============
Copyright (C) 2017-2018 Joachim Stolberg
LGPLv2.1+
See LICENSE.txt for more information
History:
see init.lua
tubes2.lua: Node registration and API function to move items via tubes
]]--
-- Convertion of contact side to facedir
local SideToFacedir = {B=0, R=1, F=2, L=3, D=4, U=5}
-- Calculate the facedir to the other node, based on both node positions
local function dir_to_facedir(my_pos, other_pos)
if my_pos.z ~= other_pos.z then return my_pos.z - other_pos.z + 1 end
if my_pos.x ~= other_pos.x then return my_pos.x - other_pos.x + 2 end
if my_pos.y > other_pos.y then return 5 else return 4 end
end
local function remote_node(pos, dir)
local meta = minetest.get_meta(pos)
local cnt
-- legacy tube?
if meta:get_string("dest_pos2") ~= "" then
meta:from_table(nil)
end
-- data available
local dest_pos = meta:get_string("dest_pos")
if dest_pos ~= "" then
local npos = minetest.string_to_pos(dest_pos)
local facedir = meta:get_int("facedir")
return npos, facedir+1
end
-- determine data and store as meta
cnt, pos, dir = tubelib.walk_to_peer(pos, dir)
local pos1,_ = tubelib.get_next_tube(pos, dir)
meta:set_string("dest_pos", minetest.pos_to_string(pos1))
meta:set_int("facedir", dir - 1)
return pos1, dir
end
-- Determine neighbor position and own facedir to the node.
-- based on own pos and contact side 'B' - 'U'.
-- Function considers also tube connections.
function tubelib.get_neighbor_pos(pos, side)
local facedir = SideToFacedir[side]
local dir
if facedir < 4 then
local node = minetest.get_node(pos)
dir = ((facedir + node.param2) % 4) + 1
end
local npos, ndir = tubelib.get_next_tube(pos, dir)
local node = minetest.get_node(npos)
if tubelib.TubeNames[node.name] then
if ndir then
npos, ndir = remote_node(npos, ndir)
end
return npos, dir-1
end
return npos, facedir
end
-------------------------------------------------------------------------------
-- Node registration
-------------------------------------------------------------------------------
local DefNodeboxes = {
-- x1 y1 z1 x2 y2 z2
{ -1/4, -1/4, -1/4, 1/4, 1/4, 1/4 },
{ -1/4, -1/4, -1/4, 1/4, 1/4, 1/4 },
}
local DirCorrections = {
{3, 6}, {2, 5}, -- standard tubes
{3, 1}, {3, 2}, {3, 5}, -- knees from front to..
}
local SelectBoxes = {
{ -1/4, -1/4, -1/2, 1/4, 1/4, 1/2 },
{ -1/4, -1/2, -1/4, 1/4, 1/2, 1/4 },
{ -1/2, -1/4, -1/2, 1/4, 1/4, 1/4 },
{ -1/4, -1/2, -1/2, 1/4, 1/4, 1/4 },
{ -1/4, -1/4, -1/2, 1/4, 1/2, 1/4 },
}
local TilesData = {
-- up, down, right, left, back, front
{
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png",
"tubelib_tube.png",
"tubelib_hole.png",
"tubelib_hole.png",
},
{
"tubelib_hole.png",
"tubelib_hole.png",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
"tubelib_tube.png^[transformR90",
},
{
"tubelib_knee.png^[transformR270",
"tubelib_knee.png^[transformR180",
"tubelib_knee2.png^[transformR270",
"tubelib_hole2.png^[transformR90",
"tubelib_knee2.png^[transformR90",
"tubelib_hole2.png^[transformR270",
},
{
"tubelib_knee2.png",
"tubelib_hole2.png^[transformR180",
"tubelib_knee.png^[transformR270",
"tubelib_knee.png",
"tubelib_knee2.png",
"tubelib_hole2.png",
},
{
"tubelib_hole2.png",
"tubelib_knee2.png^[transformR180",
"tubelib_knee.png^[transformR180",
"tubelib_knee.png^[transformR90",
"tubelib_knee2.png^[transformR180",
"tubelib_hole2.png^[transformR180",
},
}
for idx,pos in ipairs(DirCorrections) do
local node_box_data = table.copy(DefNodeboxes)
node_box_data[1][pos[1]] = node_box_data[1][pos[1]] * 2
node_box_data[2][pos[2]] = node_box_data[2][pos[2]] * 2
local tiles_data = TilesData[idx]
local hidden
if idx == 1 then
hidden = 0
else
hidden = 1
end
minetest.register_node("tubelib:tube"..idx, {
description = "Tubelib Tube",
tiles = tiles_data,
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = node_box_data,
},
selection_box = {
type = "fixed",
fixed = SelectBoxes[idx],
},
collision_box = {
type = "fixed",
fixed = SelectBoxes[idx],
},
after_place_node = function(pos, placer, itemstack, pointed_thing)
local res
local pitch = placer:get_look_pitch()
if pitch > 1 then
res = tubelib.update_tubes(pos, 6)
elseif pitch < -1 then
res = tubelib.update_tubes(pos, 5)
else
local dir = placer:get_look_dir()
local facedir = minetest.dir_to_facedir(dir)
res = tubelib.update_tubes(pos, facedir + 1)
end
if res == false then
tubelib.delete_meta_data(pos, minetest.get_node(pos))
minetest.remove_node(pos)
return itemstack
end
end,
after_dig_node = function(pos, oldnode, oldmetadata, digger)
tubelib.delete_meta_data(pos, oldnode)
end,
on_rotate = screwdriver.disallow,
paramtype2 = "facedir",
paramtype = "light",
sunlight_propagates = true,
is_ground_content = false,
groups = {choppy=2, cracky=3, stone=1, not_in_creative_inventory=hidden},
drop = "tubelib:tube1",
sounds = default.node_sound_wood_defaults(),
})
end
minetest.register_craft({
output = "tubelib:tube1 4",
recipe = {
{"default:steel_ingot", "", "group:wood"},
{"", "group:wood", ""},
{"group:wood", "", "default:tin_ingot"},
},
})