567 lines
20 KiB
Lua
567 lines
20 KiB
Lua
local modpath = minetest.get_modpath(minetest.get_current_modname())
|
|
|
|
dofile(modpath .. "/saveload/keycolors.lua")
|
|
|
|
local OptionParser = dofile(modpath .. "/saveload/optparse.lua")
|
|
local orderedPairs = dofile(modpath .. "/saveload/orderedpairs.lua")
|
|
local parse_graphml_recipes = dofile(modpath .. "/saveload/readrecipegraph.lua")
|
|
local write_graphml_recipes = dofile(modpath .. "/saveload/writerecipegraph.lua")
|
|
local write_gv_recipes = dofile(modpath .. "/saveload/writerecipegv.lua")
|
|
|
|
-- Given a list of mods, returns a filter with indices for all registered items
|
|
-- that belong to one of those mods and all group names that belong to at least
|
|
-- one item in one of those mods.
|
|
-- TODO: this doesn't handle multigroup recipe items, such as "flower,yellow"
|
|
-- Might be better to reuse methods from postprocessing.lua, they're more expensive but
|
|
-- they handle this.
|
|
local create_mod_filter = function(mod_list)
|
|
|
|
local filter_obj = {}
|
|
if next(mod_list) == nil then
|
|
filter_obj.filter = function() return true end -- if there's nothing in the mod list, make a filter that always returns true
|
|
return filter_obj
|
|
end
|
|
|
|
local mods = {}
|
|
for _, mod in pairs(mod_list) do
|
|
mods[mod] = true
|
|
end
|
|
local all_members = {}
|
|
for itemname, itemdef in pairs(minetest.registered_items) do
|
|
local colon_index = string.find(itemname, ":")
|
|
if colon_index then
|
|
local mod = string.sub(itemname, 1, colon_index-1)
|
|
if mods[mod] then
|
|
all_members[itemname] = true
|
|
if itemdef.groups then
|
|
for group, _ in pairs(itemdef.groups) do
|
|
all_members[group] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
filter_obj.filter = function(recipe)
|
|
if recipe.input then
|
|
for item, _ in pairs(recipe.input) do
|
|
if all_members[item] then return true end
|
|
end
|
|
end
|
|
if recipe.output then
|
|
if all_members[recipe.output:get_name()] then return true end
|
|
end
|
|
if recipe.returns then
|
|
for item, _ in pairs(recipe.returns) do
|
|
if all_members[item] then return true end
|
|
end
|
|
end
|
|
end
|
|
return filter_obj
|
|
end
|
|
|
|
-- Writing recipe dump to a .lua file
|
|
---------------------------------------------------------------------------------
|
|
|
|
-- Writes a single recipe to a table in the output file
|
|
local write_recipe = function(file, recipe)
|
|
file:write("\t{\n")
|
|
for key, val in orderedPairs(recipe) do
|
|
if type(val) == "function" then
|
|
minetest.log("error", "[simplecrafting_lib] recipe write: " .. key .. "'s value is a function")
|
|
else
|
|
file:write("\t\t"..key.." = ")
|
|
if key == "output" then
|
|
file:write("\t\"" .. ItemStack(val):to_string() .."\",\n")
|
|
elseif type(val) == "table" then
|
|
file:write("\t{")
|
|
for kk, vv in orderedPairs(val) do
|
|
if type(vv) == "string" then
|
|
file:write("[\"" .. kk .. "\"] = \"" .. tostring(vv) .. "\", ")
|
|
else
|
|
file:write("[\"" .. kk .. "\"] = " .. tostring(vv) .. ", ")
|
|
end
|
|
end
|
|
file:write("},\n")
|
|
elseif type(val) == "string" then
|
|
file:write("\t\"" .. tostring(val) .. "\",\n")
|
|
else
|
|
file:write("\t" .. tostring(val) .. ",\n")
|
|
end
|
|
end
|
|
end
|
|
file:write("\t},\n")
|
|
end
|
|
|
|
local write_craft_list = function(file, craft_type, recipe_list_by_out, recipe_filter)
|
|
file:write("-- Craft Type " .. craft_type .. "--------------------------------------------------------\n[\"" .. craft_type .. "\"] = {\n")
|
|
for out, recipe_list in orderedPairs(recipe_list_by_out) do
|
|
local output_comment_written = false
|
|
for _, recipe in ipairs(recipe_list) do
|
|
if recipe_filter.filter(recipe) then
|
|
if not output_comment_written then
|
|
file:write("-- Output: " .. out .. "\n")
|
|
output_comment_written = true
|
|
end
|
|
write_recipe(file, recipe)
|
|
end
|
|
end
|
|
end
|
|
file:write("},\n")
|
|
end
|
|
|
|
-- Dumps recipes from the existing crafting system into a file that can be used to recreate them.
|
|
local save_recipes = function(param, craft_types, recipe_filter)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/" .. param .. ".lua"
|
|
local file, err = io.open(filename, "w")
|
|
if err ~= nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not save recipes to \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
|
|
file:write("return {\n")
|
|
|
|
if table.getn(craft_types) == 0 then
|
|
for craft_type, recipe_list in orderedPairs(simplecrafting_lib.type) do
|
|
write_craft_list(file, craft_type, recipe_list.recipes_by_out, recipe_filter)
|
|
end
|
|
else
|
|
for _, craft_type in ipairs(craft_types) do
|
|
if simplecrafting_lib.type[craft_type] then
|
|
write_craft_list(file, craft_type, simplecrafting_lib.type[craft_type].recipes_by_out, recipe_filter)
|
|
-- else
|
|
-- TODO: error message
|
|
end
|
|
end
|
|
end
|
|
|
|
file:write("}\n")
|
|
|
|
file:flush()
|
|
file:close()
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
|
|
local save_recipes_graph = function(name, craft_types, recipe_filter, show_unused, save_function, extension)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/" .. name .. "." .. extension
|
|
local file, err = io.open(filename, "w")
|
|
if err ~= nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not save recipes to \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
|
|
if not craft_types or table.getn(craft_types) == 0 then
|
|
save_function(file, simplecrafting_lib.type, recipe_filter, show_unused)
|
|
else
|
|
local recipes = {}
|
|
for _, craft_type in ipairs(craft_types) do
|
|
recipes[craft_type] = simplecrafting_lib.type[craft_type]
|
|
end
|
|
save_function(file, recipes, recipe_filter, show_unused)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
|
|
local save_recipes_graphml = function(name, craft_types, recipe_filter, show_unused)
|
|
return save_recipes_graph(name, craft_types, recipe_filter, show_unused, write_graphml_recipes, "graphml")
|
|
end
|
|
|
|
local read_recipes_graphml = function(name)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/" .. name .. ".graphml"
|
|
|
|
local file, err = io.open(filename, "r")
|
|
if err ~= nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
local myxml = file:read('*all')
|
|
local parse_error
|
|
myxml, parse_error = parse_graphml_recipes(myxml)
|
|
if parse_error then
|
|
minetest.log("error", "Failed to parse graphml " .. filename .. " with error: " .. parse_error)
|
|
return false
|
|
end
|
|
|
|
return myxml
|
|
end
|
|
|
|
-------------------------------------------------------------
|
|
|
|
local save_recipes_gv = function(name, craft_types, recipe_filter)
|
|
return save_recipes_graph(name, craft_types, recipe_filter, false, write_gv_recipes, "gv")
|
|
end
|
|
|
|
-------------------------------------------------------------
|
|
|
|
-- registers all recipes in the provided filename, which is usually a file generated by save_recipes and then perhaps modified by the developer.
|
|
local load_recipes = function(param, craft_set, recipe_filter)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/" .. param .. ".lua"
|
|
local new_recipes = loadfile(filename)
|
|
if new_recipes == nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
new_recipes = new_recipes()
|
|
|
|
for crafting_type, recipes in pairs(new_recipes) do
|
|
if craft_set == nil or craft_set[crafting_type] then
|
|
for _, recipe in pairs(recipes) do
|
|
if recipe_filter.filter(recipe) then
|
|
simplecrafting_lib.register(crafting_type, recipe)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- What the function name says it does
|
|
local get_recipes_that_are_in_first_recipe_list_but_not_in_second_recipe_list = function(first_recipe_list, second_recipe_list)
|
|
if first_recipe_list == nil then
|
|
return nil
|
|
elseif second_recipe_list == nil then
|
|
return first_recipe_list
|
|
end
|
|
|
|
local returns
|
|
|
|
for _, first_recipe in pairs(first_recipe_list) do
|
|
local found = false
|
|
for _, second_recipe in pairs(second_recipe_list) do
|
|
if simplecrafting_lib.recipe_equals(first_recipe, second_recipe) then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
if found ~= true then
|
|
returns = returns or {}
|
|
table.insert(returns, first_recipe)
|
|
end
|
|
end
|
|
|
|
return returns
|
|
end
|
|
|
|
-- Used in diff_recipes for writing lists of recipes
|
|
local write_recipe_lists = function(file, recipe_lists)
|
|
for craft_type, recipe_list in orderedPairs(recipe_lists) do
|
|
file:write("-- Craft Type " .. craft_type .. "--------------------------------------------------------\n[\"" .. craft_type .. "\"] = {\n")
|
|
for _, recipe in ipairs(recipe_list) do
|
|
write_recipe(file, recipe)
|
|
end
|
|
file:write("},\n")
|
|
end
|
|
end
|
|
|
|
-- compares the recipes in the infile (of the form written by save_recipes) to the recipes in the existing crafting system, and outputs differences to outfile
|
|
local diff_recipes = function(infile, outfile)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/" .. infile .. ".lua"
|
|
local new_recipes = loadfile(filename)
|
|
if new_recipes == nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
new_recipes = new_recipes()
|
|
|
|
local new_only_recipes = {}
|
|
local existing_only_recipes = {}
|
|
|
|
for craft_type, recipe_lists in pairs(simplecrafting_lib.type) do
|
|
if new_recipes[craft_type] ~= nil then
|
|
new_only_recipes[craft_type] = get_recipes_that_are_in_first_recipe_list_but_not_in_second_recipe_list(new_recipes[craft_type], recipe_lists.recipes)
|
|
else
|
|
existing_only_recipes[craft_type] = recipe_lists.recipes
|
|
end
|
|
end
|
|
for craft_type, recipe_lists in pairs(new_recipes) do
|
|
local existing_recipes = simplecrafting_lib.type[craft_type]
|
|
if existing_recipes ~= nil then
|
|
existing_only_recipes[craft_type] = get_recipes_that_are_in_first_recipe_list_but_not_in_second_recipe_list(existing_recipes.recipes, recipe_lists)
|
|
else
|
|
new_only_recipes[craft_type] = recipe_lists
|
|
end
|
|
end
|
|
|
|
filename = path .. "/" .. outfile .. ".txt"
|
|
local file, err = io.open(filename, "w")
|
|
if err ~= nil then
|
|
minetest.log("error", "[simplecrafting_lib] Could not save recipe diffs to \"" .. filename .. "\"")
|
|
return false
|
|
end
|
|
|
|
file:write("-- Recipes found only in the external file:\n--------------------------------------------------------\n")
|
|
write_recipe_lists(file, new_only_recipes)
|
|
file:write("\n")
|
|
|
|
file:write("-- Recipes found only in the existing crafting database:\n--------------------------------------------------------\n")
|
|
write_recipe_lists(file, existing_only_recipes)
|
|
file:write("\n")
|
|
|
|
file:flush()
|
|
file:close()
|
|
|
|
return true
|
|
end
|
|
|
|
---------------------------------------------------------------
|
|
|
|
function split(inputstr, seperator)
|
|
if inputstr == nil then return {} end
|
|
if seperator == nil then
|
|
seperator = "%s"
|
|
end
|
|
local out={}
|
|
local i=1
|
|
for substring in string.gmatch(inputstr, "([^"..seperator.."]+)") do
|
|
out[i] = substring
|
|
i = i + 1
|
|
end
|
|
return out
|
|
end
|
|
|
|
local saveoptparse = OptionParser{usage="[options] file"}
|
|
saveoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
|
|
saveoptparse.add_option{"-l", "--lua", action="store_true", dest="lua", help="saves recipes as \"(world folder)/<file>.lua\""}
|
|
saveoptparse.add_option{"-d", "--dot", action="store_true", dest="dot", help="saves recipes as \"(world folder)/<file>.gv\""}
|
|
saveoptparse.add_option{"-g", "--graphml", action="store_true", dest="graphml", help="saves recipes as \"(world folder)/<file>.graphml\""}
|
|
saveoptparse.add_option{"-t", "--type", action="store", dest="types", help="craft_type to save. Leave unset to save all. Use a comma-delimited list (eg, \"table,furnace\") to save multiple specific craft types."}
|
|
saveoptparse.add_option{"-m", "--mod", action="store", dest="mods", help="only recipes with these mods in them will be saved. Leave unset to save all. Use a comma-delimited list with no spaces (eg, \"default,stairs\") to save multiple specific mod types."}
|
|
saveoptparse.add_option{"-u", "--unused", action="store_true", dest="unused", help="Include all registered unused items in graphml output (no effect with lua or dot output)."}
|
|
|
|
minetest.register_chatcommand("recipesave", {
|
|
params = saveoptparse.print_help(),
|
|
description = "Saves recipes to external files",
|
|
func = function(name, param)
|
|
if not minetest.check_player_privs(name, {server = true}) then
|
|
minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
|
|
return
|
|
end
|
|
|
|
local success, options, args = saveoptparse.parse_args(param)
|
|
if not success then
|
|
minetest.chat_send_player(name, options)
|
|
return
|
|
end
|
|
|
|
if options.help then
|
|
minetest.chat_send_player(name, saveoptparse.print_help())
|
|
return
|
|
end
|
|
|
|
if table.getn(args) ~= 1 then
|
|
minetest.chat_send_player(name, "A filename argument is needed.")
|
|
return
|
|
end
|
|
|
|
if not (options.lua or options.graphml or options.dot) then
|
|
minetest.chat_send_player(name, "Neither lua nor graphml nor DOT output was selected, defaulting to lua.")
|
|
options.lua = true
|
|
end
|
|
|
|
if options.unused and not options.graphml then
|
|
minetest.chat_send_player(name, "Unused items are only included in graphml output, which was not selected.")
|
|
end
|
|
|
|
local craft_types = split(options.types, ",")
|
|
local recipe_filter = create_mod_filter(split(options.mods, ","))
|
|
|
|
if options.lua then
|
|
if save_recipes(args[1], craft_types, recipe_filter) then
|
|
minetest.chat_send_player(name, "Lua recipes saved to "..args[1]..".lua", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to save lua recipes", false)
|
|
end
|
|
end
|
|
|
|
if options.graphml then
|
|
if save_recipes_graphml(args[1], craft_types, recipe_filter, options.unused) then
|
|
minetest.chat_send_player(name, "Graphml recipes saved to " .. args[1]..".graphml", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to save graphml recipes", false)
|
|
end
|
|
end
|
|
|
|
if options.dot then
|
|
if save_recipes_gv(args[1], craft_types, recipe_filter) then
|
|
minetest.chat_send_player(name, "DOT recipes saved to " .. args[1]..".gv", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to save DOT recipes", false)
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
-- TODO: combine the load commands too. Include an option to clear craft types being loaded.
|
|
|
|
local loadoptparse = OptionParser{usage="[options] file"}
|
|
loadoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
|
|
loadoptparse.add_option{"-l", "--lua", action="store_true", dest="lua", help="loads recipes from \"(world folder)/<file>.lua\""}
|
|
loadoptparse.add_option{"-g", "--graphml", action="store_true", dest="graphml", help="loads recipes from \"(world folder)/<file>.graphml\""}
|
|
loadoptparse.add_option{"-t", "--type", action="store", dest="types", help="craft_type to load. Leave unset to load all. Use a comma-delimited list (eg, \"table,furnace\") to load multiple specific craft types."}
|
|
loadoptparse.add_option{"-m", "--mod", action="store", dest="mods", help="only recipes with these mods in them will be loaded. Leave unset to load all. Use a comma-delimited list with no spaces (eg, \"default,stairs\") to load multiple specific mod types."}
|
|
--loadoptparse.add_option{"-c", "--clear", action="store_true", dest="clear", help="Clears existing recipes of the craft_types being loaded before loading."}
|
|
|
|
minetest.register_chatcommand("recipeload", {
|
|
params = loadoptparse.print_help(),
|
|
description = "Loads recipes from external files",
|
|
func = function(name, param)
|
|
if not minetest.check_player_privs(name, {server = true}) then
|
|
minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
|
|
return
|
|
end
|
|
|
|
local success, options, args = loadoptparse.parse_args(param)
|
|
if not success then
|
|
minetest.chat_send_player(name, options)
|
|
return
|
|
end
|
|
|
|
if options.help then
|
|
minetest.chat_send_player(name, loadoptparse.print_help())
|
|
return
|
|
end
|
|
|
|
if table.getn(args) ~= 1 then
|
|
minetest.chat_send_player(name, "A single filename argument is needed.")
|
|
return
|
|
end
|
|
|
|
if not (options.lua or options.graphml) or (options.lua and options.graphml) then
|
|
minetest.chat_send_player(name, "One of lua or graphml output formats should be selected. Defaulting to lua.")
|
|
options.lua = true
|
|
options.graphml = false
|
|
end
|
|
|
|
local craft_types = split(options.types, ",")
|
|
local craft_set
|
|
if table.getn(craft_types) > 0 then
|
|
craft_set = {}
|
|
for _, craft_type in pairs(craft_types) do
|
|
craft_set[craft_type] = true
|
|
end
|
|
end
|
|
local recipe_filter = create_mod_filter(split(options.mods, ","))
|
|
|
|
if options.graphml then
|
|
local read_recipes = read_recipes_graphml(args[1])
|
|
if read_recipes then
|
|
for _, recipe in pairs(read_recipes) do
|
|
local craft_type = recipe.craft_type
|
|
if (craft_set == nil or craft_set[craft_type]) and recipe_filter.filter(recipe) then
|
|
recipe.craft_type = nil
|
|
simplecrafting_lib.register(craft_type, recipe)
|
|
end
|
|
end
|
|
minetest.chat_send_player(name, "Recipes read from graphml", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to read recipes from graphml", false)
|
|
end
|
|
else
|
|
if load_recipes(args[1], craft_set, recipe_filter) then
|
|
minetest.chat_send_player(name, "Recipes loaded from lua", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to load recipes from lua", false)
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
local clearoptparse = OptionParser{usage="[options]"}
|
|
clearoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
|
|
clearoptparse.add_option{"-t", "--type", action="store", dest="types", help = "Clear only these recipe types. Leave unset to clear all. Use a comma-delimited list with no spaces (eg, \"table,furnace\") to load multiple specific craft types."}
|
|
minetest.register_chatcommand("recipeclear", {
|
|
params = "",
|
|
description = "Clear all recipes from simplecrafting_lib",
|
|
func = function(name, param)
|
|
if not minetest.check_player_privs(name, {server = true}) then
|
|
minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
|
|
return
|
|
end
|
|
|
|
local success, options, args = clearoptparse.parse_args(param)
|
|
if not success then
|
|
minetest.chat_send_player(name, options)
|
|
return
|
|
end
|
|
|
|
if options.help then
|
|
minetest.chat_send_player(name, clearoptparse.print_help())
|
|
return
|
|
end
|
|
|
|
local craft_types = split(options.types, ",")
|
|
if table.getn(craft_types) > 0 then
|
|
for _, craft_type in pairs(craft_types) do
|
|
if simplecrafting_lib.type[craft_type] == nil then
|
|
minetest.chat_send_player(name, "Craft type " .. craft_type .. " was already clear.", false)
|
|
else
|
|
simplecrafting_lib.type[craft_type] = nil
|
|
end
|
|
end
|
|
else
|
|
simplecrafting_lib.type = {}
|
|
end
|
|
minetest.chat_send_player(name, "Recipes cleared", false)
|
|
end,
|
|
})
|
|
|
|
|
|
minetest.register_chatcommand("recipecompare", {
|
|
params="<infile> <outfile>",
|
|
description="Compares existing recipe data to the data in \"(world folder)/<infile>.lua\", outputting the differences to \"(world folder)/<outfile>.txt\"",
|
|
func = function(name, param)
|
|
if not minetest.check_player_privs(name, {server = true}) then
|
|
minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
|
|
return
|
|
end
|
|
|
|
local params = split(param)
|
|
if #params ~= 2 then
|
|
minetest.chat_send_player(name, "Invalid usage, two filename parameters separted by a space are needed", false)
|
|
return
|
|
end
|
|
|
|
if diff_recipes(params[1], params[2]) then
|
|
minetest.chat_send_player(name, "Recipes diffed", false)
|
|
else
|
|
minetest.chat_send_player(name, "Failed to diff recipes", false)
|
|
end
|
|
end,
|
|
})
|
|
|
|
minetest.register_chatcommand("recipestats", {
|
|
params="",
|
|
description="Outputs stats about registered recipes",
|
|
func = function(name, param)
|
|
if not minetest.check_player_privs(name, {server = true}) then
|
|
minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
|
|
return
|
|
end
|
|
|
|
for craft_type, recipe_lists in pairs(simplecrafting_lib.type) do
|
|
|
|
minetest.chat_send_player(name, "recipe type: "..craft_type)
|
|
minetest.chat_send_player(name, tostring(table.getn(recipe_lists.recipes)) .. " recipes")
|
|
local max_inputs = 0
|
|
for _, recipe in pairs(recipe_lists.recipes) do
|
|
local itemcount = 0
|
|
for item, count in pairs(recipe.input) do
|
|
itemcount = itemcount + 1
|
|
end
|
|
max_inputs = math.max(max_inputs, itemcount)
|
|
end
|
|
minetest.chat_send_player(name, "Largest number of input types: " .. tostring(max_inputs))
|
|
end
|
|
end,
|
|
})
|
|
|
|
-- TODO: need a recipestats command to get general information about recipes |