microexpansion/modules/storage/cterminal.lua

757 lines
25 KiB
Lua
Raw Normal View History

2023-12-31 00:52:43 +01:00
-- crafting terminal
-- microexpansion/cterminal.lua
-- TODO: Bugs, can't craft sticks, oil extract by using the
-- output. Does work when updating the recipe. Groups are hanky. We
-- only use the last recipe registered. Would be nice to be able to
-- cycle trough them. We handle this by merely requiring the user to
-- select the input recipe they want.
-- TODO: Bugs in original, if you remove controller, this wipes all drives
-- Spacing sucks.
-- The search list doesn't update when main updates or when autocrafting updates.
local autocrafterCache = {} -- caches some recipe data to avoid to call the slow function minetest.get_craft_result() every second
local me = microexpansion
local pipeworks_enabled = minetest.get_modpath("pipeworks") and true or false
-- [me chest] Get formspec
local function chest_formspec(pos, start_id, listname, page_max, q)
local list
local page_number = ""
local buttons = ""
local query = q or ""
local net,cpos = me.get_connected_network(pos)
if cpos then
local inv = net:get_inventory()
if listname and (inv:get_size(listname) > 0 or net:get_item_capacity() > 0) then
local ctrlinvname = net:get_inventory_name()
if listname == "main" then
list = "list[detached:"..ctrlinvname..";"
.. listname .. ";0,0.3;8,4;" .. (start_id - 1) .. "]"
else
list = "list[context;" .. listname .. ";0,0.3;8,4;" .. (start_id - 1) .. "]"
end
if minetest.get_modpath("i3") then
list = list .. [[
list[current_player;main;0,8.5;9,4;]
]]
else
list = list .. [[
list[current_player;main;0,8.5;8,1;]
list[current_player;main;0,9.73;8,3;8]
]]
end
list = list .. [[
list[context;recipe;0.22,5.22;3,3;]
list[context;output;4,6.22;1,1;]
]]
list = list .. [[
listring[current_player;main]
listring[detached:]]..ctrlinvname..[[;main]
listring[current_player;main]
listring[context;recipe]
listring[current_player;main]
listring[context;output]
listring[current_player;main]
]]
buttons = [[
button[3.56,4.35;1.8,0.9;tochest;To Drive]
tooltip[tochest;Move everything from your inventory to the ME network.]
button[5.4,4.35;0.8,0.9;prev;<]
button[7.25,4.35;0.8,0.9;next;>]
tooltip[prev;Previous]
tooltip[next;Next]
field[0.29,4.6;2.2,1;filter;;]]..query..[[]
button[2.1,4.5;0.8,0.5;search;?]
button[2.75,4.5;0.8,0.5;clear;X]
tooltip[search;Search]
tooltip[clear;Reset]
field[6,5.42;2,1;autocraft;;1]
tooltip[autocraft;Number of items to Craft]
]]
else
list = "label[3,2;" .. minetest.colorize("red", "No connected storage!") .. "]"
end
else
list = "label[3,2;" .. minetest.colorize("red", "No connected network!") .. "]"
end
if page_max then
page_number = "label[6.15,4.5;" .. math.floor((start_id / 32)) + 1 ..
"/" .. page_max .."]"
end
return [[
size[9,12.5]
]]..
microexpansion.gui_bg ..
microexpansion.gui_slots ..
list ..
[[
label[0,-0.23;ME Crafting Terminal]
field_close_on_enter[filter;false]
field_close_on_enter[autocraft;false]
]]..
page_number ..
buttons
end
local function update_chest(pos,_,ev)
--for now all events matter
local net = me.get_connected_network(pos)
local meta = minetest.get_meta(pos)
if net == nil then
meta:set_int("page", 1)
meta:set_string("formspec", chest_formspec(pos, 1))
return
end
local size = net:get_item_capacity()
local page_max = me.int_to_pagenum(size) + 1
meta:set_string("inv_name", "main")
meta:set_string("formspec", chest_formspec(pos, 1, "main", page_max))
end
-- From pipeworks/autocrafter.lua
local function count_index(invlist)
local index = {}
for _, stack in pairs(invlist) do
if not stack:is_empty() then
local stack_name = stack:get_name()
index[stack_name] = (index[stack_name] or 0) + stack:get_count()
end
end
return index
end
-- From pipeworks/autocrafter.lua
function me.get_craft(pos, inventory, hash)
local hash = hash or minetest.hash_node_position(pos)
local craft = autocrafterCache[hash]
if not craft then
local recipe = inventory:get_list("recipe")
for i = 1, 9 do
if recipe[i]:get_count() > 1 then
recipe[i] = ItemStack(recipe[i]:get_name())
end
end
local output, decremented_input = minetest.get_craft_result({method = "normal", width = 3, items = recipe})
craft = {recipe = recipe, consumption=count_index(recipe), output = output, decremented_input = decremented_input}
autocrafterCache[hash] = craft
end
return craft
end
-- From pipeworks/autocrafter.lua
-- note, that this function assumes allready being updated to virtual items
-- and doesn't handle recipes with stacksizes > 1
local function after_recipe_change(pos, inventory)
local meta = minetest.get_meta(pos)
-- if we emptied the grid, there's no point in keeping it running or cached
if inventory:is_empty("recipe") then
autocrafterCache[minetest.hash_node_position(pos)] = nil
inventory:set_stack("output", 1, "")
return
end
local recipe = inventory:get_list("recipe")
local hash = minetest.hash_node_position(pos)
local craft = autocrafterCache[hash]
if craft then
-- check if it changed
local cached_recipe = craft.recipe
for i = 1, 9 do
if recipe[i]:get_name() ~= cached_recipe[i]:get_name() then
autocrafterCache[hash] = nil -- invalidate recipe
craft = nil
break
end
end
end
craft = craft or me.get_craft(pos, inventory, hash)
local output_item = craft.output.item
inventory:set_stack("output", 1, output_item)
end
-- From pipeworks/autocrafter.lua
-- clean out unknown items and groups, which would be handled like unknown items in the crafting grid
-- if minetest supports query by group one day, this might replace them
-- with a canonical version instead
local function normalize(item_list)
for i = 1, #item_list do
local name = item_list[i]
if not minetest.registered_items[name] then
item_list[i] = ""
if name == "group:stick" then
item_list[i] = "default:stick"
elseif name == "group:glass" then
item_list[i] = "default:glass"
elseif name == "group:wood" then
name = "moretrees:oak_planks"
if minetest.registered_items[name] then
item_list[i] = name
else
item_list[i] = "default:wood"
end
elseif name == "group:wood" then
item_list[i] = "moretrees:oak_trunk"
elseif name == "group:wool" then
item_list[i] = "wool:white"
elseif name == "group:sand" then
item_list[i] = "default:sand"
elseif name == "group:stone" then
item_list[i] = "default:cobble"
elseif name == "group:leaves" then
name = "moretrees:sequoia_leaves"
if minetest.registered_items[name] then
item_list[i] = name
else
item_list[i] = "default:leaves"
end
elseif name == "group:coal" then
item_list[i] = "default:coal"
elseif name == "group:tree" then
item_list[i] = "default:tree"
end
end
end
return item_list
end
-- 0 to 34
function me.uranium_dust(p)
return "technic:uranium"..(p == 7 and "" or p).."_dust"
end
for pa = 0, 34 do
-- uranium_dust(pa)
-- me.uranium_dust(pa-1).." 2"
-- No, this would require a billion uranium to do this. :-(
-- Make a uranium centrifuge controller and have it be smart.
--me.register_output_by_typename("separating", "")
end
me.output_by_typename = {
-- aka me.register_output_by_typename("cooking", "default:stone")
["cooking"] = { "default:stone" }
}
-- Used to register what machine types (typename) produce which outputs.
-- Used to figure out what machine to use to create the given output.
-- If multiple outputs are produced, only use the main output, not the
-- incidental output.
function me.register_output_by_typename(typename, output)
if not me.output_by_typename[typename] then
me.output_by_typename[typename] = {}
end
table.insert(me.output_by_typename[typename], output)
end
function me.register_output_to_inputs(output, inputs)
me.log("REG: output "..output.." from inputs "..dump(inputs))
me.map_output_to_inputs[output] = inputs
end
me.map_output_to_inputs = {
-- furnace ("cooking")
["default:stone"] = { ItemStack("default:cobble") },
}
function me.find_by_output(name)
-- TODO: we'd love to be able to look this stuff up in the recipes.
-- technic.recipes["technic:doped_silicon_wafer"].alloy.recipes[4].input[1]
return me.map_output_to_inputs[name]
end
function me.register_inventory(name, func)
-- me.log("INVENTORY: registering "..name, "error")
if not me.registered_inventory then
me.registered_inventory = {}
end
me.registered_inventory[name] = func
end
-- Allow any type of machine process to be registered. For example,
-- "alloy" for an alloy furnace for example. These must be done
-- before specific me.register_inventory calls, as that one needs to
-- override this call.
function me.register_typename(name, typename)
me.block_to_typename_map[name] = typename
me.register_inventory(name, function() end)
end
me.block_to_typename_map = {
}
-- default wiring
me.register_typename("default:furnace", "cooking")
function me.get_recipe(typename, inputs)
return technic.get_recipe(typename, inputs)
end
-- TODO: Removing an output when the recipe is empty that is in
-- net.autocrafters should not be allowed as the output in that case
-- is virtual. It can be removed if rinv:"output" has the item iff
-- that item is removed from the autocrafter's output.
local function on_output_change(pos, linv, stack)
local name = stack:get_name()
-- me.log("PROCESS: "..name.." was found0", "error")
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local has_enough = true
local function clear_recipe()
local has_enough = true
for i = 1, 9 do
local prev = linv:get_stack("recipe", i)
if prev and prev:get_name() ~= "" and not inv:room_for_item("main", prev) then
-- full, no room to remove
has_enough = false
elseif prev and prev:get_name() ~= "" and me.insert_item(prev, net, inv, "main"):get_count() > 0 then
net:set_storage_space(true)
-- full, no room to remove
-- push into player inventory?
-- Don't have to worry about this happening until minetest is fully multithreaded
has_enough = false
else
net:set_storage_space(true)
linv:set_stack("recipe", i, ItemStack(""))
end
end
return has_enough
end
if not net.process then
-- rewalk the interfaces on the network to rebuild loans and machines.
net:reload_network()
end
if net and net.process[name] then
-- me.log("PROCESS: "..name.." was found1", "error")
has_enough = clear_recipe()
local pos,ipos = next(net.process[name])
if has_enough and pos then
-- me.log("PROCESS: "..name.." was found2", "error")
local inputs = me.find_by_output(name)
-- me.log("PROCESS: inputs are "..dump(inputs), "error")
local machine_name = minetest.get_node(pos).name
local typename = me.block_to_typename_map[machine_name]
local recip = typename and me.get_recipe(typename, inputs)
if recip and recip.output then
recip.intput = inputs
-- me.log("PROCESS: "..name.." was found for "..typename.." on a "..machine_name, "error")
-- freezer can produce two outputs, we only care about the first.
if recip.output[1] then
recip.output = recip.output[1]
end
stack = ItemStack(recip.output)
linv:set_stack("output", 1, stack)
-- me.log("PROCESS: and the output is "..minetest.serialize(recip.output), "error")
-- me.log("PROCESS: and the output is "..stack:get_name(), "error")
else
me.log("PROCESS: "..name.." was missing from recipe on a "..machine_name, "error")
linv:set_stack("output", 1, ItemStack())
end
end
return 0
elseif net and net.autocrafters[name] then
has_enough = clear_recipe()
if has_enough then
local pos,ipos = next(net.autocrafters[name])
if pos then
local rinv = minetest.get_meta(pos):get_inventory()
stack = ItemStack(rinv:get_stack("output", 1))
linv:set_stack("output", 1, stack)
else
-- me.log("pos in autocrafters was missing", "error")
linv:set_stack("output", 1, ItemStack())
end
else
linv:set_stack("output", 1, ItemStack())
end
return 0
end
local input = minetest.get_craft_recipe(name)
if not input.items or input.type ~= "normal" then return 0 end
local items, width = normalize(input.items), input.width
local item_idx, width_idx = 1, 1
for i = 1, 9 do
local prev = linv:get_stack("recipe", i)
if prev and prev:get_name() ~= "" and not inv:room_for_item("main", prev) then
-- full, no room to remove
has_enough = false
if width_idx <= width then
item_idx = item_idx + 1
end
elseif prev and prev:get_name() ~= "" and me.insert_item(prev, net, inv, "main"):get_count() > 0 then
net:set_storage_space(true)
-- full, no room to remove
-- push into player inventory?
-- Don't have to worry about this happening until minetest is fully multithreaded
has_enough = false
if width_idx <= width then
item_idx = item_idx + 1
end
elseif width_idx <= width then
net:set_storage_space(true)
if inv:contains_item("main", items[item_idx]) then
me.remove_item(net, inv, "main", ItemStack(items[item_idx]))
linv:set_stack("recipe", i, items[item_idx])
else
has_enough = false
linv:set_stack("recipe", i, ItemStack(""))
end
item_idx = item_idx + 1
else
linv:set_stack("recipe", i, ItemStack(""))
end
width_idx = (width_idx < 3) and (width_idx + 1) or 1
end
-- we'll set the output slot in after_recipe_change to the actual result of the new recipe
after_recipe_change(pos, linv)
return 0
end
-- [me cterminal] Register node
me.register_node("cterminal", {
description = "ME Crafting Terminal",
usedfor = "Can interact with storage cells in ME networks",
tiles = {
"chest_top",
"chest_top",
"chest_side",
"chest_side",
"chest_side",
"chest_front",
},
recipe = {
{ 1, {
{"microexpansion:term", "default:chest"},
},
}
},
is_ground_content = false,
groups = { cracky = 1, me_connect = 1, tubedevice = 1, tubedevice_receiver = 1 },
paramtype = "light",
paramtype2 = "facedir",
me_update = update_chest,
on_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("formspec", chest_formspec(pos, 1))
meta:set_string("inv_name", "none")
meta:set_int("page", 1)
local own_inv = meta:get_inventory()
own_inv:set_size("src", 1)
own_inv:set_size("recipe", 3*3)
own_inv:set_size("output", 1)
local net = me.get_connected_network(pos)
me.send_event(pos,"connect",{net=net})
if net then
update_chest(pos)
end
end,
after_destruct = function(pos)
me.send_event(pos,"disconnect")
end,
can_dig = function(pos, player)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
return inv:is_empty("recipe")
end,
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
me.log("Allow a move from "..from_list.." to "..to_list, "error")
local meta = minetest.get_meta(pos)
if to_list == "search" then
local net = me.get_connected_network(pos)
local linv = minetest.get_meta(pos):get_inventory()
local inv = net:get_inventory()
local stack = linv:get_stack(from_list, from_index)
stack:set_count(count)
-- local meta = minetest.get_meta(pos)
-- meta:set_string("infotext", "allow moving: "..stack:get_name())
-- TODO: Check capacity? Test.
me.insert_item(stack, net, inv, "main")
return count
end
if to_list == "recipe" and from_list == "search" then
local net = me.get_connected_network(pos)
local linv = minetest.get_meta(pos):get_inventory()
local inv = net:get_inventory()
local stack = linv:get_stack(from_list, from_index)
local meta = minetest.get_meta(pos)
count = math.min(count, stack:get_stack_max())
stack:set_count(count)
me.remove_item(net, inv, "main", stack)
return count
end
if to_list == "output" then
local linv = minetest.get_meta(pos):get_inventory()
local stack = linv:get_stack(from_list, from_index)
return on_output_change(pos, linv, stack)
end
return count
end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
-- This is used for removing items from "search", "recipe" and "output".
--me.log("Allow a take from "..listname, "error")
local count = stack:get_count()
if listname == "search" or listname == "recipe" then
count = math.min(count, stack:get_stack_max())
end
--[[if listname == "main" then
-- This should be unused, we don't have a local inventory called main.
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local ret = me.remove_item(net, inv, "main", stack)
me.log("REMOVE: after remove count is "..ret:get_count(), "error")
return ret:get_count()
end
]]
return count
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if listname == "output" then
local linv = minetest.get_meta(pos):get_inventory()
return on_output_change(pos, linv, stack)
elseif listname == "search" or listname == "main" then
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
-- TODO: Check full inv, should be fixed now, confirm.
local leftovers = me.insert_item(stack, net, inv, "main")
return stack:get_count() - leftovers:get_count()
end
return stack:get_count()
end,
on_metadata_inventory_put = function(pos, listname, _, stack)
if listname == "recipe" then
local linv = minetest.get_meta(pos):get_inventory()
after_recipe_change(pos, linv)
elseif listname == "search" or listname == "main" then
-- done above in allow, nothing left to do here
elseif listname == output then
-- done above
else
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local leftovers = me.insert_item(stack, net, inv, "main")
if leftovers:get_count() > 0 then
fixme()
end
net:set_storage_space(true)
end
end,
on_metadata_inventory_take = function(pos, listname, index, stack)
me.log("A taking of "..stack:get_name().." from "..listname, "error")
if listname == "output" then
local linv = minetest.get_meta(pos):get_inventory()
local num_left = linv:get_stack("output", 1):get_count()
-- We only need to consume the recipe if there are no more items
-- if num_left ~= 0 then return end
if num_left > 1 then return end
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local replace = true
-- This assumes that all inputs are only just 1 item, always true?
for i = 1, 9 do
local inp = linv:get_stack("recipe", i)
if inp and inp:get_name() ~= "" then
local consume = ItemStack(inp:get_name())
replace = replace and (inp:get_count() > 1 or inv:contains_item("main", consume))
end
end
for i = 1, 9 do
local inp = linv:get_stack("recipe", i)
if inp and inp:get_name() ~= "" then
if inp:get_count() == 1 then
if inv:contains_item("main", inp) then
local r = me.remove_item(net, inv, "main", inp)
if r:get_count() ~= 1 then
linv:set_stack("recipe", i, ItemStack(""))
replace = false
end
else
linv:set_stack("recipe", i, ItemStack(""))
replace = false
end
else
local stack_copy = ItemStack(inp)
stack_copy:set_count(inp:get_count()-1)
linv:set_stack("recipe", i, stack_copy)
end
end
end
-- deal with replacements
local hash = minetest.hash_node_position(pos)
local craft = autocrafterCache[hash] or me.get_craft(pos, linv, hash)
for i = 1, 9 do
if (craft.decremented_input.items[i]:get_count() ~= linv:get_stack("recipe", i):get_count()
or craft.decremented_input.items[i]:get_name() ~= linv:get_stack("recipe", i):get_name())
and not craft.decremented_input.items[i]:is_empty() then
local leftovers = me.insert_item(craft.decremented_input.items[i], net, inv, "main")
if leftovers:get_count() > 0 then
-- Ick, no room, just drop on the floor. Maybe player inventory?
minetest.add_item(pos, leftovers)
end
end
if replace then
linv:set_stack("output", 1, craft.output.item)
else
linv:set_list("output", {})
end
end
elseif listname == "recipe" then
local linv = minetest.get_meta(pos):get_inventory()
after_recipe_change(pos, linv)
elseif listname ~= "main" then
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
me.remove_item(net, inv, "main", stack)
end
end,
on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
me.log("A move from "..from_list.." to "..to_list, "error")
if to_list == "recipe" or from_list == "recipe" then
local inv = minetest.get_meta(pos):get_inventory()
after_recipe_change(pos, inv)
end
end,
tube = {
can_insert = function(pos, _, stack) --pos, node, stack, direction
-- TODO: update to use capacity_cache?
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local max_slots = inv:get_size("main")
local max_items = net.capacity_cache
local slots, items = 0, 0
-- Get amount of items in drive
for i = 1, max_slots do
local dstack = inv:get_stack("main", i)
if dstack:get_name() ~= "" then
slots = slots + 1
local num = dstack:get_count()
if num == 0 then num = 1 end
items = items + num
end
end
items = items + stack:get_count()
return max_items > items
end,
insert_object = function(pos, _, stack)
local net = me.get_connected_network(pos)
local inv = net:get_inventory()
local leftovers = me.insert_item(stack, net, inv, "main")
net:set_storage_space(true)
return leftovers
end,
connect_sides = {left=1, right=1, front=1, back=1, top=1, bottom=1},
},
after_place_node = pipeworks_enabled and pipeworks.after_place,
after_dig_node = pipeworks_enabled and pipeworks.after_dig,
on_receive_fields = function(pos, _, fields, sender)
local net,cpos = me.get_connected_network(pos)
if net then
if cpos then
me.log("network and ctrl_pos","info")
else
me.log("network but no ctrl_pos","warning")
end
else
if cpos then
me.log("no network but ctrl_pos","warning")
else
me.log("no network and no ctrl_pos","info")
end
end
local meta = minetest.get_meta(pos)
local page = meta:get_int("page")
local inv_name = meta:get_string("inv_name")
local own_inv = meta:get_inventory()
local ctrl_inv
if cpos then
ctrl_inv = net:get_inventory()
else
me.log("no network connected","warning")
return
end
local inv
if inv_name == "main" then
inv = ctrl_inv
assert(inv,"no control inv")
else
inv = own_inv
assert(inv,"no own inv")
end
local page_max = math.floor(inv:get_size(inv_name) / 32) + 1
if inv_name == "none" then
return
end
if fields.next then
if page + 32 > inv:get_size(inv_name) then
return
end
meta:set_int("page", page + 32)
meta:set_string("formspec", chest_formspec(pos, page + 32, inv_name, page_max))
elseif fields.prev then
if page - 32 < 1 then
return
end
meta:set_int("page", page - 32)
meta:set_string("formspec", chest_formspec(pos, page - 32, inv_name, page_max))
elseif fields.search or fields.key_enter_field == "filter" then
own_inv:set_size("search", 0)
if fields.filter == "" then
meta:set_int("page", 1)
meta:set_string("inv_name", "main")
meta:set_string("formspec", chest_formspec(pos, 1, "main", page_max))
else
local tab = {}
for i = 1, ctrl_inv:get_size("main") do
local match = ctrl_inv:get_stack("main", i):get_name():find(fields.filter)
if match then
tab[#tab + 1] = ctrl_inv:get_stack("main", i)
end
end
own_inv:set_list("search", tab)
meta:set_int("page", 1)
meta:set_string("inv_name", "search")
meta:set_string("formspec", chest_formspec(pos, 1, "search", page_max, fields.filter))
end
elseif fields.clear then
own_inv:set_size("search", 0)
meta:set_int("page", 1)
meta:set_string("inv_name", "main")
meta:set_string("formspec", chest_formspec(pos, 1, "main", page_max))
elseif fields.tochest then
local pinv = minetest.get_inventory({type="player", name=sender:get_player_name()})
-- TODO: test and fix, net:set_storage_space(pinv:get_size("main"))
local space = net:get_item_capacity()
local contents = ctrl_inv:get_list("main") or {}
for _,s in pairs(contents) do
if not s:is_empty() then
space = space - s:get_count()
end
end
me.move_inv(net, { inv=pinv, name="main" }, { inv=ctrl_inv, name="main", huge=true }, space)
net:set_storage_space(true)
elseif fields.autocraft or fields.key_enter_field == "autocraft" then
if fields.autocraft ~= "" and tonumber(fields.autocraft) ~= nil then
local count = tonumber(fields.autocraft)
fields.autocraft = nil
if not own_inv:get_stack("output", 1):is_empty() and count < math.pow(2,16) then
me.autocraft(autocrafterCache, pos, net, own_inv, ctrl_inv, count)
end
end
end
end,
})