-- tool mods, by gsmanners -- license: WTFPL -------------------------------------------------- -- workbenches are extensions of your own personal crafting capabilities -- they provide guides, automation, and four additional crafting areas -- consequently, they can also store up to 36 stacks of items -------------------------------------------------- gs_tools.mods = {} gs_tools.groups = {} gs_tools.items = {} gs_tools.crafts = {} gs_tools.all_mods = " all" gs_tools.player_inv_width = 8 -- get list of items in all the indicated groups gs_tools.get_group_items = function(groups) local f = string.split(groups) -- current list of items in the indicated groups local i = gs_tools.groups[f[1]] local c = 2 while f[c] do -- find items that match up with the list in the next group local i2 = {} if i then -- make sure the item group is a real one if gs_tools.groups[f[c]] then for _,v1 in pairs(i) do for _,v2 in pairs(gs_tools.groups[f[c]]) do if v1 == v2 then table.insert(i2, v1) end end end end i = i2 end c = c + 1 end return i end -- load the crafting recipes for a particular item gs_tools.load_crafts = function(item) local cr = minetest.get_all_craft_recipes(item) gs_tools.crafts[item] = {} if not cr then -- note: this is undocumented -- :: realname = registered_aliases[alias] for k,v in pairs(minetest.registered_aliases) do if v == item then -- get craft recipes listed under the alias cr = minetest.get_all_craft_recipes(k) end end end if cr and #cr > 0 then -- sanity check the recipes for k1,v1 in pairs(cr) do local gc = 1 -- assume it's good for k2,v2 in pairs(v1.items) do if string.sub(v2, 1, 6) == "group:" then -- check for bad group names local i = gs_tools.get_group_items(string.sub(v2, 7)) if not i or #i == 0 then gc = 0 end end end if gc > 0 then table.insert(gs_tools.crafts[item], v1) end end end -- technic support if minetest.get_modpath("technic") then -- alloy recipes for _, r in pairs(technic.alloy_recipes) do local t = { type = "alloy" } if r.output.name == item then t.width = 2 t.items = {r.input[1].name, r.input[2].name} t.count = {r.input[1].count, r.input[2].count} t.output = r.output.name.." "..r.output.count table.insert(gs_tools.crafts[item], t) end end -- grinder recipes for _, r in pairs(technic.grinder_recipes) do local t = { type = "grind" } if r.output == item or string.match(r.output, "(.*) %d+$") == item then t.width = 0 t.items = {r.input} t.output = r.output table.insert(gs_tools.crafts[item], t) end end -- compressor recipes for k, r in pairs(technic.compressor_recipes) do local t = { type = "comp" } if r.dst_name == item then t.width = 0 t.items = {k} t.count = {r.src_count} t.output = r.dst_name.." "..r.dst_count table.insert(gs_tools.crafts[item], t) end end -- extractor recipes for k, r in pairs(technic.extractor_recipes) do local t = { type = "extr" } if r.dst_name == item then t.width = 0 t.items = {k} t.count = {r.src_count} t.output = r.dst_name.." "..r.dst_count table.insert(gs_tools.crafts[item], t) end end end if #gs_tools.crafts[item] == 0 then gs_tools.crafts[item] = { { type = "unknown" } } end end -- update the crafting preview gs_tools.update_preview = function(pos) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() local w = meta:get_int("gs_slot") * 9 local s = {} for i=1,9 do table.insert(s, inv:get_stack("main", i+w)) end local o, r = minetest.get_craft_result( { method = "normal", width = 3, items = s } ) inv:set_stack("preview", 1, o.item) end -- spill items into the world gs_tools.do_spill_items = function(pos, stack) if stack and not stack:is_empty() then local p = {x=pos.x,y=pos.y+1,z=pos.z} local obj = minetest.add_entity(p, "__builtin:item") obj:get_luaentity():set_item(stack:to_string()) -- undocumented obj:setvelocity({x=math.random(0,4)-2, y=0, z=math.random(0,4)-2}) end end -- perform the craft gs_tools.do_craft = function(pos, player) minetest.log("action", player:get_player_name() .. " crafts stuff on workbench at " .. minetest.pos_to_string(pos)) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() local w = meta:get_int("gs_slot") * 9 local s = {} for i=1,9 do table.insert(s, inv:get_stack("main", i+w)) end local o, r = minetest.get_craft_result( { method = "normal", width = 3, items = s } ) -- set decremented_input for k,v in pairs(r.items) do inv:set_stack("main", k+w, v) end -- check for leftovers local l = inv:get_stack("preview", 1) if not l:is_empty() then local p = player:get_inventory() local x = p:add_item("main", l) gs_tools.do_spill_items(pos, x) end gs_tools.update_preview(pos) end -- move items off the workbench gs_tools.clear_workbench = function(pos, player) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() local w = meta:get_int("gs_slot") * 9 local s = {} local p = player:get_inventory() for i=1,9 do local x = p:add_item("main", inv:get_stack("main", i+w)) gs_tools.do_spill_items(pos, x) inv:set_stack("main", i+w, "") end gs_tools.update_preview(pos) end -- add components of the current recipe to the workbench gs_tools.add_craft = function(pos, player, count) if not count then count = 1 end local meta = minetest.get_meta(pos) local inv = meta:get_inventory() local i = meta:get_string("gs_item") local r = meta:get_int("gs_recipe") local w = meta:get_int("gs_slot") * 9 local p = player:get_inventory() if i and #i > 0 then if not gs_tools.crafts[i] then gs_tools.load_crafts(i) end local rt = gs_tools.crafts[i][r].type local ri = gs_tools.crafts[i][r].items local rw = gs_tools.crafts[i][r].width if ri and rt == "normal" then local ix = 1 local ic = w + 1 for k=1,9 do local s = inv:get_stack("main", ic) local s2 = ItemStack("") local m = 1 local c = count local v = ri[k] if v and #v > 0 then if string.sub(v, 1, 6) == "group:" then -- group item recipe component local g = gs_tools.get_group_items(string.sub(v, 7)) if g and #g > 0 then m = 0 -- if the item on the workbench is in these groups, then use that for _,v2 in pairs(g) do if s:get_name() == v2 then s2 = ItemStack(v2) s2:set_count(c) m = 1 end end -- if not, then find something in the player's inventory that works if m == 0 then local q = 0 for _,v2 in pairs(g) do local s3 = ItemStack(v2) s3:set_count(c) if p:contains_item("main", s3) and q == 0 then s2 = s3 q = 1 end end end end else -- new stack = item s2 = ItemStack(v) s2:set_count(c) -- new item matches old? if s:get_name() ~= v then m = 0 end end if not s2:is_empty() then -- check whether count will overflow and clamp it if m > 0 and s:get_count() + c > s:get_stack_max() then c = s:get_stack_max() - s:get_count() s2:set_count(c) end -- check player's inventory if p:contains_item("main", s2) then p:remove_item("main", s2) -- clear off junk in the workbench if m == 0 then local x = p:add_item("main", s) gs_tools.do_spill_items(pos, x) inv:set_stack("main", ic, "") s:clear() end -- move stack to the workbench s2:set_count(c + s:get_count()) inv:set_stack("main", ic, s2) end end end -- next item in recipe ix = ix + 1 ic = ic + 1 if ix > rw then ix = 1 if rw > 0 then ic = ic + (3 - rw) end end end end end gs_tools.update_preview(pos) end -- do formspec stuff -- mental note: lots going on here, might want to divide this up at some point gs_tools.workbench_formspec = function(pos, params) local modname = params.modname local mode = params.mode local item = params.item local page = params.page local search = params.search local recipe = params.recipe local hist = string.split(params.history) local slot = params.slot -- modnames local mods = "button[0,0;1,1;do_mods;Mods]dropdown[1,0.2;3;mods;" local i = 1 local all = gs_tools.all_mods mods = mods .. all .. "," local c = 2 for _,n in pairs(gs_tools.mods) do mods = mods .. n .. "," if n == modname then i = c end c = c + 1 end mods = string.sub(mods, 1, #mods - 1) .. ";" .. i .. "]" -- check for mode-changing conditions if mode ~= 0 and string.sub(search, 1, 6) == "group:" then mode = 0 end -- put together the item list local itemlist = {} local listhead = "" if mode == 1 and item and #item > 0 then local def = minetest.registered_items[item] if def and def.description then listhead = "label[0,0.75;Uses for " .. def.description .. ":]" end -- find the uses for selected item for _,v1 in pairs(gs_tools.items) do -- this could take a while if not gs_tools.crafts[v1] then gs_tools.load_crafts(v1) end local c = gs_tools.crafts[v1] -- table of recipes for this item local fc = 0 for _,v2 in pairs(c) do if v2.items and fc == 0 then for _,v3 in pairs(v2.items) do -- exact match? if item == v3 and fc == 0 then table.insert(itemlist, v1) fc = 1 -- no need for redundancy end -- group match? if string.sub(v3, 1, 6) == "group:" and fc == 0 then local ggi = gs_tools.get_group_items(string.sub(v3, 7)) if ggi then for _,v4 in pairs(ggi) do if item == v4 then table.insert(itemlist, v1) fc = 1 -- this works, too end end end -- wheee! end end end end end -- aww... no more slide elseif mode < 0 then -- show history mode listhead = "label[0,0.75;Previously selected items:]" for _,v in pairs(hist) do table.insert(itemlist, v) end else -- normal mode listhead = "label[0,0.75;Items in " if search and #search > 0 then listhead = listhead .. search else listhead = listhead .. modname end listhead = listhead ..":]" -- get a list of items by mod/search if i > 1 then -- factor in the modname for _,z in pairs(gs_tools.items) do local m = string.match(z, "(.*):") if m == modname then table.insert(itemlist, z) end end else itemlist = gs_tools.items end -- factor in the search local xs = 1 local itemlist2 = {} if search and #search > 0 then if string.sub(search, 1, 6) == "group:" then -- group search local gi = gs_tools.get_group_items(string.sub(search, 7)) if gi then for _,v in pairs(gi) do table.insert(itemlist2, v) end end else for _,z in pairs(itemlist) do -- search the description of the item local def = minetest.registered_items[z] if def and def.description then -- case insensitive local s1 = string.lower(def.description) local s2 = string.lower(search) if string.find(s1, s2, 1, true) then table.insert(itemlist2, z) end end end end itemlist = itemlist2 end end -- calculate number of pages local pages = math.floor((#itemlist - 1) / (4 * 6)) + 1 -- sanity check the page value if page >= pages then page = pages - 1 end if page < 0 then page = 0 end -- item list view (4x6 item_image_button grid) local p = page * 4 * 6 local ilv = "" for y=1,6 do for x=0,3 do p = p + 1 if itemlist[p] then local s = itemlist[p] ilv = ilv .. "item_image_button[" .. x .. "," .. y+0.25 .. ";1,1;" .. s .. ";item:" .. s .. ";]" end end end -- arrow buttons if pages > 0 then if page > 0 then ilv = ilv .. "button[0.25,7.25;1.25,1;list_prev;<<]" end ilv = ilv .. "label[1.6,7.35;" .. page + 1 .. "/" .. pages .."]" if (page + 1) < pages then ilv = ilv .. "button[2.5,7.25;1.25,1;list_next;>>]" end end -- search item local stxt = "button[0,8;1.2,1;do_search;Search]" .. "textarea[1.4,8.33;1.85,0.65;search;;" .. search .. "]" .. "button[2.9,8;0.6,1;clear_search;x]" .. "button[3.4,8;0.6,1;show_history;^]" -- recipe local rec = "" if item and #item > 0 then -- get the registered recipes that produce this item local ritems = {} if not gs_tools.crafts[item] then gs_tools.load_crafts(item) end -- sanity check the recipe value if recipe > #gs_tools.crafts[item] then recipe = #gs_tools.crafts[item] end -- recipe type local rt = gs_tools.crafts[item][recipe].type if rt == "normal" then rec = rec .. "button[4.5,0;1,1;craft_this;Craft]" end if rt == "cooking" then rec = rec .. "button[4.5,0;1,1;cook_this;Cook]" end if rt == "alloy" then rec = rec .. "button[4.5,0;1,1;alloy_this;Alloy]" end if rt == "grind" then rec = rec .. "button[4.5,0;1,1;grind_this;Grind]" end if rt == "comp" then rec = rec .. "button[4.5,0;1,1;comp_this;Comp]" end if rt == "extr" then rec = rec .. "button[4.5,0;1,1;extr_this;Extr]" end -- recipe buttons if recipe > 1 then rec = rec .. "button[5.5,0;1,1;prev_recipe;<<]" end if recipe < #gs_tools.crafts[item] then rec = rec .. "button[6.5,0;1,1;next_recipe;>>]" end local ri = gs_tools.crafts[item][recipe].items local ro = gs_tools.crafts[item][recipe].output local rw = gs_tools.crafts[item][recipe].width if rw and rw == 0 then rw = 3 end -- recipe list grid if ri then c = 1 if rw > 3 then rw = 3 end -- todo: figure out how to support higher widths for y=1,3 do for x=0,rw-1 do s = ri[c] c = c + 1 if s and #s > 0 then -- is this a group item? if string.sub(s, 1, 6) == "group:" then s = string.sub(s, 7) i = gs_tools.get_group_items(s) -- find the representative sample if i and #i > 0 then local xl = 9999 local xn = "" for _,v in pairs(i) do -- the one with the shortest name is usually it if #v < xl then xl = #v xn = v end end -- item to texture local tex = "unknown_item.png" local d = minetest.registered_items[xn] if d then if d.inventory_image and #d.inventory_image > 0 then tex = d.inventory_image else -- this is a little hacky (only uses 1 tile) -- the dialog blinks when this comes up the 1st time -- \ is an undocumented feature tex = "\\" .. minetest.inventorycube(d.tiles[1]) end end -- use image rather than item -- so you don't get a misleading tooltip rec = rec .. "image_button[".. (x + 4.5) .. "," .. y .. ";1,1;" .. tex .. ";group:" .. s .. ";Group]" end else -- not a group item (way easier to deal with) local cc = gs_tools.crafts[item][recipe].count if cc then -- input item counts (this is for Technic) rec = rec .. "item_image_button[" .. (x + 4.5) .. "," .. y .. ";1,1;" .. s .. ";item:" .. s .. ";" .. cc[c-1] .. "]" else rec = rec .. "item_image_button[" .. (x + 4.5) .. "," .. y .. ";1,1;" .. s .. ";item:" .. s .. ";]" end end end end end else -- unknown if gs_tools.crafts[item][recipe].type == "unknown" then rec = rec .. "label[4.5,1;No known recipe for]" .. "label[4.5,1.25;" .. item .. "]" end end -- mode button if mode < 1 then rec = rec .. "button[4.5,3.9;1,1;do_uses;Uses]" else rec = rec .. "button[4.5,3.9;1,1;do_list;List]" end -- item label local def = minetest.registered_items[item] if def and def.description then s = def.description if ro then -- output more than 1? local n = string.match(ro, " (%d+)$") if n and tonumber(n) > 1 then s = n .. " x " .. s end end rec = rec .. "label[5.5,4;".. s .."]" end end -- craft buttons local cbut = "button[8,0;1,1;plus1;+]" .. "button[9,0;1,1;plus10;+10]" .. "button[10,0;1,1;do_make;Make]" .. "button[11,0;1,1;do_clear;Clear]" -- craft list local blabs = { "A", "B", "C", "D" } blabs[slot + 1] = ">" .. blabs[slot + 1] .. "<" local craft = "list[current_name;main;8,1;3,3;" .. slot * 9 .. "]" .. "list[current_name;preview;11.5,2;1,1;]" .. "button[8,3.9;1,1;slot0;" .. blabs[1] .. "]" .. "button[9,3.9;1,1;slot1;" .. blabs[2] .. "]" .. "button[10,3.9;1,1;slot2;" .. blabs[3] .. "]" .. "button[11,3.9;1,1;slot3;" .. blabs[4] .. "]" -- player inventory local pinv = "list[current_player;main;4.5,4.75;" .. gs_tools.player_inv_width .. ",4;]" -- update the meta with the new values local meta = minetest.get_meta(pos) meta:set_string("gs_search", search) meta:set_string("gs_modname", modname) meta:set_int("gs_page", page) meta:set_string("gs_item", item) meta:set_int("gs_recipe", recipe) meta:set_int("gs_mode", mode) meta:set_string("gs_history", table.concat(hist, ",")) meta:set_int("gs_slot", slot) -- assemble formspec string local fw = gs_tools.player_inv_width + 4.5 local fs = "size[" .. fw .. ",8.75]" .. mods .. listhead .. ilv .. stxt .. rec .. cbut .. craft .. pinv meta:set_string("formspec", fs) end -- reg minetest.register_node("gs_tools:workbench", { description = "Workbench", drawtype = "nodebox", tiles = {"gs_workbench.png","default_wood.png","default_wood.png", "default_wood.png","default_wood.png","default_wood.png" }, paramtype = "light", paramtype2 = "facedir", is_ground_content = true, node_box = { type = "fixed", fixed = { { -0.5, 0.3,-0.5, 0.5, 0.5, 0.5 }, { -0.5,-0.5,-0.5, -0.3, 0.3,-0.3 }, { 0.5,-0.5, 0.5, 0.3, 0.3, 0.3 }, { -0.5,-0.5, 0.5, -0.3, 0.3, 0.3 }, { 0.5,-0.5,-0.5, 0.3, 0.3,-0.3 }, } }, groups = { choppy=2, oddly_breakable_by_hand=2 }, sounds = default.node_sound_wood_defaults(), on_construct = function(pos) local meta = minetest.get_meta(pos) meta:set_string("infotext", "Workbench") local inv = meta:get_inventory() inv:set_size("main", 9*4) inv:set_size("preview", 1) end, after_place_node = function(pos, placer, itemstack) gs_tools.workbench_formspec(pos, { modname=gs_tools.all_mods, mode=0, item="", page=0, search="", recipe=1, history="", slot=0 }) end, can_dig = function(pos, player) local meta = minetest.get_meta(pos) local inv = meta:get_inventory() return inv:is_empty("main") end, allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) if from_list == "preview" or to_list == "preview" then return 0 else return count end end, allow_metadata_inventory_put = function(pos, listname, index, stack, player) if listname == "preview" then return 0 else return stack:get_count() end end, allow_metadata_inventory_take = function(pos, listname, index, stack, player) local meta = minetest.get_meta(pos) return stack:get_count() end, on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) gs_tools.update_preview(pos) end, on_metadata_inventory_put = function(pos, listname, index, stack, player) gs_tools.update_preview(pos) end, on_metadata_inventory_take = function(pos, listname, index, stack, player) if listname == "preview" then gs_tools.do_craft(pos, player) else gs_tools.update_preview(pos) end end, on_receive_fields = function(pos, formname, fields, sender) if fields then -- print(dump(fields)) local b = 0 local pre = 0 local meta = minetest.get_meta(pos) local s = meta:get_string("gs_search") local m = meta:get_string("gs_modname") local p = meta:get_int("gs_page") local i = meta:get_string("gs_item") local r = meta:get_int("gs_recipe") local u = meta:get_int("gs_mode") local h = meta:get_string("gs_history") local w = meta:get_int("gs_slot") if fields.do_mods then u = 0 s = "" b = 1 end if fields.list_prev then p = p - 1 b = 1 end if fields.list_next then p = p + 1 b = 1 end if fields.prev_recipe then r = r - 1 b = 1 end if fields.next_recipe then r = r + 1 b = 1 end if fields.clear_search then s = "" b = 1 end if fields.show_history then u = -1 s = "" b = 1 end if fields.do_list then u = 0 b = 1 end if fields.do_uses then u = 1 s = "" b = 1 end if fields.plus1 then gs_tools.add_craft(pos, sender) end if fields.plus10 then gs_tools.add_craft(pos, sender, 10) end if fields.do_make then gs_tools.do_craft(pos, sender) end if fields.do_clear then gs_tools.clear_workbench(pos, sender) end if fields.slot0 then w = 0 b = 1 pre = 1 end if fields.slot1 then w = 1 b = 1 pre = 1 end if fields.slot2 then w = 2 b = 1 pre = 1 end if fields.slot3 then w = 3 b = 1 pre = 1 end if fields.mods ~= m then m = fields.mods u = 0 p = 0 b = 1 end if fields.search ~= s and b == 0 then s = fields.search u = 0 b = 1 end for k, v in pairs(fields) do if string.sub(k, 1, 5) == "item:" then i = string.sub(k, 6) b = 1 -- add this item into history local h2 = string.split(h) local f = 0 for _,v in pairs(h2) do if v == i then f = 1 end -- redundant? end if f == 0 then table.insert(h2, i) if #h2 > (4 * 6) then -- no more than 1 page worth table.remove(h2, 1) -- FIFO queue end end h = table.concat(h2, ",") end if string.sub(k, 1, 6) == "group:" then s = k b = 1 end end if b > 0 then gs_tools.workbench_formspec(pos, { modname=m, mode=u, item=i, page=p, search=s, recipe=r, history=h, slot=w }) if pre > 0 then gs_tools.update_preview(pos) end end end end, }) minetest.register_craft({ output = "gs_tools:workbench", recipe = { { "group:wood","group:wood","group:wood" }, { "group:stick","default:chest","group:stick" }, { "group:stick","","group:stick" }, } }) -- examine items after loading all the mods minetest.after(0, function() local moditems = {} for name,def in pairs(minetest.registered_items) do -- anything good enough for creative is good enough for me if (not def.groups.not_in_creative_inventory or def.groups.not_in_creative_inventory == 0) and def.description and def.description ~= "" then -- one more gotcha if (not def.groups.not_in_craft_guide) then -- has modname? local m = string.match(name, "(.*):") if m and #m > 0 then moditems[m] = 1 -- just make note of this modname end -- add item name to the groups list for each group it belongs in for g,v in pairs(def.groups) do if not gs_tools.groups[g] then gs_tools.groups[g] = {} end table.insert(gs_tools.groups[g], name) end -- add item name to the master list table.insert(gs_tools.items, name) end end end -- unspool the list of mods for n,v in pairs(moditems) do table.insert(gs_tools.mods, n) end -- sort things table.sort(gs_tools.mods) table.sort(gs_tools.items) for n,v in pairs(gs_tools.groups) do table.sort(v) end end) -- get player inventory width -- this is set dynamically, so I have to detect this dynamically gs_tools.get_player_inv_width = function() local p = minetest.get_connected_players() if p and #p > 0 then local i,v = next(p) -- I'm kind of assuming that the player inventory has 4 rows, here gs_tools.player_inv_width = math.floor( v:get_inventory():get_size("main") / 4 ) else -- just keep waiting till we get this info minetest.after(1.5, gs_tools.get_player_inv_width) end end minetest.after(1.5, gs_tools.get_player_inv_width)