crafting/api.lua

307 lines
7.8 KiB
Lua

-- Crafting Mod - semi-realistic crafting in minetest
-- Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
--
-- This library is free software; you can redistribute it and/or
-- modify it under the terms of the GNU Lesser General Public
-- License as published by the Free Software Foundation; either
-- version 2.1 of the License, or (at your option) any later version.
--
-- This library is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-- Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public
-- License along with this library; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
crafting = {
recipes = {},
recipes_by_id = {},
registered_on_crafts = {},
}
function crafting.register_type(name)
crafting.recipes[name] = {}
end
local recipe_counter = 0
function crafting.register_recipe(def)
assert(def.output, "Output needed in recipe definition")
assert(def.type, "Type needed in recipe definition")
assert(def.items, "Items needed in recipe definition")
def.level = def.level or 1
local tab = crafting.recipes[def.type]
assert(tab, "Unknown craft type " .. def.type)
recipe_counter = recipe_counter + 1
def.id = recipe_counter
crafting.recipes_by_id[recipe_counter] = def
tab[#tab + 1] = def
return def.id
end
local unlocked_cache = {}
function crafting.get_unlocked(name)
local player = minetest.get_player_by_name(name)
if not player then
minetest.log("warning", "Crafting doesn't support getting unlocks for offline players")
return {}
end
local retval = unlocked_cache[name]
if not retval then
retval = minetest.parse_json(
player:get_meta():get("crafting:unlocked") or "{}")
unlocked_cache[name] = retval
end
assert(retval)
return retval
end
if minetest then
minetest.register_on_leaveplayer(function(player)
unlocked_cache[player:get_player_name()] = nil
end)
end
local function write_json_dictionary(value)
if next(value) then
return minetest.write_json(value)
else
return "{}"
end
end
function crafting.lock_all(name)
local player = minetest.get_player_by_name(name)
if not player then
minetest.log("warning", "Crafting doesn't support setting unlocks for offline players")
return {}
end
local unlocked = crafting.get_unlocked(name)
for key, _ in pairs(unlocked) do
unlocked[key] = nil
end
unlocked_cache[name] = unlocked
player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked))
end
function crafting.unlock(name, output)
local player = minetest.get_player_by_name(name)
if not player then
minetest.log("warning", "Crafting doesn't support setting unlocks for offline players")
return {}
end
local unlocked = crafting.get_unlocked(name)
if type(output) == "table" then
for i=1, #output do
unlocked[output[i]] = true
minetest.chat_send_player(name, "You've unlocked " .. output[i])
end
else
unlocked[output] = true
minetest.chat_send_player(name, "You've unlocked " .. output)
end
unlocked_cache[name] = unlocked
player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked))
end
function crafting.get_recipe(id)
return crafting.recipes_by_id[id]
end
function crafting.get_all(type, level, item_hash, unlocked)
assert(crafting.recipes[type], "No such craft type!")
local results = {}
for _, recipe in pairs(crafting.recipes[type]) do
local craftable = true
if recipe.level <= level and (recipe.always_known or unlocked[recipe.output]) then
-- Check all ingredients are available
local items = {}
for _, item in pairs(recipe.items) do
item = ItemStack(item)
local needed_count = item:get_count()
local available_count = item_hash[item:get_name()] or 0
if available_count < needed_count then
craftable = false
end
items[#items + 1] = {
name = item:get_name(),
have = available_count,
need = needed_count,
}
end
results[#results + 1] = {
recipe = recipe,
items = items,
craftable = craftable,
}
end
end
return results
end
function crafting.set_item_hashes_from_list(inv, listname, item_hash)
for _, stack in pairs(inv:get_list(listname)) do
if not stack:is_empty() then
local itemname = stack:get_name()
item_hash[itemname] = (item_hash[itemname] or 0) + stack:get_count()
local def = minetest.registered_items[itemname]
if def and def.groups then
for groupname, _ in pairs(def.groups) do
local group = "group:" .. groupname
item_hash[group] = (item_hash[group] or 0) + stack:get_count()
end
end
end
end
end
function crafting.get_all_for_player(player, type, level)
local unlocked = crafting.get_unlocked(player:get_player_name())
-- Get items hashed
local item_hash = {}
crafting.set_item_hashes_from_list(player:get_inventory(), "main", item_hash)
return crafting.get_all(type, level, item_hash, unlocked)
end
function crafting.can_craft(name, type, level, recipe)
local unlocked = crafting.get_unlocked(name)
return recipe.type == type and recipe.level <= level and
(recipe.always_known or unlocked[recipe.output])
end
local function give_all_to_player(inv, list)
for _, item in pairs(list) do
inv:add_item("main", item)
end
end
function crafting.find_required_items(inv, listname, recipe)
local items = {}
for _, item in pairs(recipe.items) do
item = ItemStack(item)
local itemname = item:get_name()
if item:get_name():sub(1, 6) == "group:" then
local groupname = itemname:sub(7, #itemname)
local required = item:get_count()
-- Find stacks in group
for i = 1, inv:get_size(listname) do
local stack = inv:get_stack(listname, i)
-- Is it in group?
local def = minetest.registered_items[stack:get_name()]
if def and def.groups and def.groups[groupname] then
stack = ItemStack(stack)
if stack:get_count() > required then
stack:set_count(required)
end
items[#items + 1] = stack
required = required - stack:get_count()
if required == 0 then
break
end
end
end
if required > 0 then
return nil
end
else
if inv:contains_item(listname, item) then
items[#items + 1] = item
else
return nil
end
end
end
return items
end
function crafting.has_required_items(inv, listname, recipe)
return crafting.find_required_items(inv, listname, recipe) ~= nil
end
function crafting.register_on_craft(func)
table.insert(crafting.registered_on_crafts, func)
end
function crafting.perform_craft(name, inv, listname, outlistname, recipe)
local items = crafting.find_required_items(inv, listname, recipe)
if not items then
return false
end
-- Take items
local taken = {}
for _, item in pairs(items) do
item = ItemStack(item)
local took = inv:remove_item(listname, item)
taken[#taken + 1] = took
if took:get_count() ~= item:get_count() then
minetest.log("error", "Unexpected lack of items in inventory")
give_all_to_player(inv, taken)
return false
end
end
for i=1, #crafting.registered_on_crafts do
crafting.registered_on_crafts[i](name, recipe)
end
-- Add output
if inv:room_for_item("main", recipe.output) then
inv:add_item(outlistname, recipe.output)
else
local pos = minetest.get_player_by_name(name):get_pos()
minetest.chat_send_player(name, "No room in inventory!")
minetest.add_item(pos, recipe.output)
end
return true
end
local function to_hex(str)
return (str:gsub('.', function (c)
return string.format('%02X', string.byte(c))
end))
end
function crafting.calc_inventory_list_hash(inv, listname)
local str = ""
for _, stack in pairs(inv:get_list(listname)) do
str = str .. stack:get_name() .. stack:get_count()
end
return minetest.sha1(to_hex(str))
end