From 885f8daf7b83e8d7a8f49a7fced5a01734b2af6d Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Sat, 18 May 2019 23:02:00 +0200 Subject: [PATCH] Rewrite drawer controller scanning This fixes the issue that some drawers have not been found by the controller. --- LICENSE.txt | 9 +- README.md | 4 +- init.lua | 7 +- lua/api.lua | 2 +- lua/controller.lua | 983 ++++++++++++---------- lua/visual.lua | 2 +- textures/drawer_controller_side.png | Bin 577 -> 0 bytes textures/drawer_controller_top_bottom.png | Bin 490 -> 0 bytes textures/drawers_controller_front.png | Bin 0 -> 366 bytes textures/drawers_controller_side.png | Bin 0 -> 374 bytes textures/drawers_controller_top.png | Bin 0 -> 331 bytes 11 files changed, 563 insertions(+), 444 deletions(-) delete mode 100644 textures/drawer_controller_side.png delete mode 100644 textures/drawer_controller_top_bottom.png create mode 100644 textures/drawers_controller_front.png create mode 100644 textures/drawers_controller_side.png create mode 100644 textures/drawers_controller_top.png diff --git a/LICENSE.txt b/LICENSE.txt index 60a1cdd..f2073f0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -4,7 +4,7 @@ Version 0.4.3 License of source code: ----------------------- -Copyright (C) 2017 LNJ +Copyright (C) 2017-2019 Linus Jahn Copyright (C) 2016 Mango Tango MIT License @@ -76,6 +76,9 @@ Copyright (C) 2014 Justin Aquadro (MIT): textures/drawers_birch_wood_front_2.png textures/drawers_birch_wood_front_4.png textures/drawers_birch_wood.png + textures/drawers_controller_front.png + textures/drawers_controller_side.png + textures/drawers_controller_top.png textures/drawers_dark_oak_wood_front_1.png textures/drawers_dark_oak_wood_front_2.png textures/drawers_dark_oak_wood_front_4.png @@ -109,8 +112,6 @@ Copyright (C) 2014 Justin Aquadro (MIT): textures/drawers_wood_front_2.png textures/drawers_wood_front_4.png textures/drawers_wood.png - textures/drawer_controller_side.png - textures/drawer_controller_top_bottom.png Everything not listed in here: -Copyright (C) 2017 LNJ (MIT) +Copyright (C) 2017-2019 Linus Jahn (MIT) diff --git a/README.md b/README.md index 4528cee..e1e873a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ MineClone 2 mods are only optional dependencies for crafting recipes. - [x] Add 2x2 and 1x2 drawers - [ ] Add compacting drawers for auto-crafting blocks/ingots/fragments - [ ] Add a key (or something similar) for locking the item (so the item is - also displayed at count 0) + also displayed at count 0) - [ ] Add duct tape to transport drawers - [x] Support pipeworks - [ ] Support hoppers (needs hoppers mod change) @@ -40,5 +40,5 @@ alternatively you can also [email](mailto:git@lnj.li) me. * [Minetest Forums](https://forum.minetest.net/viewtopic.php?f=9&t=17134) * [Minetest Wiki](http://wiki.minetest.net/Mods/Storage_Drawers) * [Weblate](https://hosted.weblate.org/projects/minetest/mod-storage-drawers/) -* [GitHub](http://github.com/lnj2/drawers/) +* [GitHub](http://github.com/minetest-mods/drawers/) diff --git a/init.lua b/init.lua index 0274e03..a39ce68 100755 --- a/init.lua +++ b/init.lua @@ -1,7 +1,7 @@ --[[ Minetest Mod Storage Drawers - A Mod adding storage drawers -Copyright (C) 2017 LNJ +Copyright (C) 2017-2019 Linus Jahn MIT License @@ -51,6 +51,7 @@ drawers.enable_1x1 = not core.settings:get_bool("drawers_disable_1x1") drawers.enable_1x2 = not core.settings:get_bool("drawers_disable_1x2") drawers.enable_2x2 = not core.settings:get_bool("drawers_disable_2x2") +drawers.CONTROLLER_RANGE = 8 -- -- GUI @@ -334,7 +335,7 @@ if core.get_modpath("mcl_core") and mcl_core then core.register_node("drawers:trim", { description = S("Wooden Trim"), tiles = {"drawers_trim.png"}, - groups = {handy = 1, axey = 1, flammable = 3, wood = 1, building_block = 1, material_wood = 1}, + groups = {drawer_connector = 1, handy = 1, axey = 1, flammable = 3, wood = 1, building_block = 1, material_wood = 1}, _mcl_blast_resistance = 15, _mcl_hardness = 2, }) @@ -342,7 +343,7 @@ else core.register_node("drawers:trim", { description = S("Wooden Trim"), tiles = {"drawers_trim.png"}, - groups = {drawer = 1, choppy = 3, oddly_breakable_by_hand = 2}, + groups = {drawer_connector = 1, choppy = 3, oddly_breakable_by_hand = 2}, }) end diff --git a/lua/api.lua b/lua/api.lua index 6c87f94..06c5cfd 100755 --- a/lua/api.lua +++ b/lua/api.lua @@ -1,7 +1,7 @@ --[[ Minetest Mod Storage Drawers - A Mod adding storage drawers -Copyright (C) 2017 LNJ +Copyright (C) 2017-2019 Linus Jahn Copyright (C) 2016 Mango Tango MIT License diff --git a/lua/controller.lua b/lua/controller.lua index e6e0bf6..d848fb0 100644 --- a/lua/controller.lua +++ b/lua/controller.lua @@ -1,433 +1,550 @@ ---[[ -Minetest Mod Storage Drawers - A Mod adding storage drawers - -Copyright (C) 2018 isaiah658 -Copyright (C) 2017 LNJ - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]]-- - ---[[ The gist of how the drawers mod stores data is that there are entities -and the drawer node itself. The entities are needed to allow having multiple -drawers in one node. The entities and node each store metadata about the item -counts and such. It is necessary to change both at once otherwise in some cases -the entity values are used and in other cases the node metadata is used. - -The gist of how the controller works is this. The drawer controller scans the -adjacent tiles (length and height is configurable) and puts the item names and -other info such as coordinates and the visualid of the entity in a table. That -table is saved in the controllers metadata. The table is used to help prevent -needing to scan all the drawers to deposit an item in certain situations. The -table is only updated on an as needed basis, not by a specific time/interval. -Controllers that have no items will not continue scanning drawers. ]]-- - --- Load support for intllib. -local MP = core.get_modpath(core.get_current_modname()) -local S, NS = dofile(MP.."/intllib.lua") - -local controller_interval = tonumber(core.setting_get("drawers_controller_interval")) or 7.0 - - -local function controller_can_dig(pos, player) - local meta = core.get_meta(pos); - local inv = meta:get_inventory() - return inv:is_empty("src") -end - -local function controller_allow_metadata_inventory_put(pos, listname, index, stack, player) - if core.is_protected(pos, player:get_player_name()) then - return 0 - end - if listname == "src" then - return stack:get_count() - end -end - -local function controller_allow_metadata_inventory_move(pos, from_list, from_index, to_list, to_index, count, player) - local meta = core.get_meta(pos) - local inv = meta:get_inventory() - local stack = inv:get_stack(from_list, from_index) - return controller_allow_metadata_inventory_put(pos, to_list, to_index, stack, player) -end - -local function controller_allow_metadata_inventory_take(pos, listname, index, stack, player) - if core.is_protected(pos, player:get_player_name()) then - return 0 - end - return stack:get_count() -end - -local function controller_formspec(pos, meta_current_state) - local formspec = - "size[8,8.5]".. - drawers.gui_bg.. - drawers.gui_bg_img.. - drawers.gui_slots.. - "label[0,0;" .. S("Current State: ") .. meta_current_state .. "]" .. - "list[current_name;src;3.5,1.75;1,1;]".. - "list[current_player;main;0,4.25;8,1;]".. - "list[current_player;main;0,5.5;8,3;8]".. - "listring[current_player;main]".. - "listring[current_name;src]".. - "listring[current_player;main]" - return formspec -end - -local function index_drawers(pos) - --[[ The pos parameter is the controllers position - - We store the item name as a string key and the value is a table with position x, - position y, position z, and visualid. Those are all strings as well with the - values assigned to them that way we don't need to worry about the ordering of - the table. The count and max count are not stored as those values have a high - potential of being outdated quickly. It's better to grab the values from the - drawer when needed so you know you are working with accurate numbers. - - Indexing starts on the row (meaning same y coordinate) of the controller on the - adjacent sides and moves each column on the same y level. A row is searched until - it either hits the max length setting size or until a node that isn't a drawer is - indexed and then moves up on (y coordinate increases by 1) and starts the process - until the y coord is reaches the max height or until a node that isn't a drawer - is indexed and at the point it stops indexing all together. This makes it so all - drawers need to be next to each other on the rows without spacing or other blocks - in between. ]]-- - - local drawers_table_index = {} - local x_or_z_axis = 1 - - -- Variables for managing the max length and max height that is searched in each adjacent direction from the controller - -- These could potentially be exposed to the user through the formspec, allowed to bigger with upgrades, etc - local max_search_length = 8 - local max_search_height = 8 - - -- Index the x axis and z axis in both the positive and negative directions - for x_z = 1,4 do - for y = 1,max_search_height do - for x_or_z = 1,max_search_length do - -- x_z if for controlling which axis and direction is being searched - -- x_or_z is for the column in each row that is searched - -- x_or_z_axis is used for breaking out of the loop if the first block searched in a row is not a drawer - x_or_z_axis = x_or_z - local drawer_pos - -- If x_z is 1, we check the positive x axis - if x_z == 1 then - drawer_pos = {x = pos.x + x_or_z, y = pos.y + y - 1, z = pos.z} - -- If x_z is 2, we check the negative x axis - elseif x_z == 2 then - drawer_pos = {x = pos.x - x_or_z, y = pos.y + y - 1, z = pos.z} - -- If x_z is 3, we check the positive z axis - elseif x_z == 3 then - drawer_pos = {x = pos.x, y = pos.y + y - 1, z = pos.z + x_or_z} - -- If x_z is 4, we check the negative z axis - elseif x_z == 4 then - drawer_pos = {x = pos.x, y = pos.y + y - 1, z = pos.z - x_or_z} - end - local drawer_meta = core.get_meta(drawer_pos) - local drawer_node = core.get_node(drawer_pos) - - -- There might be a better way to know if the node is a drawer other than matching a string in the node name - -- Can't trust metadata only in case another mod has a block with the same metadata strings - if string.match(drawer_node.name, 'drawers:') and drawer_node.name ~= "drawers:controller" and drawer_meta ~= nil then - for i = 0,4 do - -- This is needed for the special case where drawers that store one item don't have an id appended to them - local visualid = i - if i == 0 then - visualid = "" - end - local drawer_meta_name = drawer_meta:get_string("name" .. visualid) - local drawer_meta_entity_infotext = drawer_meta:get_string("entity_infotext" .. visualid) - -- Only one empty drawer needs to be indexed because everything is indexed again when an item isn't found in the index - if drawer_meta_name == "" and not drawers_table_index["empty"] and drawer_meta_entity_infotext ~= "" then - drawers_table_index["empty"] = {drawer_pos_x = drawer_pos.x, drawer_pos_y = drawer_pos.y, drawer_pos_z = drawer_pos.z, visualid = visualid} - elseif drawer_meta_name ~= "" then - -- If we already indexed this item previously, check which drawer has the most space and have that one be the one indexed - if drawers_table_index[drawer_meta_name] then - local indexed_drawer_meta = core.get_meta({x = drawers_table_index[drawer_meta_name]["drawer_pos_x"], y = drawers_table_index[drawer_meta_name]["drawer_pos_y"], z = drawers_table_index[drawer_meta_name]["drawer_pos_z"]}) - local indexed_drawer_meta_count = indexed_drawer_meta:get_int("count" .. drawers_table_index[drawer_meta_name]["visualid"]) - local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int("max_count" .. drawers_table_index[drawer_meta_name]["visualid"]) - local drawer_meta_count = drawer_meta:get_int("count" .. visualid) - local drawer_meta_max_count = drawer_meta:get_int("max_count" .. visualid) - -- If the already indexed drawer has less space, we override the table index for that item with the new drawer - if indexed_drawer_meta_max_count - indexed_drawer_meta_count < drawer_meta_max_count - drawer_meta_count then - drawers_table_index[drawer_meta_name] = {drawer_pos_x = drawer_pos.x, drawer_pos_y = drawer_pos.y, drawer_pos_z = drawer_pos.z, visualid = visualid} - end - else - drawers_table_index[drawer_meta_name] = {drawer_pos_x = drawer_pos.x, drawer_pos_y = drawer_pos.y, drawer_pos_z = drawer_pos.z, visualid = visualid} - end - -- If the drawer contained something and was a drawer type that only holds one item, stop the loop as there is no need to search through other drawer types - if i == 0 then - break - end - end - end - -- If the node isn't a drawer or doesn't have metadata, we break the loop to stop searching the row - else - break - end - end - -- If we break out of the above loop while x or z is 1, it means the first block searched in a row did not contain a drawer. - -- All searching for an axis is stopped when a row starts with a non-drawer. - if x_or_z_axis == 1 then - break - end - end - end - - return drawers_table_index -end - -local function controller_node_timer(pos, elapsed) - -- Inizialize metadata - local meta = core.get_meta(pos) - local meta_current_state = meta:get_string("current_state") - local meta_times_ran_while_jammed = meta:get_float("times_ran_while_jammed") - local meta_jammed_item_name = meta:get_string("jammed_item_name") - local inv = meta:get_inventory() - local src = inv:get_stack("src", 1) - local src_name = src:get_name() - - --[[ There are four scenarios for the item slot in the controller. - 1: No item is in the controller. - 2: Item is not stackable. - 3. Item is allowed and there is either an existing drawer for that item with room or an empty drawer. - 4: Item is allowed, but there is no room. - - There are three different possibilities for "current_state". - 1: "running" which means means it's operating normally. - 2: "stopped" meaning the controller makes no attempt to put in the item possibly due to being unallowed for various reasons. - 3: "jammed" meaning the item is allowed in to drawers, but there was no space to deposit it last time it ran. ]]-- - - --[[ If current state is jammed, the item that jammed it is the same item in the - src inv slot, and the amount of times ran while jammed is 8 or higher, we - set the current state to stopped. Will possibly want to make an option in the - formspec to ignore this an continue running if the user plans on using the - system in a way that may cause frequent jams making it a hassle to manually - clear it each time ]]-- - if meta_current_state == "jammed" and meta_jammed_item_name == src_name and meta_times_ran_while_jammed >= 8 then - meta:set_string("current_state", "stopped") - meta:set_string("formspec", controller_formspec(pos, S("Stopped"))) - return true - end - - -- If current state is stopped, and the item that jammed it is the same item in the src inv slot, we don't do anything - if meta_current_state == "stopped" and meta_jammed_item_name == src_name then - return true - end - - -- If current state is stopped, and the item that jammed it is not the same item in the src inv slot, we set the current state to running and clear the jam counter - if meta_current_state == "stopped" and meta_jammed_item_name ~= src_name then - meta:set_string("current_state", "running") - meta:set_string("formspec", controller_formspec(pos, S("Running"))) - meta:set_float("times_ran_while_jammed", 0) - end - - -- If no item is in the controller, nothing is searched and current_state is set to running and no jams - if inv:is_empty("src") then - meta:set_string("current_state", "running") - meta:set_string("formspec", controller_formspec(pos, S("Running"))) - meta:set_float("times_ran_while_jammed", 0) - return true - end - - -- If a non stackable item is in the controller, such as a written book, set the current_state to stopped because they are not allowed in drawers - if src:get_stack_max() == 1 then - meta:set_string("current_state", "stopped") - meta:set_string("formspec", controller_formspec(pos, S("Stopped"))) - meta:set_string("jammed_item_name", src_name) - meta:set_float("times_ran_while_jammed", 1) - return true - end - - -- If the index has not been created, the item isn't in the index, the item in the drawer is no longer the same item in the index, or the item is in the index but it's full, run the index_drawers function - local drawers_table_index = core.deserialize(meta:get_string("drawers_table_index")) - -- If the index has not been created - if not drawers_table_index then - drawers_table_index = index_drawers(pos) - meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) - -- If the item isn't in the index - elseif not drawers_table_index[src_name] then - drawers_table_index = index_drawers(pos) - meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) - -- If the item is in the index but either the name that was indexed is not the same as what is currently in the drawer or the drawer is full - elseif drawers_table_index[src_name] then - local visualid = drawers_table_index[src_name]["visualid"] - local indexed_drawer_meta = core.get_meta({x = drawers_table_index[src_name]["drawer_pos_x"], y = drawers_table_index[src_name]["drawer_pos_y"], z = drawers_table_index[src_name]["drawer_pos_z"]}) - local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) - local indexed_drawer_meta_count = indexed_drawer_meta:get_int("count" .. visualid) - local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int("max_count" .. visualid) - if indexed_drawer_meta_name ~= src_name or indexed_drawer_meta_count >= indexed_drawer_meta_max_count then - drawers_table_index = index_drawers(pos) - meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) - end - end - - -- This might not be needed, but my concern is if the above indexing takes enough time, there could be a "race condition" where the item in the src inventory is no longer the same item when we checked before or the quantity of the items changed so I'm having it grab the item stack again just in case - -- If a race condition does occur, items could be lost or duplicated - src = inv:get_stack("src", 1) - src_name = src:get_name() - local src_count = src:get_count() - local src_stack_max = src:get_stack_max() - - -- At this point, the item either was in the index or everything was reindexed so we check again - -- If there is a drawer with the item and it isn't full, we will put the items we can in to it - if drawers_table_index[src_name] then - local indexed_drawer_pos = {x = drawers_table_index[src_name]["drawer_pos_x"], y = drawers_table_index[src_name]["drawer_pos_y"], z = drawers_table_index[src_name]["drawer_pos_z"]} - local visualid = drawers_table_index[src_name]["visualid"] - local indexed_drawer_meta = core.get_meta(indexed_drawer_pos) - local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) - local indexed_drawer_meta_count = indexed_drawer_meta:get_int("count" .. visualid) - local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int("max_count" .. visualid) - -- If the the item in the drawer is the same as the one we are trying to store, the drawer is not full, and the drawer entity is loaded, we will put the items in the drawer - if indexed_drawer_meta_name == src_name and indexed_drawer_meta_count < indexed_drawer_meta_max_count and drawers.drawer_visuals[core.serialize(indexed_drawer_pos)] then - local leftover = drawers.drawer_insert_object(indexed_drawer_pos, nil, src, nil) - inv:set_stack("src", 1, leftover) - -- Set the controller metadata - meta:set_string("current_state", "running") - meta:set_string("formspec", controller_formspec(pos, S("Running"))) - meta:set_float("times_ran_while_jammed", 0) - else - meta:set_string("current_state", "jammed") - meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) - meta:set_string("jammed_item_name", src_name) - meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) - end - elseif drawers_table_index["empty"] then - local indexed_drawer_pos = {x = drawers_table_index["empty"]["drawer_pos_x"], y = drawers_table_index["empty"]["drawer_pos_y"], z = drawers_table_index["empty"]["drawer_pos_z"]} - local visualid = drawers_table_index["empty"]["visualid"] - local indexed_drawer_meta = core.get_meta(indexed_drawer_pos) - local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) - -- If the drawer is still empty and the drawer entity is loaded, we will put the items in the drawer - if indexed_drawer_meta_name == "" and drawers.drawer_visuals[core.serialize(indexed_drawer_pos)] then - local leftover = drawers.drawer_insert_object(indexed_drawer_pos, nil, src, nil) - inv:set_stack("src", 1, leftover) - -- Add the item to the drawers table index and set the empty one to nil - drawers_table_index["empty"] = nil - drawers_table_index[src_name] = {drawer_pos_x = indexed_drawer_pos.x, drawer_pos_y = indexed_drawer_pos.y, drawer_pos_z = indexed_drawer_pos.z, visualid = visualid} - -- Set the controller metadata - meta:set_string("current_state", "running") - meta:set_string("formspec", controller_formspec(pos, S("Running"))) - meta:set_float("times_ran_while_jammed", 0) - meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) - else - meta:set_string("current_state", "jammed") - meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) - meta:set_string("jammed_item_name", src_name) - meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) - end - else - meta:set_string("current_state", "jammed") - meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) - meta:set_string("jammed_item_name", src_name) - meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) - end - - return true -end - --- Set the controller definition using a table to allow for pipeworks and potentially other mod support -local controller_def = {} --- MCL2 requires a few different groups and parameters that default does not -if core.get_modpath("mcl_core") and mcl_core then - controller_def.groups = {pickaxey = 1, stone = 1, building_block = 1, material_stone = 1} - controller_def._mcl_blast_resistance = 30 - controller_def._mcl_hardness = 1.5 -else - controller_def.groups = {cracky = 3, level = 2} -end -controller_def.description = S("Drawer Controller") -controller_def.tiles = {"drawer_controller_top_bottom.png", "drawer_controller_top_bottom.png", "drawer_controller_side.png", "drawer_controller_side.png", "drawer_controller_side.png", "drawer_controller_side.png"} -controller_def.can_dig = controller_can_dig -controller_def.on_construct = function(pos) - local meta = core.get_meta(pos) - local inv = meta:get_inventory() - inv:set_size('src', 1) - meta:set_string("current_state", "running") - meta:set_float("times_ran_while_jammed", 0) - meta:set_string("jammed_item_name", "") - meta:set_string("drawers_table_index", "") - meta:set_string("formspec", controller_formspec(pos, S("Running"))) - local timer = core.get_node_timer(pos) - timer:start(controller_interval) -end -controller_def.on_blast = function(pos) - local drops = {} - default.get_inventory_drops(pos, "src", drops) - drops[#drops+1] = "drawers:controller" - core.remove_node(pos) - return drops -end -controller_def.on_timer = controller_node_timer -controller_def.allow_metadata_inventory_put = controller_allow_metadata_inventory_put -controller_def.allow_metadata_inventory_move = controller_allow_metadata_inventory_move -controller_def.allow_metadata_inventory_take = controller_allow_metadata_inventory_take - --- Mostly copied from the drawers in the drawer mod to add pipeworks support -if core.get_modpath("pipeworks") and pipeworks then - controller_def.groups.tubedevice = 1 - controller_def.groups.tubedevice_receiver = 1 - controller_def.tube = controller_def.tube or {} - controller_def.tube.insert_object = function(pos, node, stack, tubedir) - local meta = core.get_meta(pos) - local inv = meta:get_inventory() - return inv:add_item("src", stack) - end - controller_def.tube.can_insert = function(pos, node, stack, tubedir) - local meta = core.get_meta(pos) - local inv = meta:get_inventory() - return inv:room_for_item("src", stack) - end - controller_def.tube.connect_sides = {left = 1, right = 1, back = 1, front = 1, - top = 1, bottom = 1} - controller_def.after_place_node = pipeworks.after_place - controller_def.after_dig_node = pipeworks.after_dig -end - -core.register_node('drawers:controller', controller_def) - --- Because the rest of the drawers mod doesn't have a hard depend on default, I changed the recipe to have an alternative -if core.get_modpath("default") and default then - core.register_craft({ - output = 'drawers:controller', - recipe = { - {'default:steel_ingot', 'default:diamond', 'default:steel_ingot'}, - {'default:tin_ingot', 'group:drawer', 'default:copper_ingot'}, - {'default:steel_ingot', 'default:diamond', 'default:steel_ingot'}, - } - }) -elseif core.get_modpath("mcl_core") and mcl_core then - core.register_craft({ - output = 'drawers:controller', - recipe = { - {'mcl_core:iron_ingot', 'mcl_core:diamond', 'mcl_core:iron_ingot'}, - {'mcl_core:gold_ingot', 'group:drawer', 'mcl_core:gold_ingot'}, - {'mcl_core:iron_ingot', 'mcl_core:diamond', 'mcl_core:iron_ingot'}, - } - }) -else - core.register_craft({ - output = 'drawers:controller', - recipe = { - {'group:stone', 'group:stone', 'group:stone'}, - {'group:stone', 'group:drawer', 'group:stone'}, - {'group:stone', 'group:stone', 'group:stone'}, - } - }) -end +--[[ +Minetest Mod Storage Drawers - A Mod adding storage drawers + +Copyright (C) 2017-2019 Linus Jahn +Copyright (C) 2018 isaiah658 + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]]-- + +--[[ The gist of how the drawers mod stores data is that there are entities +and the drawer node itself. The entities are needed to allow having multiple +drawers in one node. The entities and node each store metadata about the item +counts and such. It is necessary to change both at once otherwise in some cases +the entity values are used and in other cases the node metadata is used. + +The gist of how the controller works is this. The drawer controller scans the +adjacent tiles and puts the item names and other info such as coordinates and +the visualid of the entity in a table. That table is saved in the controllers +metadata. The table is used to help prevent needing to scan all the drawers to +deposit an item in certain situations. The table is only updated on an as needed +basis, not by a specific time/interval. Controllers that have no items will not +continue scanning drawers. ]]-- + +-- Load support for intllib. +local MP = core.get_modpath(core.get_current_modname()) +local S, NS = dofile(MP.."/intllib.lua") + +local default_loaded = core.get_modpath("default") and default +local mcl_loaded = core.get_modpath("mcl_core") and mcl_core +local pipeworks_loaded = core.get_modpath("pipeworks") and pipeworks + +local controller_interval = tonumber(core.setting_get("drawers_controller_interval")) or 7.0 + +local function controller_formspec(pos, meta_current_state) + local formspec = + "size[8,8.5]".. + drawers.gui_bg.. + drawers.gui_bg_img.. + drawers.gui_slots.. + "label[0,0;" .. S("Current State: ") .. meta_current_state .. "]" .. + "list[current_name;src;3.5,1.75;1,1;]".. + "list[current_player;main;0,4.25;8,1;]".. + "list[current_player;main;0,5.5;8,3;8]".. + "listring[current_player;main]".. + "listring[current_name;src]".. + "listring[current_player;main]" + return formspec +end + +local function controller_index_slot(pos, visualid) + return { + drawer_pos_x = pos.x, + drawer_pos_y = pos.y, + drawer_pos_z = pos.z, + visualid = visualid + } +end + +local function compare_pos(pos1, pos2) + return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z +end + +local function contains_pos(list, p) + for _,v in ipairs(list) do + if compare_pos(v, p) then + return true + end + end + return false +end + +-- iterator for iterating from 1 -> to +local function range(to) + local i = 0 + return function() + if i == to then + return nil + end + i = i + 1 + return i, i + end +end + +local function pos_in_range(pos1, pos2) + local diff = { + pos1.x - pos2.x, + pos1.y - pos2.y, + pos1.z - pos2.z + } + for _,v in ipairs(diff) do + if v < 0 then + v = v * -1 + end + if v > drawers.CONTROLLER_RANGE then + return false + end + end + return true +end + +local function add_drawer_to_inventory(controllerInventory, pos) + -- the number of slots is saved as drawer group + local slots = core.get_item_group(core.get_node(pos).name, "drawer") + if not slots then + return + end + + local meta = core.get_meta(pos) + if not meta then + return + end + + local i = 1 + while i <= slots do + -- nothing is appended in case the drawer has only one slot + local slot_id = "" + if slots ~= 1 then + slot_id = tostring(i) + end + + local item_id = meta:get_string("name" .. slot_id) + local drawer_meta_entity_infotext = meta:get_string("entity_infotext" .. slot_id) + + if item_id == "" and not controllerInventory["empty"] then + controllerInventory["empty"] = controller_index_slot(pos, slot_id) + elseif item_id ~= "" then + -- If we already indexed this item previously, check which drawer + -- has the most space and have that one be the one indexed + if controllerInventory[item_id] then + local indexed_drawer_meta = core.get_meta({ + x = controllerInventory[item_id]["drawer_pos_x"], + y = controllerInventory[item_id]["drawer_pos_y"], + z = controllerInventory[item_id]["drawer_pos_z"]} + ) + local indexed_drawer_meta_count = indexed_drawer_meta:get_int( + "count" .. controllerInventory[item_id]["visualid"]) + local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int( + "max_count" .. controllerInventory[item_id]["visualid"]) + + local drawer_meta_count = meta:get_int("count" .. slot_id) + local drawer_meta_max_count = meta:get_int("max_count" .. slot_id) + + -- If the already indexed drawer has less space, we override the table index for that item with the new drawer + if (indexed_drawer_meta_max_count - indexed_drawer_meta_count) + < (drawer_meta_max_count - drawer_meta_count) then + controllerInventory[item_id] = controller_index_slot(pos, slot_id) + end + else + controllerInventory[item_id] = controller_index_slot(pos, slot_id) + end + end + + i = i + 1 + 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) + end + end + + return foundPositions +end + +local function index_drawers(pos) + --[[ + The pos parameter is the controllers position + + We store the item name as a string key and the value is a table with position x, + position y, position z, and visualid. Those are all strings as well with the + values assigned to them that way we don't need to worry about the ordering of + the table. The count and max count are not stored as those values have a high + potential of being outdated quickly. It's better to grab the values from the + drawer when needed so you know you are working with accurate numbers. + ]] + + local controllerInventory = {} + for _,drawerPos in ipairs(find_connected_drawers(pos)) do + add_drawer_to_inventory(controllerInventory, drawerPos) + end + + return controllerInventory +end + +local function controller_node_timer(pos, elapsed) + -- Inizialize metadata + local meta = core.get_meta(pos) + local meta_current_state = meta:get_string("current_state") + local meta_times_ran_while_jammed = meta:get_float("times_ran_while_jammed") + local meta_jammed_item_name = meta:get_string("jammed_item_name") + local inv = meta:get_inventory() + local src = inv:get_stack("src", 1) + local src_name = src:get_name() + + --[[ + There are four scenarios for the item slot in the controller. + 1: No item is in the controller. + 2: Item is not stackable. + 3. Item is allowed and there is either an existing drawer for that item with room or an empty drawer. + 4: Item is allowed, but there is no room. + + There are three different possibilities for "current_state". + 1: "running" which means means it's operating normally. + 2: "stopped" meaning the controller makes no attempt to put in the item possibly due to being unallowed for various reasons. + 3: "jammed" meaning the item is allowed in to drawers, but there was no space to deposit it last time it ran. + ]] + + --[[ + If current state is jammed, the item that jammed it is the same item in the + src inv slot, and the amount of times ran while jammed is 8 or higher, we + set the current state to stopped. Will possibly want to make an option in the + formspec to ignore this an continue running if the user plans on using the + system in a way that may cause frequent jams making it a hassle to manually + clear it each time + ]] + if meta_current_state == "jammed" and meta_jammed_item_name == src_name and meta_times_ran_while_jammed >= 2 then + meta:set_string("current_state", "stopped") + meta:set_string("formspec", controller_formspec(pos, S("Stopped"))) + return true + end + + -- If current state is stopped, and the item that jammed it is the same + -- item in the src inv slot, we don't do anything + if meta_current_state == "stopped" and meta_jammed_item_name == src_name then + return true + end + + -- If current state is stopped, and the item that jammed it is not the + -- same item in the src inv slot, we set the current state to running and + -- clear the jam counter. + if meta_current_state == "stopped" and meta_jammed_item_name ~= src_name then + meta:set_string("current_state", "running") + meta:set_string("formspec", controller_formspec(pos, S("Running"))) + meta:set_float("times_ran_while_jammed", 0) + end + + -- If no item is in the controller, nothing is searched and current_state + -- is set to running and no jams. + if inv:is_empty("src") then + meta:set_string("current_state", "running") + meta:set_string("formspec", controller_formspec(pos, S("Running"))) + meta:set_float("times_ran_while_jammed", 0) + return true + end + + -- If a non stackable item is in the controller, such as a written book, + -- set the current_state to stopped because they are not allowed in drawers + if src:get_stack_max() == 1 then + meta:set_string("current_state", "stopped") + meta:set_string("formspec", controller_formspec(pos, S("Stopped"))) + meta:set_string("jammed_item_name", src_name) + meta:set_float("times_ran_while_jammed", 1) + return true + end + + -- If the index has not been created, the item isn't in the index, the + -- item in the drawer is no longer the same item in the index, or the item + -- is in the index but it's full, run the index_drawers function. + local drawers_table_index = core.deserialize(meta:get_string("drawers_table_index")) + + -- If the index has not been created + if not drawers_table_index then + drawers_table_index = index_drawers(pos) + meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) + + -- If the item isn't in the index + elseif not drawers_table_index[src_name] then + drawers_table_index = index_drawers(pos) + meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) + + -- If the item is in the index but either the name that was indexed is not + -- the same as what is currently in the drawer or the drawer is full + elseif drawers_table_index[src_name] then + local visualid = drawers_table_index[src_name]["visualid"] + local indexed_drawer_meta = core.get_meta({x = drawers_table_index[src_name]["drawer_pos_x"], y = drawers_table_index[src_name]["drawer_pos_y"], z = drawers_table_index[src_name]["drawer_pos_z"]}) + local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) + local indexed_drawer_meta_count = indexed_drawer_meta:get_int("count" .. visualid) + local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int("max_count" .. visualid) + if indexed_drawer_meta_name ~= src_name or indexed_drawer_meta_count >= indexed_drawer_meta_max_count then + drawers_table_index = index_drawers(pos) + meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) + end + end + + -- This might not be needed, but my concern is if the above indexing takes + -- enough time, there could be a "race condition" where the item in the src + -- inventory is no longer the same item when we checked before or the + -- quantity of the items changed so I'm having it grab the item stack again + -- just in case. + -- If a race condition does occur, items could be lost or duplicated + src = inv:get_stack("src", 1) + src_name = src:get_name() + local src_count = src:get_count() + local src_stack_max = src:get_stack_max() + + -- At this point, the item either was in the index or everything was reindexed so we check again + -- If there is a drawer with the item and it isn't full, we will put the items we can in to it + if drawers_table_index[src_name] then + local indexed_drawer_pos = {x = drawers_table_index[src_name]["drawer_pos_x"], y = drawers_table_index[src_name]["drawer_pos_y"], z = drawers_table_index[src_name]["drawer_pos_z"]} + local visualid = drawers_table_index[src_name]["visualid"] + local indexed_drawer_meta = core.get_meta(indexed_drawer_pos) + local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) + local indexed_drawer_meta_count = indexed_drawer_meta:get_int("count" .. visualid) + local indexed_drawer_meta_max_count = indexed_drawer_meta:get_int("max_count" .. visualid) + -- If the the item in the drawer is the same as the one we are trying to store, the drawer is not full, and the drawer entity is loaded, we will put the items in the drawer + if indexed_drawer_meta_name == src_name and indexed_drawer_meta_count < indexed_drawer_meta_max_count and drawers.drawer_visuals[core.serialize(indexed_drawer_pos)] then + local leftover = drawers.drawer_insert_object(indexed_drawer_pos, nil, src, nil) + inv:set_stack("src", 1, leftover) + -- Set the controller metadata + meta:set_string("current_state", "running") + meta:set_string("formspec", controller_formspec(pos, S("Running"))) + meta:set_float("times_ran_while_jammed", 0) + else + meta:set_string("current_state", "jammed") + meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) + meta:set_string("jammed_item_name", src_name) + meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) + end + elseif drawers_table_index["empty"] then + local indexed_drawer_pos = {x = drawers_table_index["empty"]["drawer_pos_x"], y = drawers_table_index["empty"]["drawer_pos_y"], z = drawers_table_index["empty"]["drawer_pos_z"]} + local visualid = drawers_table_index["empty"]["visualid"] + local indexed_drawer_meta = core.get_meta(indexed_drawer_pos) + local indexed_drawer_meta_name = indexed_drawer_meta:get_string("name" .. visualid) + -- If the drawer is still empty and the drawer entity is loaded, we will put the items in the drawer + if indexed_drawer_meta_name == "" and drawers.drawer_visuals[core.serialize(indexed_drawer_pos)] then + local leftover = drawers.drawer_insert_object(indexed_drawer_pos, nil, src, nil) + inv:set_stack("src", 1, leftover) + -- Add the item to the drawers table index and set the empty one to nil + drawers_table_index["empty"] = nil + drawers_table_index[src_name] = {drawer_pos_x = indexed_drawer_pos.x, drawer_pos_y = indexed_drawer_pos.y, drawer_pos_z = indexed_drawer_pos.z, visualid = visualid} + -- Set the controller metadata + meta:set_string("current_state", "running") + meta:set_string("formspec", controller_formspec(pos, S("Running"))) + meta:set_float("times_ran_while_jammed", 0) + meta:set_string("drawers_table_index", core.serialize(drawers_table_index)) + else + meta:set_string("current_state", "jammed") + meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) + meta:set_string("jammed_item_name", src_name) + meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) + end + else + meta:set_string("current_state", "jammed") + meta:set_string("formspec", controller_formspec(pos, S("Jammed"))) + meta:set_string("jammed_item_name", src_name) + meta:set_float("times_ran_while_jammed", meta_times_ran_while_jammed + 1) + end + + return true +end + +local function controller_can_dig(pos, player) + local meta = core.get_meta(pos); + local inv = meta:get_inventory() + return inv:is_empty("src") +end + +local function controller_on_construct(pos) + local meta = core.get_meta(pos) + meta:set_string("current_state", "running") + meta:set_float("times_ran_while_jammed", 0) + meta:set_string("jammed_item_name", "") + meta:set_string("drawers_table_index", "") + meta:set_string("formspec", controller_formspec(pos, S("Running"))) + + meta:get_inventory():set_size("src", 1) + + core.get_node_timer(pos):start(controller_interval) +end + +local function controller_on_blast(pos) + local drops = {} + default.get_inventory_drops(pos, "src", drops) + drops[#drops+1] = "drawers:controller" + core.remove_node(pos) + return drops +end + +local function controller_allow_metadata_inventory_put(pos, listname, index, stack, player) + if core.is_protected(pos, player:get_player_name()) then + return 0 + end + if listname == "src" then + return stack:get_count() + end +end + +local function controller_allow_metadata_inventory_move(pos, from_list, from_index, to_list, to_index, count, player) + local meta = core.get_meta(pos) + local inv = meta:get_inventory() + local stack = inv:get_stack(from_list, from_index) + return controller_allow_metadata_inventory_put(pos, to_list, to_index, stack, player) +end + +local function controller_allow_metadata_inventory_take(pos, listname, index, stack, player) + if core.is_protected(pos, player:get_player_name()) then + return 0 + end + return stack:get_count() +end + +-- Registers the drawer controller +local function register_controller() + -- Set the controller definition using a table to allow for pipeworks and + -- potentially other mod support + local def = {} + + def.description = S("Drawer Controller") + def.drawtype = "nodebox" + def.node_box = { type = "fixed", fixed = drawers.node_box_simple } + def.collision_box = { type = "regular" } + def.selection_box = { type = "regular" } + def.paramtype = "light" + def.paramtype2 = "facedir" + def.legacy_facedir_simple = true + + -- add pipe connectors, if pipeworks is enabled + if pipeworks_loaded then + def.tiles = { + "drawers_controller_top.png^pipeworks_tube_connection_metallic.png", + "drawers_controller_top.png^pipeworks_tube_connection_metallic.png", + "drawers_controller_side.png^pipeworks_tube_connection_metallic.png", + "drawers_controller_side.png^pipeworks_tube_connection_metallic.png", + "drawers_controller_top.png^pipeworks_tube_connection_metallic.png", + "drawers_controller_front.png" + } + else + def.tiles = { + "drawers_controller_top.png", + "drawers_controller_top.png", + "drawers_controller_side.png", + "drawers_controller_side.png", + "drawers_controller_top.png", + "drawers_controller_front.png" + } + end + + -- MCL2 requires a few different groups and parameters that MTG does not + if mcl_loaded then + def.groups = { + pickaxey = 1, stone = 1, building_block = 1, material_stone = 1 + } + def._mcl_blast_resistance = 30 + def._mcl_hardness = 1.5 + else + def.groups = { + cracky = 3, level = 2 + } + end + + def.can_dig = controller_can_dig + def.on_construct = controller_on_construct + def.on_blast = controller_on_blast + def.on_timer = controller_node_timer + + def.allow_metadata_inventory_put = controller_allow_metadata_inventory_put + def.allow_metadata_inventory_move = controller_allow_metadata_inventory_move + def.allow_metadata_inventory_take = controller_allow_metadata_inventory_take + + if pipeworks_loaded then + def.groups.tubedevice = 1 + def.groups.tubedevice_receiver = 1 + + def.tube = {} + def.tube.insert_object = function(pos, node, stack, tubedir) + return core.get_meta(pos):get_inventory():add_item("src", stack) + end + + def.tube.can_insert = function(pos, node, stack, tubedir) + return core.get_meta(pos):get_inventory():room_for_item("src", stack) + end + + def.tube.connect_sides = { + left = 1, right = 1, back = 1, top = 1, bottom = 1 + } + + def.after_place_node = pipeworks.after_place + def.after_dig_node = pipeworks.after_dig + end + + core.register_node("drawers:controller", def) +end + +-- register drawer controller +register_controller() + +if default_loaded then + core.register_craft({ + output = 'drawers:controller', + recipe = { + {'default:steel_ingot', 'default:diamond', 'default:steel_ingot'}, + {'default:tin_ingot', 'group:drawer', 'default:copper_ingot'}, + {'default:steel_ingot', 'default:diamond', 'default:steel_ingot'}, + } + }) +elseif mcl_loaded then + core.register_craft({ + output = 'drawers:controller', + recipe = { + {'mcl_core:iron_ingot', 'mcl_core:diamond', 'mcl_core:iron_ingot'}, + {'mcl_core:gold_ingot', 'group:drawer', 'mcl_core:gold_ingot'}, + {'mcl_core:iron_ingot', 'mcl_core:diamond', 'mcl_core:iron_ingot'}, + } + }) +else + -- Because the rest of the drawers mod doesn't have a hard depend on + -- default, I changed the recipe to have an alternative + core.register_craft({ + output = 'drawers:controller', + recipe = { + {'group:stone', 'group:stone', 'group:stone'}, + {'group:stone', 'group:drawer', 'group:stone'}, + {'group:stone', 'group:stone', 'group:stone'}, + } + }) +end diff --git a/lua/visual.lua b/lua/visual.lua index 2843574..744fc07 100755 --- a/lua/visual.lua +++ b/lua/visual.lua @@ -1,7 +1,7 @@ --[[ Minetest Mod Storage Drawers - A Mod adding storage drawers -Copyright (C) 2017 LNJ +Copyright (C) 2017-2019 Linus Jahn MIT License diff --git a/textures/drawer_controller_side.png b/textures/drawer_controller_side.png deleted file mode 100644 index 5f9f7ee8a3b0389ffaf29216ed8ff631154cb2ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 577 zcmV-H0>1r;P)WdKuQbRaP^AX8;$;BPD4VjyV!?15 zOC%Bs@SV@+<#IWn&u=yxklXFnXf$wq->=nbkH^E#WHMyXfPMtp?e=oHBu|YyosRKs ztJR7!i@g1Q4|^vx0&1B~r|b2)7n&OyY{CEWkxG%lp2iX&Lgb*KO&tWL(|DBpeQhLZN`NCInKc)bF3d<~JA&X!es8m$_cA=plUsa@EOXVj(LY zEXU*V@p!};4u>FQj7B4d2%#PjSuNUd4N0NLW+oGg#Ucn9LZr=qSv40(4l_jq#19SR za=CC-Kns+)S`}jS7Ig`)N)T{12t7=;wcG7#`J(;P!bMM^qn6M7>w13yHpWm5fn2wt P00000NkvXXu0mjf*W>a0 diff --git a/textures/drawer_controller_top_bottom.png b/textures/drawer_controller_top_bottom.png deleted file mode 100644 index 0afb826e85054e05823e02b79a533dcfbe9a23a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 490 zcmVWdKuQbRaP^AX8;uq+D!$FA!xm&>%_^?D7%a5|md@3*1$csw{0Q1CWHQDl#bK^TU4 zo|~$w>bkz&Zlt(IR^=o~nx+YYz)}=N!Egii`@IT$+qRA4m@@Pn$8ppJf-KAWzBiQ5 z=QADfY3{he&hh^6d_Kt=`pUArUauNrTQyBGl+?ECK61%Ith6%xXAT}3x(|ng%dOp9 z4^)!TtF5?%1{O%u)OE%c0O0j~-#~+W`XCcGd=g;&QNWKQxx-dOTe4(>n)otV87OG` z+6uRIUA7Qg>GsE=_G_y<*9LkKZeHH(uod}Q20g(Bc?I;wl*F6WR`%g4e^E^CgZp`l g$Kw&*{(qnM2L`DaiZUvZ$N&HU07*qoM6N<$f?aFOL;wH) diff --git a/textures/drawers_controller_front.png b/textures/drawers_controller_front.png new file mode 100644 index 0000000000000000000000000000000000000000..37240900bd41878bc108a127efb4b57ce0c4fe06 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAbV^(!N}P*Q6H7Al^Atidb5j|D z6H62fjTM4Z3lxkD3{4aaO|6VftPD&PbPY@m4Cb&L)dlKfFY)wsWxvnN&Y~f(Nh3fJ zD8yOd5n0T@!1V!y8J*Uc%m50qq&xaLGB9lH=l+w(3gjR4ba4!k2#@vO-F4W3=gR&Y z0%Dsj-aJ&;VYOuEFQ)5~>vkxeoh9Jc`<;K=&11cu3*DC58Q#)0UclZw<06CK<>Q54 z_WeI{Qr?fzK=9}dX(hp>40=ZI&#ci=lInl(Klf+!=Uw-D&+!?o)-7+ATG8J4=Fp7N z^~zrbgTe~ HDWM4fVhfB< literal 0 HcmV?d00001 diff --git a/textures/drawers_controller_side.png b/textures/drawers_controller_side.png new file mode 100644 index 0000000000000000000000000000000000000000..8645f6e26d8de8fc53c1dd618b99a3dc3ebbe1ca GIT binary patch literal 374 zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAbV^(!N}P*Q6H7Al^Atidb5j|D z6H62fjTM4Z3lxkD3{4aaO|6VftPD&PbPY@m4Cb&L)dlKfFY)wsWxvPCD{RPcczO8= zpb%$)M`SSr1Gf+eGhVt|_XjA*lJ4m1$iT3%pZiZDE0BNI)5S4FB0RSLpx5C50oUj8 zQWF#uRfRaexz~tHcb(tb)-E(h#7fI!H`hOhu+2M9|8!ZdZ*9)Z{QcZa=h)l#*1le) zWfmKK_m24$&SI~ZRx=`{v?jk=vuYWG&f1l~9OUQgw>jT%o9p#Tty^wK+MBaG7DT)8 z#~U)cPi>mT__w;k@Q=$+v&cZ*8&BKf?OuOyDBZL};LJQ9vk*Ti|J(6r=EtwEo-1?O z%<6!2TJ?rx*2k9^7V-9p@+!()k$apPvmxuB(=mq3sZTdWn3e4O|EvCGS%r1%I*$WD Pk1=?<`njxgN@xNADXE9w literal 0 HcmV?d00001 diff --git a/textures/drawers_controller_top.png b/textures/drawers_controller_top.png new file mode 100644 index 0000000000000000000000000000000000000000..774bc6a2fe399dfcbf5c6b0b8c9a735af9b27b0a GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAbV^(!N}P*Q6H7Al^Atidb5j|D z6H62fjTM4Z3lxkD3{4aaO|6VftPD&PbPY@m4Cb&L)dlKfFY)wsWxvPCE3BfrU(9tj zP>8d@BeIx*fm;ZK886+f`vVkYNq6*hWMJ6X&;2Kn706%Y>Eak75gyxb$k*&3!2Dfb z!IfjKgNw`Z536zmw%wBd(7gK2jxWN&F_VDMoUo`rD7EnJVA0J!Gp? zwE1Ksn}dR8{MtPpW?d`|*_*03Z6}E=KIWK4cCRjN zR(YCune~rW!P+pVh88x@o|9RV<|?c#-eS35yP;p~RSomY=Bqt*XYMK&ZT|l||J%OC Vecr3@>j7QM;OXk;vd$@?2>|kbbA