From 9fea60e2b1c38a8cac278ac22d3d84961f0639d1 Mon Sep 17 00:00:00 2001 From: random-geek <35757396+random-geek@users.noreply.github.com> Date: Wed, 20 Mar 2019 20:40:29 -0700 Subject: [PATCH] Add everything --- README.md | 38 +++- api.lua | 219 +++++++++++++++++++++ autocrafting.lua | 124 ++++++++++++ init.lua | 121 ++++++++++++ inventory.lua | 255 +++++++++++++++++++++++++ mod.conf | 3 + settingtypes.txt | 8 + textures/cg_plus_arrow.png | Bin 0 -> 183 bytes textures/cg_plus_arrow_small.png | Bin 0 -> 170 bytes textures/cg_plus_icon_autocrafting.png | Bin 0 -> 201 bytes textures/cg_plus_icon_clear.png | Bin 0 -> 708 bytes textures/cg_plus_icon_cooking.png | Bin 0 -> 772 bytes textures/cg_plus_icon_digging.png | Bin 0 -> 223 bytes textures/cg_plus_icon_fuel.png | Bin 0 -> 581 bytes textures/cg_plus_icon_next.png | Bin 0 -> 727 bytes textures/cg_plus_icon_prev.png | Bin 0 -> 728 bytes textures/cg_plus_icon_search.png | Bin 0 -> 1908 bytes 17 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 api.lua create mode 100644 autocrafting.lua create mode 100644 init.lua create mode 100644 inventory.lua create mode 100644 mod.conf create mode 100644 settingtypes.txt create mode 100644 textures/cg_plus_arrow.png create mode 100644 textures/cg_plus_arrow_small.png create mode 100644 textures/cg_plus_icon_autocrafting.png create mode 100644 textures/cg_plus_icon_clear.png create mode 100644 textures/cg_plus_icon_cooking.png create mode 100644 textures/cg_plus_icon_digging.png create mode 100644 textures/cg_plus_icon_fuel.png create mode 100644 textures/cg_plus_icon_next.png create mode 100644 textures/cg_plus_icon_prev.png create mode 100644 textures/cg_plus_icon_search.png diff --git a/README.md b/README.md index 40fc40c..71d0c52 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -# cg_plus -Crafting Guide Plus is a simple and intuitive crafting guide and auto-crafting mod for Minetest. +# Crafting Guide Plus + +Crafting Guide Plus is a simple and intuitive crafting guide and auto-crafting mod for Minetest. CGP is compatible with +Minetest Game and any other games that use sfinv. It was built mostly from the ground up, with some inspiration from +jp's mod as well as Unified Inventory. + +## Features: + +- "Intelligent" auto-crafting, or rather, automatic craft staging. This feature can be disabled if it is not wanted. +- Group support, including group search and support for craft recipes requiring items in multiple groups. +- Shaped and shapeless crafting recipes. +- Fuel and cooking recipes, including fuel replacements and burning/cooking times. +- Digging and digging by chance (item drop) previews. + +## Known issues: + +- The auto-crafting algorithm is not *perfect*. For craft recipes requiring items in a group, only the item with the +greatest count from the player's inventory will be utilized. +- Items in multiple groups will not always display correctly in craft view. + +## License + +Code is licensed under the GNU LGPL v3.0. Images and other media are CC BY-SA 4.0 unless otherwise noted. + +The following images are from Minetest Game, and their respective licenses apply: + +``` +cg_plus_icon_autocrafting.png Based on default_tool_stonepick.png +cg_plus_icon_clear.png From creative_clear_icon.png +cg_plus_icon_cooking.png From default_furnace_front_active.png +cg_plus_icon_digging.png From default_tool_stonepick.png +cg_plus_icon_fuel.png From default_furnace_fire_fg.png +cg_plus_icon_next.png From creative_next_icon.png +cg_plus_icon_prev.png From creative_prev_icon.png +cg_plus_icon_search.png From creative_search_icon.png +``` diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..05819b7 --- /dev/null +++ b/api.lua @@ -0,0 +1,219 @@ +-- TODO: aliases? + +local get_drops = function(item, def) + local normalDrops = {} + local randomDrops = {} + + if type(def.drop) == "table" then + -- Handle complex drops. This is the method used by Unified Inventory. + local maxStart = true + local itemsLeft = def.drop.max_items + local dropTables = def.drop.items or {} + + local dStack, dName, dCount + + for _, dropTable in ipairs(dropTables) do + if itemsLeft and itemsLeft <= 0 then break end + + for _, dropItem in ipairs(dropTable.items) do + dStack = ItemStack(dropItem) + dName = dStack:get_name() + dCount = dStack:get_count() + + if dCount > 0 and dName ~= item then + if #dropTable.items == 1 and dropTable.rarity == 1 and maxStart then + normalDrops[dName] = (normalDrops[dName] or 0) + dCount + + if itemsLeft then + itemsLeft = itemsLeft - 1 + if itemsLeft <= 0 then break end + end + else + if itemsLeft then maxStart = false end + + randomDrops[dName] = (randomDrops[dName] or 0) + dCount + end + end + end + end + else + -- Handle simple, one-item drops. + local dStack = ItemStack(def.drop) + + if not dStack:is_empty() and dStack:get_name() ~= item then + normalDrops[dStack:get_name()] = dStack:get_count() + end + end + + return normalDrops, randomDrops +end + +cg.build_item_list = function() + local startTime = minetest.get_us_time() + cg.items_all.list = {} + + for item, def in pairs(minetest.registered_items) do + if def.description and def.description ~= "" and + minetest.get_item_group(item, "not_in_creative_inventory") == 0 then + table.insert(cg.items_all.list, item) + cg.crafts[item] = minetest.get_all_craft_recipes(item) or {} + end + end + + local def, fuel, decremented + + for _, item in ipairs(cg.items_all.list) do + def = minetest.registered_items[item] + + fuel, decremented = minetest.get_craft_result({method = "fuel", width = 0, items = {ItemStack(item)}}) + + if fuel.time > 0 then + table.insert(cg.crafts[item], { + type = "fuel", + items = {item}, + output = decremented.items[1]:to_string(), + time = fuel.time, + }) + end + + if def.drop then + local normalDrops, randomDrops = get_drops(item, def) + + for dItem, dCount in pairs(normalDrops) do + if cg.crafts[dItem] then + table.insert(cg.crafts[dItem], { + type = "digging", + width = 0, + items = {item}, + output = ItemStack({name = dItem, count = dCount}):to_string() + }) + end + end + + for dItem, dCount in pairs(randomDrops) do + if cg.crafts[dItem] then + table.insert(cg.crafts[dItem], { + type = "digging_chance", + width = 0, + items = {item}, + output = ItemStack({name = dItem, count = dCount}):to_string() + }) + end + end + end + + for group, _ in pairs(def.groups) do + if not cg.group_stereotypes[group] then + cg.group_stereotypes[group] = item + end + end + end + + table.sort(cg.items_all.list) + cg.items_all.num_pages = math.ceil(#cg.items_all.list / cg.PAGE_ITEMS) + + minetest.log("info", string.format("[cg_plus] Finished building item list in %.3f s.", + (minetest.get_us_time() - startTime) / 1000000)) +end + +cg.filter_items = function(player, filter) + local playerName = player:get_player_name() + + if not filter or filter == "" then + cg.items_filtered[playerName] = nil + return + end + + cg.items_filtered[playerName] = {list = {}} + + local groupFilter = string.sub(filter, 1, 6) == "group:" and filter:sub(7) + + if groupFilter and cg.group_search then + -- Search by group + local groups = string.split(groupFilter, ",") + local isInGroups + + for _, item in ipairs(cg.items_all.list) do + isInGroups = true + + for idx = 1, math.min(#groups, cg.group_search_max) do + if minetest.get_item_group(item, groups[idx]) == 0 then + isInGroups = false + break + end + end + + if isInGroups then + table.insert(cg.items_filtered[playerName].list, item) + end + end + else + -- Regular search + for _, item in ipairs(cg.items_all.list) do + if item:lower():find(filter, 1, true) or + minetest.registered_items[item].description:lower():find(filter, 1, true) then + table.insert(cg.items_filtered[playerName].list, item) + end + end + end + + cg.items_filtered[playerName].num_pages = math.ceil(#cg.get_item_list(player).list / cg.PAGE_ITEMS) +end + +cg.parse_craft = function(craft) + local type = craft.type + local template = cg.craft_types[type] or {} + + if craft.width == 0 and template.alt_zero_width then + type = template.alt_zero_width + template = cg.craft_types[template.alt_zero_width] or {} + end + + local newCraft = { + type = type, + items = {}, + output = craft.output, + } + + if template.get_infotext then + newCraft.infotext = template.get_infotext(craft) or "" + end + + local width = math.max(craft.width or 0, 1) + + if template.get_grid_size then + newCraft.grid_size = template.get_grid_size(craft) + else + newCraft.grid_size = {x = width, y = math.ceil(table.maxn(craft.items) / width)} + end + + if template.inherit_width then + -- For shapeless recipes, there is no need to modify the item list. + newCraft.items = craft.items + else + -- The craft's width is not always the same as the grid size, so items need to be shifted around. + for idx, item in pairs(craft.items) do + newCraft.items[idx + (newCraft.grid_size.x - width) * math.floor((idx - 1) / width)] = item + end + end + + return newCraft +end + +cg.get_item_list = function(player) + return cg.items_filtered[player:get_player_name()] or cg.items_all +end + +cg.register_craft_type = function(name, def) + cg.craft_types[name] = def +end + +cg.register_group_stereotype = function(group, item) + cg.group_stereotypes[group] = item +end + +minetest.register_on_mods_loaded(cg.build_item_list) + +minetest.register_on_leaveplayer(function(player, timed_out) + cg.items_filtered[player:get_player_name()] = nil +end) diff --git a/autocrafting.lua b/autocrafting.lua new file mode 100644 index 0000000..38bb773 --- /dev/null +++ b/autocrafting.lua @@ -0,0 +1,124 @@ +local add_or_create = function(t, i, n) + t[i] = t[i] and t[i] + n or n +end + +local get_group_item = function(invCache, groups) + local maxCount = 0 + local maxItem + local isInGroups + + for item, count in pairs(invCache) do + isInGroups = true + + for _, group in ipairs(groups) do + if minetest.get_item_group(item, group) == 0 then + isInGroups = false + break + end + end + + if isInGroups and count > maxCount then + maxItem = item + maxCount = count + end + end + + return maxItem +end + +cg.auto_get_craftable = function(player, craft) + local inv = player:get_inventory():get_list("main") + local invCache = {} + + -- Create a cache of the inventory with itemName = count pairs. This speeds up searching for items. + for _, stack in ipairs(inv) do + if stack:get_count() > 0 then + add_or_create(invCache, stack:get_name(), stack:get_count()) + end + end + + local reqItems = {} + local reqGroups = {} + + -- Find out how many of each item/group is required to craft one item. + for _, item in pairs(craft.items) do + if item:sub(1, 6) == "group:" then + add_or_create(reqGroups, item, 1) + else + add_or_create(reqItems, item, 1) + end + end + + local gMaxItem + + -- For each group, find the item in that group from the player's inventory with the largest count. + for group, count in pairs(reqGroups) do + gMaxItem = get_group_item(invCache, group:sub(7):split(",")) + + if gMaxItem then + add_or_create(reqItems, gMaxItem, count) + else + return 0 + end + end + + local craftable = 1000 + + for item, count in pairs(reqItems) do + if invCache[item] then + craftable = math.min(craftable, math.floor(invCache[item] / count)) + else + return 0 + end + + -- We can't craft more than the stack_max of our ingredients. + if minetest.registered_items[item].stack_max then + craftable = math.min(craftable, minetest.registered_items[item].stack_max) + end + end + + return craftable +end + +cg.auto_craft = function(player, craft, num) + inv = player:get_inventory() + + if craft.width > inv:get_width("craft") or table.maxn(craft.items) > inv:get_size("craft") then return end + + local width = craft.width == 0 and inv:get_width("craft") or craft.width + local stack, invCache + local groupCache = {} + + for idx, item in pairs(craft.items) do + -- Shift the indices so the items in the craft go to the right spots on the crafting grid. + idx = idx + (inv:get_width("craft") - width) * math.floor((idx - 1) / width) + + if item:sub(1, 6) == "group:" then + -- Create an inventory cache. + if not invCache then + invCache = {} + + for _, stack in ipairs(inv:get_list("main")) do + if stack:get_count() > 0 then + add_or_create(invCache, stack:get_name(), stack:get_count()) + end + end + end + + -- Get the most plentiful item in the group. + if not groupCache[item] then + groupCache[item] = get_group_item(invCache, item:sub(7):split(",")) + end + + -- Move the selected item. + if groupCache[item] then + stack = inv:remove_item("main", ItemStack({name = groupCache[item], count = num})) + inv:set_stack("craft", idx, stack) + end + else + -- Move the item. + stack = inv:remove_item("main", ItemStack({name = item, count = num})) + inv:set_stack("craft", idx, stack) + end + end +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..55fe5a3 --- /dev/null +++ b/init.lua @@ -0,0 +1,121 @@ +cg = { + PAGE_WIDTH = 8, + PAGE_ITEMS = 24, + items_all = {}, + items_filtered = {}, + crafts = {}, + craft_types = {}, + group_stereotypes = {}, +} + +local settings = minetest.settings + +cg.autocrafting = settings:get_bool("cg_plus_autocrafting", true) +cg.group_search = settings:get_bool("cg_plus_group_search", true) +cg.group_search_max = tonumber(settings:get("cg_plus_group_search_max")) or 5 + +cg.S = minetest.get_translator("cg_plus") +local F = minetest.formspec_escape + +local path = minetest.get_modpath("cg_plus") + +dofile(path .. "/api.lua") + +if cg.autocrafting then + dofile(path .. "/autocrafting.lua") +end + +dofile(path .. "/inventory.lua") + +cg.register_craft_type("normal", { + description = F(cg.S("Crafting")), + uses_crafting_grid = true, + alt_zero_width = "shapeless", + + get_grid_size = function(craft) + local width = math.max(craft.width, 1) + local height = math.ceil(table.maxn(craft.items) / width) + local sideLen = math.max(width, height) + + if sideLen < 3 then + return {x = 3, y = 3} + else + return {x = sideLen, y = sideLen} + end + end, +}) + +cg.register_craft_type("shapeless", { + description = F(cg.S("Mixing")), + inherit_width = true, + uses_crafting_grid = true, + + get_grid_size = function(craft) + local numItems = table.maxn(craft.items) + + if table.maxn(craft.items) <= 9 then + return {x = 3, y = 3} + else + local sideLen = math.ceil(math.sqrt(numItems)) + return {x = sideLen, y = sideLen} + end + end, +}) + +cg.register_craft_type("cooking", { + description = F(cg.S("Cooking")), + inherit_width = true, + arrow_icon = "cg_plus_arrow_small.png^cg_plus_icon_cooking.png", + + get_grid_size = function(craft) + return {x = 1, y = 1} + end, + + get_infotext = function(craft) + return minetest.colorize("#FFFF00", F(cg.S("Time: @1 s", craft.width or 0))) + end, +}) + +cg.register_craft_type("fuel", { + description = F(cg.S("Fuel")), + inherit_width = true, + arrow_icon = "cg_plus_arrow_small.png^cg_plus_icon_fuel.png", + + get_grid_size = function(craft) + return {x = 1, y = 1} + end, + + get_infotext = function(craft) + return minetest.colorize("#FFFF00", F(cg.S("Time: @1 s", craft.time or 0))) + end, +}) + +cg.register_craft_type("digging", { + description = F(cg.S("Digging")), + inherit_width = true, + arrow_icon = "cg_plus_arrow_small.png^cg_plus_icon_digging.png", + + get_grid_size = function(craft) + return {x = 1, y = 1} + end, +}) + +cg.register_craft_type("digging_chance", { + description = F(cg.S("Digging@n(by chance)")), + inherit_width = true, + arrow_icon = "cg_plus_arrow_small.png^cg_plus_icon_digging.png", + + get_grid_size = function(craft) + return {x = 1, y = 1} + end, +}) + +cg.register_group_stereotype("mesecon_conductor_craftable", "mesecons:wire_00000000_off") + +if minetest.get_modpath("default") then + cg.register_group_stereotype("stone", "default:stone") + cg.register_group_stereotype("wood", "default:wood") + cg.register_group_stereotype("sand", "default:sand") + cg.register_group_stereotype("leaves", "default:leaves") + cg.register_group_stereotype("tree", "default:tree") +end diff --git a/inventory.lua b/inventory.lua new file mode 100644 index 0000000..1250c4b --- /dev/null +++ b/inventory.lua @@ -0,0 +1,255 @@ +local F = minetest.formspec_escape + +cg.update_filter = function(player, context, filter) + if (filter or "") == (context.cg_filter or "") then return end + context.cg_page = 0 + context.cg_filter = filter + cg.filter_items(player, filter) +end + +local make_item_button = function(formspec, x, y, size, name) + if name and name ~= "" then + local groups, buttonText + + if name:sub(1, 6) == "group:" then + groups = name:sub(7):split(",") + buttonText = #groups > 1 and ("G " .. #groups) or "G" + name = name:gsub(",", "/") + end + + formspec[#formspec + 1] = string.format("item_image_button[%.2f,%.2f;%.2f,%.2f;%s;cgitem_%s;%s]", + x, y, size, size, + groups and (cg.group_stereotypes[groups[1]] or "") or name, + name:match("^%S+"), -- Keep only the item name, not the quantity. + buttonText or "" + ) + + if groups then + formspec[#formspec + 1] = string.format("tooltip[cgitem_%s;%s]", + name, + #groups > 1 and + F(cg.S("Any item in groups: @1", minetest.colorize("#72FF63", table.concat(groups, ", ")))) or + F(cg.S("Any item in group: @1", minetest.colorize("#72FF63", groups[1]))) + ) + end + else + size = size * 0.8 + 0.2 + formspec[#formspec + 1] = string.format("image[%.2f,%.2f;%.2f,%.2f;gui_hb_bg.png]", x, y, size, size) + end +end + +local make_item_grid = function(formspec, player, context) + local itemList = cg.get_item_list(player) + context.cg_page = context.cg_page or 0 + + formspec[#formspec + 1] = [[ + image_button[2.4,3.7;0.8,0.8;cg_plus_icon_search.png;cg_search;] + image_button[3.1,3.7;0.8,0.8;cg_plus_icon_clear.png;cg_clear;] + image_button[5.1,3.7;0.8,0.8;cg_plus_icon_prev.png;cg_prev;] + image_button[7.1,3.7;0.8,0.8;cg_plus_icon_next.png;cg_next;] + ]] + + formspec[#formspec + 1] = string.format("label[0,0;%s]", F(cg.S("Crafting Guide"))) + + formspec[#formspec + 1] = string.format("field[0.3,3.9;2.5,1;cg_filter;;%s]", F(context.cg_filter or "")) + formspec[#formspec + 1] = "field_close_on_enter[cg_filter;false]" + formspec[#formspec + 1] = string.format("label[6,3.8;%i / %i]", context.cg_page + 1, itemList.num_pages) + + local startIdx = context.cg_page * cg.PAGE_ITEMS + 1 + local item + + for itemIdx = 0, cg.PAGE_ITEMS - 1 do + item = itemList.list[startIdx + itemIdx] + + if item then + formspec[#formspec + 1] = string.format("item_image_button[%.2f,%.2f;1,1;%s;cgitem_%s;]", + itemIdx % cg.PAGE_WIDTH, + math.floor(itemIdx / cg.PAGE_WIDTH) + 0.5, + item, item + ) + end + end +end + +local make_craft_preview = function(formspec, player, context) + formspec[#formspec + 1] = [[ + image_button[7.1,0.1;0.8,0.8;cg_plus_icon_prev.png;cg_craft_close;] + image[0.1,0.1;0.8,0.8;gui_hb_bg.png] + ]] + local item = context.cg_selected_item + + formspec[#formspec + 1] = string.format("item_image[0.1,0.1;0.8,0.8;%s]", item) + formspec[#formspec + 1] = string.format("label[1,0;%s]", + cg.crafts[item] and minetest.registered_items[item].description or item) + + local crafts = cg.crafts[item] + + if not crafts or #crafts == 0 then + formspec[#formspec + 1] = string.format("label[1,0.5;%s]", F(cg.S("There are no recipes for this item."))) + return + end + + if #crafts > 1 then + formspec[#formspec + 1] = [[ + image_button[1.85,3.7;0.8,0.8;cg_plus_icon_prev.png;cg_craft_prev;] + image_button[3.85,3.7;0.8,0.8;cg_plus_icon_next.png;cg_craft_next;] + ]] + formspec[#formspec + 1] = string.format("label[2.75,3.8;%i / %i]", context.cg_craft_page + 1, #crafts) + end + + local craft = cg.parse_craft(crafts[context.cg_craft_page + 1]) + local template = cg.craft_types[craft.type] or {} + + if cg.autocrafting and template.uses_crafting_grid then + formspec[#formspec + 1] = "image_button[0.1,3.7;0.8,0.8;cg_plus_icon_autocrafting.png;cg_auto_menu;]" + formspec[#formspec + 1] = string.format("tooltip[cg_auto_menu;%s]", F(cg.S("Craft this recipe"))) + + if context.cg_auto_menu then + local num = 1 + local yPos = 3 + + while true do + num = math.min(num, context.cg_auto_max) + formspec[#formspec + 1] = string.format("button[0.1,%.2f;0.8,0.8;cg_auto_%i;%i]", yPos, num, num) + formspec[#formspec + 1] = string.format( + "tooltip[cg_auto_%i;%s]", + num, + num == 1 and F(cg.S("Craft @1 item", num)) or F(cg.S("Craft @1 items", num)) + ) + + if num < context.cg_auto_max then + num = num * 10 + yPos = yPos - 0.7 + else + break + end + end + end + end + + formspec[#formspec + 1] = string.format("label[5,0.5;%s]", template.description or "") + formspec[#formspec + 1] = string.format("label[5,1;%s]", craft.infotext or "") + formspec[#formspec + 1] = string.format("image[4.75,1.5;1,1;%s]", + template.arrow_icon or "cg_plus_arrow.png") + + local slotSize = math.min(3 / math.max(craft.grid_size.x, craft.grid_size.y), 1) + local xOffset = 4.75 - craft.grid_size.x * slotSize + local yOffset = 2 - craft.grid_size.y * slotSize * 0.5 + + for idx = 1, craft.grid_size.x * craft.grid_size.y do + make_item_button(formspec, + (idx - 1) % craft.grid_size.x * slotSize + xOffset, + math.floor((idx - 1) / craft.grid_size.y) * slotSize + yOffset, + slotSize, + craft.items[idx] + ) + end + + make_item_button(formspec, 5.75, 1.5, 1, craft.output) +end + +sfinv.register_page("cg:craftguide", { + title = "Crafting Guide", + get = function(self, player, context) + local formspec = {[[ + image[0,4.75;1,1;gui_hb_bg.png] + image[1,4.75;1,1;gui_hb_bg.png] + image[2,4.75;1,1;gui_hb_bg.png] + image[3,4.75;1,1;gui_hb_bg.png] + image[4,4.75;1,1;gui_hb_bg.png] + image[5,4.75;1,1;gui_hb_bg.png] + image[6,4.75;1,1;gui_hb_bg.png] + image[7,4.75;1,1;gui_hb_bg.png] + ]]} + + if context.cg_selected_item then + make_craft_preview(formspec, player, context) + else + make_item_grid(formspec, player, context) + end + + return sfinv.make_formspec(player, context, table.concat(formspec), true) + end, + + on_player_receive_fields = function(self, player, context, fields) + if fields.cg_craft_close then + context.cg_selected_item = nil + context.cg_craft_page = nil + context.cg_auto_menu = false + elseif fields.cg_prev and context.cg_page then + context.cg_page = context.cg_page - 1 + elseif fields.cg_next and context.cg_page then + context.cg_page = context.cg_page + 1 + elseif fields.cg_craft_prev and context.cg_craft_page then + context.cg_craft_page = context.cg_craft_page - 1 + context.cg_auto_menu = false + elseif fields.cg_craft_next and context.cg_craft_page then + context.cg_craft_page = context.cg_craft_page + 1 + context.cg_auto_menu = false + elseif fields.cg_search or fields.key_enter_field == "cg_filter" then + cg.update_filter(player, context, fields.cg_filter) + elseif fields.cg_clear then + cg.update_filter(player, context, "") + elseif fields.cg_auto_menu and cg.autocrafting then + if not context.cg_auto_menu then + -- Make sure the craft is valid, in case the client is sending fake formspec fields. + local crafts = cg.crafts[context.cg_selected_item] or {} + local craft = crafts[context.cg_craft_page + 1] + + if craft and cg.craft_types[craft.type] and cg.craft_types[craft.type].uses_crafting_grid then + context.cg_auto_menu = true + context.cg_auto_max = cg.auto_get_craftable(player, craft) + end + else + context.cg_auto_menu = false + end + else + for field, _ in pairs(fields) do + if field:sub(1, 7) == "cgitem_" then + local item = string.sub(field, 8) + + if item:sub(1, 6) == "group:" then + if cg.group_search then + cg.update_filter(player, context, item:gsub("/", ",")) + context.cg_selected_item = nil + context.cg_auto_menu = false + elseif cg.group_stereotypes[item:sub(7)] then + context.cg_selected_item = cg.group_stereotypes[item:sub(7)] + context.cg_craft_page = 0 + context.cg_auto_menu = false + end + elseif item ~= context.cg_selected_item then + context.cg_selected_item = item + context.cg_craft_page = 0 + context.cg_auto_menu = false + end + + break + elseif field:sub(1, 8) == "cg_auto_" and cg.autocrafting and context.cg_auto_menu then + -- No need to sanity check, we already did that when showing the autocrafting menu. + local num = tonumber(field:sub(9)) + + if num > 0 and num <= context.cg_auto_max then + cg.auto_craft(player, cg.crafts[context.cg_selected_item][context.cg_craft_page + 1], num) + sfinv.set_page(player, "sfinv:crafting") + end + + context.cg_auto_menu = false + break + end + end + end + + -- Wrap around when the player presses the next button on the last page, or the previous button on the first. + if context.cg_page then + context.cg_page = context.cg_page % math.max(cg.get_item_list(player).num_pages, 1) + end + + if context.cg_craft_page then + context.cg_craft_page = context.cg_craft_page % math.max(#(cg.crafts[context.cg_selected_item] or {}), 1) + end + + -- Update the formspec. + sfinv.set_player_inventory_formspec(player, context) + end, +}) diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..eddb767 --- /dev/null +++ b/mod.conf @@ -0,0 +1,3 @@ +name = cg_plus +description = An intuitive and full-featured craft guide and autocrafting mod which is compatible with sfinv. +depends = sfinv diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..ecc38be --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,8 @@ +# Enable the option to automatically move items to the craft grid from a craft preview window. +cg_plus_autocrafting (Autocrafting) bool true + +# Search for items by group when a search begins with "group:". +cg_plus_group_search (Search by item groups) bool true + +# Limit the number of groups which can be searched for at once when group search is enabled. +cg_plus_group_search_max (Group search max) int 5 1 100 diff --git a/textures/cg_plus_arrow.png b/textures/cg_plus_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..f58c9a5b6555b0a660b0094c28df7b972f156a16 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv{s5m4S0L@{>l+yvnVg)QmzQ_{ z@;`B)5MxP@UoeBivm0qZPMD{QV~EE2y%!Ji9#G(6ar9osFqyAOdO`bzdf!4F#e>hz z+q(a@o_JW>{soKr7QX_fNe%j%$?q~}w&)i-sW33#QHpxLj%QjXo3%s&KNC-b!xawp b_f?D^CHSi+Z~1>1Xaj?%tDnm{r-UW|R>V3w literal 0 HcmV?d00001 diff --git a/textures/cg_plus_arrow_small.png b/textures/cg_plus_arrow_small.png new file mode 100644 index 0000000000000000000000000000000000000000..5b8bca166317d8724b199c946fc9e04906d5410e GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv{s5m4*VK|qUtizI$jId6c;Xq#TSIWfjL$*cwI^cp_bu;iHR%VthG$o8Oi?zz1{ PLm50>{an^LB{Ts54b3zX literal 0 HcmV?d00001 diff --git a/textures/cg_plus_icon_autocrafting.png b/textures/cg_plus_icon_autocrafting.png new file mode 100644 index 0000000000000000000000000000000000000000..b02f0da7928226cef6837f16970bd6169f0c30b6 GIT binary patch literal 201 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 literal 0 HcmV?d00001 diff --git a/textures/cg_plus_icon_prev.png b/textures/cg_plus_icon_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..b26cd157f95aec853fce734574e5d319686b8399 GIT binary patch literal 728 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 literal 0 HcmV?d00001