Camas/mods/crafting/api.lua
2020-03-10 08:26:42 +01:00

615 lines
18 KiB
Lua

local S = minetest.get_translator("crafting")
-- Crafting menu display modes
local MODE_CRAFTABLE = 1 -- crafting guide mode, show all recipes (default)
local MODE_GUIDE = 2 -- craftable mode, only show recipes craftable from input slots
--
-- API
--
crafting = {}
-- Callbacks
crafting.callbacks = {}
-- Array of registered craft recipes
crafting.registered_crafts = {}
-- User table of last selected row etc.
crafting.userdata = {}
-- Crafting can only take 4 itemstacks as input for sanity/interface reasons
crafting.max_inputs = 4
-- Default crafting definition values
crafting.default_craftdef = {
output = nil,
items = {},
groups = {},
}
function crafting.register_craft(def)
if def.output == nil then
minetest.log("warning",
"No output for craft recipe")
return
end
local itemstack = ItemStack(def.output)
local itemkey = itemstack:to_string()
-- Each output item may only be used once. There can't be 2 recipes with
-- the exact same output
assert(crafting.registered_crafts[itemkey] == nil, "Crafting recipe collision! itemkey="..itemkey.." def="..dump(def))
if not minetest.registered_items[itemstack:get_name()] then
minetest.log("warning",
"Trying to register craft " .. itemkey
.. " that has an unknown output item")
end
local craftdef = {
output = itemstack,
items = def.items or crafting.default_craftdef.items,
groups = def.groups or crafting.default_craftdef.groups,
}
if #craftdef.items > crafting.max_inputs then
minetest.log("warning",
"Attempting to register craft " .. itemkey .." with more than "
.. crafting.max_inputs .. " inputs, allowing")
end
for i = 1, crafting.max_inputs do
if craftdef.items[i] ~= nil then
craftdef.items[i] = ItemStack(craftdef.items[i])
end
end
crafting.registered_crafts[itemkey] = craftdef
minetest.log("info", "Registered recipe for " .. itemkey)
end
function crafting.get_crafts(player_inventory)
local results = {}
local function get_filtered()
for craftname, craftdef in pairs(crafting.registered_crafts) do
local contains_all = true
for c=1, #craftdef.items do
local name = craftdef.items[c]:get_name()
if string.sub(name, 1, 6) == "group:" then
local group = string.sub(name, 7)
local gcount = craftdef.items[c]:get_count()
local items_in = player_inventory:get_list("craft_in")
if items_in == nil then
contains_all = false
break
end
local count = 0
for i=1, #items_in do
if minetest.get_item_group(items_in[i]:get_name(), group) ~= 0 then
count = count + items_in[i]:get_count()
end
end
if count < gcount then
contains_all = false
break
end
elseif not player_inventory:contains_item("craft_in", craftdef.items[c]) then
contains_all = false
break
end
end
if contains_all then
table.insert(results, craftname)
end
end
end
local function get_all()
for craftname, _ in pairs(crafting.registered_crafts) do
table.insert(results, craftname)
end
end
if player_inventory == nil then
get_all()
else
get_filtered()
end
local function sort_function(a, b)
local a_itemn = ItemStack(a):get_name()
local b_itemn = ItemStack(b):get_name()
local a_name = minetest.registered_items[a_itemn].description
local b_name = minetest.registered_items[b_itemn].description
return a_name < b_name
end
table.sort(results, sort_function)
return results
end
function crafting.register_on_craft(func)
if not crafting.callbacks.on_craft then
crafting.callbacks.on_craft = {}
end
table.insert(crafting.callbacks.on_craft, func)
end
function crafting.craft(player, wanted, wanted_count, output, items)
-- `output` can be any ItemStack value
-- Duplicate items in `items` should work correctly
if wanted:is_empty() then
return nil
end
local craftdef = crafting.registered_crafts[wanted:to_string()]
if craftdef == nil then
minetest.log("warning",
"Tried to craft an unregistered item " .. wanted:to_string())
return nil
end
-- Check for validity
local craft_count = wanted_count
for i = 1, crafting.max_inputs do
local required_itemstack = ItemStack(craftdef.items[i])
local itemc = 0
local group = string.match(required_itemstack:get_name(), "group:(.*)")
if required_itemstack ~= nil and required_itemstack:get_count() ~= 0 then
for j = 1, crafting.max_inputs do
local input_itemstack = ItemStack(items[j])
if (group ~= nil
and minetest.get_item_group(input_itemstack:get_name(), group) ~= 0)
or (input_itemstack ~= nil
and input_itemstack:get_name() == required_itemstack:get_name()) then
itemc = itemc + input_itemstack:get_count()
end
end
craft_count = math.min(craft_count,
math.floor(itemc / required_itemstack:get_count()))
if craft_count < 1 then
return nil -- Not enough items to craft
end
end
end
-- Put stuff in output stack
local free_space = wanted:get_stack_max() - output:get_count()
if free_space < (craft_count * craftdef.output:get_count()) then
craft_count = math.floor(free_space / craftdef.output:get_count())
end
if craft_count < 1 then
return nil -- Can't hold any output
end
output:add_item(
ItemStack({
name = craftdef.output:get_name(),
count = craftdef.output:get_count() * craft_count
}))
-- Iterate through second time to take items used for crafting
local function remove_used_item(itemn, count)
local items_required = count
local group = string.match(itemn, "group:(.*)")
for i = 1, crafting.max_inputs do
local input_itemstack = ItemStack(items[i])
if (group ~= nil
and minetest.get_item_group(input_itemstack:get_name(), group) ~= 0)
or (items[i] ~= nil
and input_itemstack:get_name() == itemn) then
local items_left = items_required - input_itemstack:get_count()
input_itemstack:take_item(items_required)
if items_left > 0 then
items_required = items_required - (items_required - items_left)
else
items[i] = input_itemstack:to_table()
break
end
items[i] = input_itemstack:to_table()
end
end
end
for i = 1, crafting.max_inputs do
local required_itemstack = ItemStack(craftdef.items[i])
if craftdef.items[i] ~= nil then
remove_used_item(required_itemstack:get_name(),
required_itemstack:get_count() * craft_count)
end
end
for _, func in ipairs(crafting.callbacks.on_craft) do
for i = 1, (craftdef.output:get_count() * craft_count) do
func(output, player)
end
end
return {items = items, output = output}
end
local form = default.ui.get_page("default:2part")
form = form .. "field[-1,-1;0,0;crafting_tracker;;]"
form = form .. "list[current_player;main;0.25,4.75;8,4;]"
form = form .. default.ui.get_hotbar_itemslot_bg(0.25, 4.75, 8, 1)
form = form .. default.ui.get_itemslot_bg(0.25, 5.75, 8, 3)
form = form .. "list[current_player;craft_in;0.25,0.25;1,4;]"
form = form .. "list[current_player;craft_out;7.25,3.25;1,1;]"
form = form .. "listring[current_player;main]"
form = form .. "listring[current_player;craft_in]"
form = form .. "listring[current_player;main]"
form = form .. "listring[current_player;craft_out]"
form = form .. default.ui.get_itemslot_bg(0.25, 0.25, 1, 4)
form = form .. default.ui.get_output_itemslot_bg(7.25, 3.25, 1, 1)
form = form .. default.ui.button(7.25, 1.25, 1, 1, "do_craft_1", "1", nil, S("Craft once"))
form = form .. default.ui.button(7.25, 2.25, 1, 1, "do_craft_10", "10", nil, S("Craft 10 times"))
form = form .. "tablecolumns[text,align=left,width=2;text,align=left,width=40]"
default.ui.register_page("crafting:crafting", form)
function crafting.get_formspec(name, select_item)
local row = 1
if crafting.userdata[name] ~= nil then
row = crafting.userdata[name].row
end
local inv = minetest.get_player_by_name(name):get_inventory()
local craft_list = ""
local craftitems
if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
craftitems = crafting.get_crafts()
else
craftitems = crafting.get_crafts(inv)
end
if select_item == nil then
if row > #craftitems then
row = #craftitems
if crafting.userdata[name] ~= nil then
crafting.userdata[name].row = row
end
elseif row < 1 and #craftitems >= 1 then
row = 1
if crafting.userdata[name] ~= nil then
crafting.userdata[name].row = row
end
end
end
local selected_craftdef = nil
local craft_count = 0
for i, itemn in ipairs(craftitems) do
local itemstack = ItemStack(itemn)
local itemname = itemstack:get_name()
local itemdef = minetest.registered_items[itemname]
if select_item then
if itemn == select_item then
selected_craftdef = crafting.registered_crafts[itemn]
row = i
if crafting.userdata[name] ~= nil then
crafting.userdata[name].row = row
end
end
elseif i == row then
selected_craftdef = crafting.registered_crafts[itemn]
end
if itemdef ~= nil then
local craftdef = crafting.registered_crafts[itemn]
if craft_list ~= "" then
craft_list = craft_list .. ","
end
if itemstack:get_count() ~= 1 then
craft_list = craft_list .. minetest.formspec_escape(itemstack:get_count())
end
local desc = itemdef.description
-- Cut off item description after first newline
local firstnewline = string.find(desc, "\n")
if firstnewline then
desc = string.sub(desc, 1, firstnewline-1)
end
craft_list = craft_list .. "," .. minetest.formspec_escape(desc)
craft_count = craft_count + 1
end
end
if select_item and (not selected_craftdef) and #craftitems > 0 then
row = 1
selected_craftdef = crafting.registered_crafts[craftitems[row]]
if crafting.userdata[name] ~= nil then
crafting.userdata[name].row = row
end
end
local form = default.ui.get_page("crafting:crafting")
if craft_count > 0 then
form = form .. "table[2.25,0.25;4.75,3.75;craft_list;" .. craft_list
.. ";" .. row .. "]"
end
if selected_craftdef ~= nil then
if selected_craftdef.items[1] ~= nil then
form = form .. default.ui.fake_itemstack_any(
1.25, 0.25, selected_craftdef.items[1], "craftex_in_1")
end
if selected_craftdef.items[2] ~= nil then
form = form .. default.ui.fake_itemstack_any(
1.25, 1.25, selected_craftdef.items[2], "craftex_in_2")
end
if selected_craftdef.items[3] ~= nil then
form = form .. default.ui.fake_itemstack_any(
1.25, 2.25, selected_craftdef.items[3], "craftex_in_3")
end
if selected_craftdef.items[4] ~= nil then
form = form .. default.ui.fake_itemstack_any(
1.25, 3.25, selected_craftdef.items[4], "craftex_in_4")
end
if selected_craftdef.output ~= nil then
form = form .. default.ui.fake_itemstack_any(
7.25, 0.25, selected_craftdef.output, "craftex_out")
end
end
local guide_icon, guide_tip
if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
guide_icon = "ui_icon_craftingguide_active.png"
guide_tip = S("Show only craftable recipes")
else
guide_icon = "ui_icon_craftingguide.png"
guide_tip = S("Show all recipes")
end
form = form .. default.ui.tab(8.4, 0.5, "toggle_filter", guide_icon, guide_tip, "right")
return form
end
local function clear_craft_slots(player)
local inv = player:get_inventory()
-- Move items out of input and output slots
local items_moved = false
local pos = player:get_pos()
local lists = { "craft_out", "craft_in" }
for l = 1, #lists do
local list = lists[l]
for i = 1, inv:get_size(list) do
local itemstack = inv:get_stack(list, i)
if not itemstack:is_empty() then
if inv:room_for_item("main", itemstack) then
inv:add_item("main", itemstack)
else
item_drop.drop_item(pos, itemstack)
end
itemstack:clear()
inv:set_stack(list, i, itemstack)
items_moved = true
end
end
end
if items_moved then
player:set_inventory_formspec(crafting.get_formspec(player:get_player_name()))
end
end
local function on_player_receive_fields(player, form_name, fields)
local inv = player:get_inventory()
if fields.quit then
clear_craft_slots(player)
end
if fields.crafting_tracker == nil then
return
end
local name = player:get_player_name()
local do_craft_1, do_craft_10 = false, false
if fields.craft_list then
-- Double-click on list entry crafts single time
local selection = minetest.explode_table_event(fields.craft_list)
if selection.type == "DCL" then
do_craft_1 = true
end
else
do_craft_1 = fields.do_craft_1 ~= nil
end
do_craft_10 = fields.do_craft_10 ~= nil
if do_craft_1 or do_craft_10 then
local craftitems
if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
craftitems = crafting.get_crafts()
else
craftitems = crafting.get_crafts(inv)
end
local old_item = nil
if crafting.userdata[name] then
old_item = craftitems[crafting.userdata[name].row]
end
local wanted_itemstack = ItemStack(craftitems[crafting.userdata[name].row])
local output_itemstack = inv:get_stack("craft_out", 1)
if output_itemstack:get_name() ~= wanted_itemstack:get_name()
and output_itemstack:get_count() ~= 0 then
return -- Different item type in output already
end
local count = 1
if do_craft_1 then
count = 1
elseif do_craft_10 then
count = 10
else
return
end
local crafted = crafting.craft(player, wanted_itemstack, count,
output_itemstack, inv:get_list("craft_in"))
if crafted then
inv:set_stack("craft_out", 1, "")
if inv:room_for_item("craft_out", crafted.output) then
inv:set_stack("craft_out", 1, crafted.output)
local new_list = {}
for i=1, #crafted.items do
new_list[i] = ItemStack(crafted.items[i])
end
inv:set_list("craft_in", new_list)
crafting.update_crafting_formspec(player, old_item)
end
end
elseif fields.craft_list then
local selection = minetest.explode_table_event(fields.craft_list)
if selection.type == "CHG" then
crafting.userdata[name].row = selection.row
minetest.show_formspec(name, "crafting:crafting",
crafting.get_formspec(name))
end
elseif fields.toggle_filter then
local craftitems
if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
craftitems = crafting.get_crafts()
else
craftitems = crafting.get_crafts(inv)
end
local old_item = craftitems[crafting.userdata[name].row]
if crafting.userdata[name].mode == MODE_GUIDE then
crafting.userdata[name].mode = MODE_CRAFTABLE
else
crafting.userdata[name].mode = MODE_GUIDE
end
minetest.show_formspec(name, "crafting:crafting",
crafting.get_formspec(name, old_item))
end
player:set_inventory_formspec(crafting.get_formspec(name))
end
function crafting.update_crafting_formspec(player, old_item)
local name = player:get_player_name()
local newform = crafting.get_formspec(name, old_item)
minetest.show_formspec(name, "crafting:crafting", newform)
player:set_inventory_formspec(newform)
end
minetest.register_allow_player_inventory_action(function(player, action, inventory, inventory_info)
if action == "put" then
if inventory_info.listname == "craft_out" then
return 0
end
elseif action == "move" then
if inventory_info.to_list == "craft_out" then
return 0
end
end
end)
minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info)
if action == "move" then
if inventory_info.from_list == "craft_in" or inventory_info.to_list == "craft_in" then
crafting.update_crafting_formspec(player)
end
elseif action == "put" or action == "take" then
if inventory_info.listname == "craft_in" then
crafting.update_crafting_formspec(player)
end
end
end)
local function on_joinplayer(player)
local name = player:get_player_name()
local inv = player:get_inventory()
if crafting.userdata[name] == nil then
crafting.userdata[name] = {row = 1, mode = MODE_CRAFTABLE}
end
if inv:get_size("craft_in") ~= 4 then
inv:set_size("craft_in", 4)
end
if inv:get_size("craft_out") ~= 1 then
inv:set_size("craft_out", 1)
end
clear_craft_slots(player)
end
local function on_leaveplayer(player)
local name = player:get_player_name()
crafting.userdata[name] = nil
end
if minetest.get_modpath("drop_items_on_die") ~= nil then
drop_items_on_die.register_listname("craft_in")
drop_items_on_die.register_listname("craft_out")
end
minetest.register_on_joinplayer(on_joinplayer)
minetest.register_on_leaveplayer(on_leaveplayer)
minetest.register_on_player_receive_fields(on_player_receive_fields)
default.log("api", "loaded")