Skip to content
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ read_globals = {
-- Minetest
"vector", "ItemStack",
"dump", "VoxelArea",
"VoxelManip",

-- deps
"minetest",
Expand All @@ -24,6 +25,7 @@ read_globals = {
"pipeworks",
"screwdriver",
"digilines",
"digiline",
"mesecon",
"techage"
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ Do you have too many cobblestones for one drawer? No problem, just add some
drawer upgrades to your drawer! They are available in different sizes and are
crafted by steel, gold, obsidian, diamonds or mithril.

## Digilines
The drawer controller is digilines-compatible. To request an item from the
surrounding drawers, send an itemstring to its channel:
* `"default:dirt 15"`
* `"default:cobble"`

The items will be sent out the back of the drawer controller.

To request the contents of a drawer network, send a table with the following
format:

1. `command` (string) - `"get"`
2. `offset` (integer) - Used to paginate the results if the amount of drawers in
the network exceeds `drawers.CONTROLLER_MAX_COUNT`. Defaults to 1.
3. `max_count` (integer) - Must be between 1 and `CONTROLLER_MAX_COUNT`.
Defaults to `CONTROLLER_MAX_COUNT`. Maximum amount of drawers to return.

A table will be sent back on the same channel containing each drawer's position
and the contents of each of their slots.

## Notes
This mod requires Minetest 5.0 or later. The `default` mod from MTG or the
MineClone 2 mods are only optional dependencies for crafting recipes.
Expand Down
7 changes: 6 additions & 1 deletion init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ end
dofile(MP .. "/lua/helpers.lua")
dofile(MP .. "/lua/visual.lua")
dofile(MP .. "/lua/api.lua")
dofile(MP .. "/lua/controller.lua")


--
Expand Down Expand Up @@ -373,6 +372,12 @@ core.register_craft({
}
})

--
-- Register drawer controller
--

dofile(MP .. "/lua/controller.lua")

--
-- Register drawer upgrade template
--
Expand Down
144 changes: 122 additions & 22 deletions lua/controller.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ local pipeworks_loaded = core.get_modpath("pipeworks") and pipeworks
local digilines_loaded = core.get_modpath("digilines") and digilines
local techage_loaded = core.get_modpath("techage") and techage

local max_matches = tonumber(core.settings:get("drawers.controller_max_matches")) or 50

-- Cache content IDS of all registered drawer items
local controller_content_id
local trim_content_id = core.get_content_id("drawers:trim")
local drawer_content_ids = {}
for name, _ in pairs(core.registered_items) do
if core.get_item_group(name, "drawer") > 0 or core.get_item_group(name, "drawer_connector") > 0 then
drawer_content_ids[core.get_content_id(name)] = true
end
end
Comment on lines +55 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should run in on_mods_loaded, in case further registrations follow after this code is executed.


-- Cache position offsets to find connected drawers
local offsets = {}
for dx = -1, 1 do
for dy = -1, 1 do
for dz = -1, 1 do
if dx ~= 0 or dy ~= 0 or dz ~= 0 then
table.insert(offsets, vector.new(dx, dy, dz))
end
end
end
end

local function controller_formspec(pos)
local formspec =
"size[9,8.5]"..
Expand Down Expand Up @@ -171,28 +195,42 @@ local function add_drawer_to_inventory(controllerInventory, pos)
end
end

local function find_connected_drawers(controller_pos, pos, foundPositions)
foundPositions = foundPositions or {}
pos = pos or controller_pos

local newPositions = core.find_nodes_in_area(
{x = pos.x - 1, y = pos.y - 1, z = pos.z - 1},
{x = pos.x + 1, y = pos.y + 1, z = pos.z + 1},
{"group:drawer", "group:drawer_connector"}
)

for _,p in ipairs(newPositions) do
-- check that this node hasn't been scanned yet
if not compare_pos(pos, p) and not contains_pos(foundPositions, p)
and pos_in_range(controller_pos, pos) then
-- add new position
table.insert(foundPositions, p)
-- search for other drawers from the new pos
find_connected_drawers(controller_pos, p, foundPositions)
local function find_connected_drawers(controller_pos)
local minp = vector.subtract(controller_pos, drawers.CONTROLLER_RANGE)
local maxp = vector.add(controller_pos, drawers.CONTROLLER_RANGE)

local vm = VoxelManip()
local emin, emax = vm:read_from_map(minp, maxp)
local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
local data = vm:get_data()

local found = {}
local visited = {}

local function dfs(pos)
local index = area:indexp(pos)
if visited[index] then return end
visited[index] = true
Comment on lines +212 to +213
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could set data[index] = nil to mark it as done.


local content_id = data[index]
if drawer_content_ids[content_id] then
if content_id ~= trim_content_id then
table.insert(found, pos)
end
elseif content_id ~= controller_content_id then
return
end
Comment on lines +220 to +222
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think all controllers should be traversed. From what I can see, the previous code did not do that.
If you need this as a start condition, then perhaps pass a 2nd (optional) argument to dfs.

Copy link
Author

@Bituvo Bituvo Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, should I traverse only one drawer controller?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, otherwise players can make bigger networks than the max radius.


for _, offset in ipairs(offsets) do
local neighbor = vector.add(pos, offset)
if pos_in_range(controller_pos, neighbor) then
dfs(neighbor)
end
end
end

return foundPositions
dfs(controller_pos)
return found
end

local function index_drawers(pos)
Expand Down Expand Up @@ -297,6 +335,58 @@ local function controller_insert_to_drawers(pos, stack)
return stack
end

--[[
Returns an array of drawers in the drawer network with their positions and slot
data. Slot data includes stored item, item count, and max count.

Results can be paginated by specifying an `offset` and a `max_count`.
]]
local function controller_get_network_info(pos, offset, max_count)
local found_drawers = {}
local connected_drawers = find_connected_drawers(pos)

-- Sort drawers by their positions to keep order for pagination
table.sort(connected_drawers, function(a, b)
return core.hash_node_position(a) < core.hash_node_position(b)
end)

-- Offset must be an integer and >= 1
offset = math.max(1, math.floor(tonumber(offset) or 1))
-- Max count must be an integer, >= 1, and <= max_matches
max_count = math.floor(tonumber(max_count) or max_matches)
max_count = math.min(math.max(1, max_count), max_matches)

for i = offset, offset + max_count - 1 do
local position = connected_drawers[i]
if not position then break end

local node = core.get_node(position)
local drawer_meta = core.get_meta(position)
local node_def = core.registered_nodes[node.name]
local drawer_type = node_def.groups.drawer

-- Record information of each slot
local slots = {}
for slot = 1, drawer_type do
-- 1x1 drawers don't have numbers in the meta fields
local slot_id = (drawer_type == 1 and "") or slot
local slot_name = drawer_meta:get_string("name" .. slot_id)
local slot_count = drawer_meta:get_int("count" .. slot_id)
local slot_max = drawer_meta:get_int("max_count" .. slot_id)

table.insert(slots, {
name = slot_name,
count = slot_count,
max = slot_max
})
end

table.insert(found_drawers, {position=position, slots=slots})
end

return found_drawers
end

local function controller_can_dig(pos, player)
local meta = core.get_meta(pos);
local inv = meta:get_inventory()
Expand Down Expand Up @@ -374,12 +464,21 @@ end
local function controller_on_digiline_receive(pos, _, channel, msg)
local meta = core.get_meta(pos)

if channel ~= meta:get_string("digilineChannel") then
if not msg or channel ~= meta:get_string("digilineChannel") then
return
end

if msg and type(msg) ~= "string" and type(msg) ~= "table" then
return -- Protect against ItemStack(...) errors
if type(msg) == "table" then
if msg.command == "get" then
digiline:receptor_send(pos, digilines.rules.default, channel,
controller_get_network_info(pos, msg.offset, msg.max_count)
)
return
end

elseif type(msg) ~= "string" then
-- Protect against ItemStack(...) errors
return
end

local item = ItemStack(msg)
Expand Down Expand Up @@ -501,6 +600,7 @@ local function register_controller()
end

core.register_node("drawers:controller", def)
controller_content_id = core.get_content_id("drawers:controller")

if techage_loaded then
techage.register_node({"drawers:controller"}, {
Expand Down
1 change: 1 addition & 0 deletions settingtypes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
drawers.controller_max_matches (Maximum controller request matches) int 50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A description that this is about digilines might be helpful to the player/user.