From 6511eb8261cb1c292228e76c0906b642e26f2a16 Mon Sep 17 00:00:00 2001 From: random-geek <35757396+random-geek@users.noreply.github.com> Date: Sun, 21 Apr 2024 01:39:27 -0700 Subject: [PATCH] Bunch of updates - Update and document public API - Fixes to group handling - New clear icon --- README.md | 12 ++- api.lua | 138 ++++++++++++++++++++------------ developer_docs.md | 93 +++++++++++++++++++++ init.lua | 41 ++++------ inventory.lua | 49 +++++++----- textures/cg_plus_icon_clear.png | Bin 132 -> 143 bytes 6 files changed, 235 insertions(+), 98 deletions(-) create mode 100644 developer_docs.md diff --git a/README.md b/README.md index eea50ca..df3446c 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,22 @@ Note: If `mtg_craftguide` is present, CGP will override its page in the inventor ## 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. +- "Intelligent" auto-crafting, or rather, automatic craft staging. - Shaped and shapeless crafting recipe previews of any size. - Fuel and cooking recipes, including fuel replacements and burning/cooking times. - Digging and digging by chance (item drop) previews. +- Group support, including group search (try searching `group:wood` in the craft guide) and support for craft recipes + requiring items in one or more groups. +- Various settings for server owners + +## For Mod/Game Developers + +For information on the Crafting Guide Plus API and other tips for modders, see [developer_docs.md](developer_docs.md). ## 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. + greatest count from the player's inventory will be utilized. - Items in multiple groups will not always display correctly in craft view. ## License diff --git a/api.lua b/api.lua index 9039a69..22d59ae 100644 --- a/api.lua +++ b/api.lua @@ -1,5 +1,7 @@ -- TODO: aliases? +local custom_crafts = {} + local function get_drops(item, def) local normalDrops = {} local randomDrops = {} @@ -55,18 +57,65 @@ local function get_drops(item, def) return normalDrops, randomDrops end +local function build_group_stereotypes_list() + -- Remember: Some group stereotypes are already registered + local startTime = minetest.get_us_time() + local usedMultiGroups = {} + + for _, recipes in pairs(cg.crafts) do + for _, recipe in ipairs(recipes) do + for _, item in ipairs(recipe.items) do + if item:sub(1, 6) == "group:" then + local groupsString = item:sub(7) + local groupsTable = groupsString:split(",") + if #groupsTable > 1 then + usedMultiGroups[groupsString] = groupsTable + end + end + end + end + end + + for _, item in ipairs(cg.items_all.list) do + local groups = minetest.registered_items[item].groups + + for group, _ in pairs(groups) do + if cg.group_stereotypes[group] == nil then + cg.group_stereotypes[group] = item + end + end + + for clusterString, clusterTable in pairs(usedMultiGroups) do + if cg.group_stereotypes[clusterString] == nil then + local match = true + for _, group in ipairs(clusterTable) do + if not groups[group] then + match = false + break + end + end + if match then + cg.group_stereotypes[clusterString] = item + end + end + end + end + + minetest.log("info", string.format("[cg_plus] Finished building group stereotype list in %.3f s.", + (minetest.get_us_time() - startTime) / 1000000)) +end + function cg.build_item_list() 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 - and minetest.get_item_group(item, - "not_in_craft_guide") == 0 then + and minetest.get_item_group(item, "not_in_creative_inventory") == 0 + and minetest.get_item_group(item, "not_in_craft_guide") == 0 then table.insert(cg.items_all.list, item) cg.crafts[item] = minetest.get_all_craft_recipes(item) or {} + table.insert_all(cg.crafts[item], custom_crafts[item] or {}) end end @@ -83,7 +132,7 @@ function cg.build_item_list() if fuel.time > 0 then table.insert(cg.crafts[item], { - type = "fuel", + method = "fuel", items = {item}, output = decremented.items[1]:to_string(), time = fuel.time, @@ -96,7 +145,7 @@ function cg.build_item_list() for dItem, dCount in pairs(normalDrops) do if cg.crafts[dItem] then table.insert(cg.crafts[dItem], { - type = "digging", + method = "digging", width = 0, items = {item}, output = ItemStack({ @@ -110,7 +159,7 @@ function cg.build_item_list() for dItem, dCount in pairs(randomDrops) do if cg.crafts[dItem] then table.insert(cg.crafts[dItem], { - type = "digging_chance", + method = "digging_chance", width = 0, items = {item}, output = ItemStack({ @@ -121,12 +170,6 @@ function cg.build_item_list() 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) @@ -138,6 +181,8 @@ function cg.build_item_list() (minetest.get_us_time() - startTime) / 1000000 ) ) + + build_group_stereotypes_list() end function cg.filter_items(player, filter) @@ -192,56 +237,51 @@ function cg.filter_items(player, filter) end function cg.parse_craft(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 "" + local method + if craft.method == "normal" and craft.width == 0 then -- Special rules for shapeless recipes + method = "shapeless" + else + method = craft.method end - local width = math.max(craft.width or 0, 1) + local template = cg.craft_methods[method] or {} - 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 + local gridSize = (template.get_grid_size and template.get_grid_size(craft)) or {x = 3, y = 3} + local width = craft.width or 0 + local items = {} - if template.inherit_width then - -- For shapeless recipes, there is no need to modify the item list. - newCraft.items = craft.items + if width == 0 then + -- Shapeless recipes + 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 + -- The craft's width is not always the same as the grid size, so items need to be shifted around. + for i, item in pairs(craft.items) do + items[i + (gridSize.x - width) * math.floor((i - 1) / width)] = item end end - return newCraft + return { + method = method, + infotext = (template.get_infotext and template.get_infotext(craft)) or "", + grid_size = gridSize, + width = width, + items = items, + output = craft.output or "", + } end function cg.get_item_list(player) return cg.player_data[player:get_player_name()].items or cg.items_all end -function cg.register_craft_type(name, def) - cg.craft_types[name] = def +function cg.register_crafting_method(name, def) + cg.craft_methods[name] = def +end + +function cg.register_craft(recipe, assign_to) + local item = ItemStack(assign_to or recipe.output):get_name() -- Removes quantity, etc. from itemstring + custom_crafts[item] = custom_crafts[item] or {} + table.insert(custom_crafts[item], recipe) end function cg.register_group_stereotype(group, item) diff --git a/developer_docs.md b/developer_docs.md new file mode 100644 index 0000000..dcfbbbb --- /dev/null +++ b/developer_docs.md @@ -0,0 +1,93 @@ +# Crafting Guide Plus developer documentation + +## Groups + +cg_plus respects the standard groups `not_in_creative_inventory` and `not_in_craft_guide`. +Adding either of these to an item/node definition will make it not appear in the crafting guide. + +## API + +### `cg.register_crafting_method(name, def)` + +Adds or overrides a type of craft in cg_plus, for use with `cg.register_craft`. Default craft methods are `normal`, +`shapeless`, `cooking`, `fuel`, `digging`, and `digging_chance`. + +#### Parameters + +* `name` (string): The name of the new craft method. Matches the `method` field in craft definitions. +* `def` (table): A craft method definition with the following fields: + * `description` (string): A human-readable name for the crafting method, which will be shown next to recipes in the + crafting guide. + * `arrow_icon` (string): Texture to use for the arrow icon in recipes. Default is a plain arrow. + * `uses_crafting_grid` (bool): Setting to `true` allows crafts of this method to be automatically staged in the + default crafting grid when autocrafting is enabled. + * `get_grid_size = function(craft)`: Used to calculate the shape of the crafting grid displayed for each recipe. + Takes a recipe defintion `craft` and returns a table in the format `{x = width, y = height}`. + * `get_infotext = function(craft)`: Optional, used to add additional information to a recipe page, e.g. cooking or + burning times. Takes a recipe defintion `craft` and returns a string. + +#### Example + +See below. + +### `cg.register_craft(recipe, [assign_to])` + +Registers a craft to appear only in the crafting guide, independent of `minetest.register_craft`. Useful for mods that +implement crafting outside the default crafting grid. + +#### Parameters + +* `recipe` (table): Possible keys: + * `method` (string): Can be an official crafting method or one created with `cg.register_craft`. + * `width` (integer): Width of the recipe inputs, which may be less than the width of the crafting grid. If zero, the + recipe will expand to the full width of the crafting grid. + * `items` (table): One-dimensional table of input item names, listed from left-to-right and top-to-bottom. May be + groups such as `group:dye` or `group:dye,color_violet`. + * `output` (string): Output itemstring, e.g. `default:stone` or `default:wood 4`. + * Additional fields can be added (e.g. cooking time) which can be displayed using `get_infotext` in + `cg.register_crafting_method`. The `items` and `width` fields are reserved. +* `assign_to` (string): Optional itemstring; if specified, the craft will be assigned to this item rather than the + output item. Useful for fuel recipes that consume the input, etc. + +#### Example + +Register a craft for a theoretical mod `woodmod` which allows sawing of stairs using a table saw: + +```lua +cg.register_crafting_method("woodmod_table_saw", { + description = "Table Saw", + arrow_icon = "cg_plus_arrow_bottom.png^woodmod_icon_saw.png", + uses_crafting_grid = false, + get_grid_size = function(craft) + return {x = 4, y = 4} + end, + get_infotext = function(craft) + return string.format("Cutting time: %i seconds", craft.cutting_time) + end, +}) + +cg.register_craft({ + method = "woodmod_table_saw", + width = 2, + items = {"group:wood", "", "group:wood", "group:wood"}, + output = "stairs:stair_wood 4", + cutting_time = 10, +}) +``` + +### `cg.register_group_stereotype(group, item)` + +Adds or overrides a group stereotype. When a recipe takes a generic item in the given group, the given item will be +displayed instead of a randomly-chosen item in that group. Clicking on the item button with group search disabled will +also search for the stereotype item. + +`group` can be multiple comma-separated groups (e.g. `dye,color_blue`) for use by recipes with multi-group items. The +order of the groups *does* matter. + +#### Example + +Show yellow dye as the default for items in the `dye` group: + +```lua +cg.register_group_stereotype("dye", "dye:yellow") +``` diff --git a/init.lua b/init.lua index 4b3d2de..8cf1ad7 100644 --- a/init.lua +++ b/init.lua @@ -4,7 +4,7 @@ cg = { items_all = {}, player_data = {}, crafts = {}, - craft_types = {}, + craft_methods = {}, group_stereotypes = {}, } @@ -26,10 +26,9 @@ end dofile(path .. "/inventory.lua") -cg.register_craft_type("normal", { - description = F(cg.S("Crafting")), +cg.register_crafting_method("normal", { + description = cg.S("Crafting"), uses_crafting_grid = true, - alt_zero_width = "shapeless", get_grid_size = function(craft) local width = math.max(craft.width, 1) @@ -44,9 +43,8 @@ cg.register_craft_type("normal", { end }) -cg.register_craft_type("shapeless", { - description = F(cg.S("Mixing")), - inherit_width = true, +cg.register_crafting_method("shapeless", { + description = cg.S("Mixing"), uses_crafting_grid = true, get_grid_size = function(craft) @@ -61,9 +59,8 @@ cg.register_craft_type("shapeless", { end }) -cg.register_craft_type("cooking", { - description = F(cg.S("Cooking")), - inherit_width = true, +cg.register_crafting_method("cooking", { + description = cg.S("Cooking"), arrow_icon = "cg_plus_arrow_bottom.png^cg_plus_icon_cooking.png", get_grid_size = function(craft) @@ -71,14 +68,12 @@ cg.register_craft_type("cooking", { end, get_infotext = function(craft) - return minetest.colorize("#FFFF00", - F(cg.S("Time: @1 s", craft.width or 0))) + return minetest.colorize("#FFFF00", cg.S("Time: @1 s", craft.width or 0)) end }) -cg.register_craft_type("fuel", { - description = F(cg.S("Fuel")), - inherit_width = true, +cg.register_crafting_method("fuel", { + description = cg.S("Fuel"), arrow_icon = "cg_plus_arrow_bottom.png^cg_plus_icon_fuel.png", get_grid_size = function(craft) @@ -86,14 +81,12 @@ cg.register_craft_type("fuel", { end, get_infotext = function(craft) - return minetest.colorize("#FFFF00", - F(cg.S("Time: @1 s", craft.time or 0))) + return minetest.colorize("#FFFF00", cg.S("Time: @1 s", craft.time or 0)) end }) -cg.register_craft_type("digging", { - description = F(cg.S("Digging")), - inherit_width = true, +cg.register_crafting_method("digging", { + description = cg.S("Digging"), arrow_icon = "cg_plus_arrow_bottom.png^cg_plus_icon_digging.png", get_grid_size = function(craft) @@ -101,9 +94,8 @@ cg.register_craft_type("digging", { end }) -cg.register_craft_type("digging_chance", { - description = F(cg.S("Digging@n(by chance)")), - inherit_width = true, +cg.register_crafting_method("digging_chance", { + description = cg.S("Digging@n(by chance)"), arrow_icon = "cg_plus_arrow_bottom.png^cg_plus_icon_digging.png", get_grid_size = function(craft) @@ -111,8 +103,7 @@ cg.register_craft_type("digging_chance", { end }) -cg.register_group_stereotype("mesecon_conductor_craftable", - "mesecons:wire_00000000_off") +cg.register_group_stereotype("mesecon_conductor_craftable", "mesecons:wire_00000000_off") if minetest.get_modpath("default") then cg.register_group_stereotype("stone", "default:stone") diff --git a/inventory.lua b/inventory.lua index baefa0e..fef03ae 100644 --- a/inventory.lua +++ b/inventory.lua @@ -24,22 +24,31 @@ function cg.update_selected_item(player, context, item, force) context.cg_auto_menu = false end -local function make_item_button(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(",") +local function make_item_button(formspec, x, y, size, itemstring) + if itemstring and itemstring ~= "" then + local itemName = itemstring:match("^%S+") -- Remove quantity, etc. Note: may be a group item. + + local groups, shownItem, buttonID, buttonText + if itemName:sub(1, 6) == "group:" then + local groupString = itemName:sub(7) + groups = groupString:split(",") + if #groups == 1 then + shownItem = cg.group_stereotypes[groups[1]] or "" + elseif #groups > 1 then + shownItem = cg.group_stereotypes[groupString] or "" + end + -- shownItem = (cg.group_stereotypes[groupString] or cg.group_stereotypes[groups[1]]) or "" + buttonID = itemName:gsub(",", "/") buttonText = #groups == 1 and "G" or ("G " .. #groups) - name = name:gsub(",", "/") + else + shownItem = itemstring + buttonID = itemName + buttonText = "" 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 "" + x, y, size, size, shownItem, buttonID, buttonText ) if groups then @@ -56,8 +65,7 @@ local function make_item_button(formspec, x, y, size, name) )) end - formspec[#formspec + 1] = string.format("tooltip[cgitem_%s;%s]", - name, tooltipText) + formspec[#formspec + 1] = string.format("tooltip[cgitem_%s;%s]", buttonID, tooltipText) end else formspec[#formspec + 1] = string.format( @@ -147,7 +155,7 @@ local function make_craft_preview(formspec, player, context) end local craft = cg.parse_craft(crafts[context.cg_craft_page + 1]) - local template = cg.craft_types[craft.type] or {} + local template = cg.craft_methods[craft.method] or {} -- Auto-crafting buttons if cg.AUTOCRAFTING and template.uses_crafting_grid then @@ -184,11 +192,9 @@ local function make_craft_preview(formspec, player, context) end end - -- Craft type/info text - formspec[#formspec + 1] = string.format("label[6.7,1.8;%s]", - template.description or "") - formspec[#formspec + 1] = string.format("label[6.7,2.4;%s]", - craft.infotext or "") + -- Craft method/infotext + formspec[#formspec + 1] = string.format("label[6.7,1.8;%s]", F(template.description) or "") + formspec[#formspec + 1] = string.format("label[6.7,2.4;%s]", F(craft.infotext) or "") -- Draw craft item grid, feat. maths. @@ -265,8 +271,8 @@ local function page_on_player_receive_fields(self, player, context, 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 + if craft and cg.craft_methods[craft.method] + and cg.craft_methods[craft.method].uses_crafting_grid then context.cg_auto_menu = true context.cg_auto_max = cg.auto_get_craftable(player, craft) end @@ -279,6 +285,7 @@ local function page_on_player_receive_fields(self, player, context, fields) local item = string.sub(field, 8) if item:sub(1, 6) == "group:" then + item = item:gsub("/", ",") if cg.GROUP_SEARCH then cg.update_filter(player, context, item) cg.update_selected_item(player, context, nil) diff --git a/textures/cg_plus_icon_clear.png b/textures/cg_plus_icon_clear.png index 73d8cea6f69f79cea47dd9bc3ffdff50f0aa31be..279d85fc4c8e258f84adfdee7b2d38ded71f573c 100644 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^f*{Pq3?$u{*JlDLo&cW^*9|+EXU$uD^w^2pw{Kes zmAnRuGnNGT1v5B2yO9Ru7<#%mhHzX@PB_47XJF27(zJ%pA?TKn%As#8E=IZx6KgCL oE(J-s*d;QroO9qs!(|X>+aNg2(&163*@PVj cxFVdQ&MBb@02U4>umAu6