saveload.lua 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. local modpath = minetest.get_modpath(minetest.get_current_modname())
  2. dofile(modpath .. "/saveload/keycolors.lua")
  3. local OptionParser = dofile(modpath .. "/saveload/optparse.lua")
  4. local orderedPairs = dofile(modpath .. "/saveload/orderedpairs.lua")
  5. local parse_graphml_recipes = dofile(modpath .. "/saveload/readrecipegraph.lua")
  6. local write_graphml_recipes = dofile(modpath .. "/saveload/writerecipegraph.lua")
  7. local write_gv_recipes = dofile(modpath .. "/saveload/writerecipegv.lua")
  8. -- Given a list of mods, returns a filter with indices for all registered items
  9. -- that belong to one of those mods and all group names that belong to at least
  10. -- one item in one of those mods.
  11. -- TODO: this doesn't handle multigroup recipe items, such as "flower,yellow"
  12. -- Might be better to reuse methods from postprocessing.lua, they're more expensive but
  13. -- they handle this.
  14. local create_mod_filter = function(mod_list)
  15. local filter_obj = {}
  16. if next(mod_list) == nil then
  17. filter_obj.filter = function() return true end -- if there's nothing in the mod list, make a filter that always returns true
  18. return filter_obj
  19. end
  20. local mods = {}
  21. for _, mod in pairs(mod_list) do
  22. mods[mod] = true
  23. end
  24. local all_members = {}
  25. for itemname, itemdef in pairs(minetest.registered_items) do
  26. local colon_index = string.find(itemname, ":")
  27. if colon_index then
  28. local mod = string.sub(itemname, 1, colon_index-1)
  29. if mods[mod] then
  30. all_members[itemname] = true
  31. if itemdef.groups then
  32. for group, _ in pairs(itemdef.groups) do
  33. all_members[group] = true
  34. end
  35. end
  36. end
  37. end
  38. end
  39. filter_obj.filter = function(recipe)
  40. if recipe.input then
  41. for item, _ in pairs(recipe.input) do
  42. if all_members[item] then return true end
  43. end
  44. end
  45. if recipe.output then
  46. if all_members[recipe.output:get_name()] then return true end
  47. end
  48. if recipe.returns then
  49. for item, _ in pairs(recipe.returns) do
  50. if all_members[item] then return true end
  51. end
  52. end
  53. end
  54. return filter_obj
  55. end
  56. -- Writing recipe dump to a .lua file
  57. ---------------------------------------------------------------------------------
  58. -- Writes a single recipe to a table in the output file
  59. local write_recipe = function(file, recipe)
  60. file:write("\t{\n")
  61. for key, val in orderedPairs(recipe) do
  62. if type(val) == "function" then
  63. minetest.log("error", "[simplecrafting_lib] recipe write: " .. key .. "'s value is a function")
  64. else
  65. file:write("\t\t"..key.." = ")
  66. if key == "output" then
  67. file:write("\t\"" .. ItemStack(val):to_string() .."\",\n")
  68. elseif type(val) == "table" then
  69. file:write("\t{")
  70. for kk, vv in orderedPairs(val) do
  71. if type(vv) == "string" then
  72. file:write("[\"" .. kk .. "\"] = \"" .. tostring(vv) .. "\", ")
  73. else
  74. file:write("[\"" .. kk .. "\"] = " .. tostring(vv) .. ", ")
  75. end
  76. end
  77. file:write("},\n")
  78. elseif type(val) == "string" then
  79. file:write("\t\"" .. tostring(val) .. "\",\n")
  80. else
  81. file:write("\t" .. tostring(val) .. ",\n")
  82. end
  83. end
  84. end
  85. file:write("\t},\n")
  86. end
  87. local write_craft_list = function(file, craft_type, recipe_list_by_out, recipe_filter)
  88. file:write("-- Craft Type " .. craft_type .. "--------------------------------------------------------\n[\"" .. craft_type .. "\"] = {\n")
  89. for out, recipe_list in orderedPairs(recipe_list_by_out) do
  90. local output_comment_written = false
  91. for _, recipe in ipairs(recipe_list) do
  92. if recipe_filter.filter(recipe) then
  93. if not output_comment_written then
  94. file:write("-- Output: " .. out .. "\n")
  95. output_comment_written = true
  96. end
  97. write_recipe(file, recipe)
  98. end
  99. end
  100. end
  101. file:write("},\n")
  102. end
  103. -- Dumps recipes from the existing crafting system into a file that can be used to recreate them.
  104. local save_recipes = function(param, craft_types, recipe_filter)
  105. local path = minetest.get_worldpath()
  106. local filename = path .. "/" .. param .. ".lua"
  107. local file, err = io.open(filename, "w")
  108. if err ~= nil then
  109. minetest.log("error", "[simplecrafting_lib] Could not save recipes to \"" .. filename .. "\"")
  110. return false
  111. end
  112. file:write("return {\n")
  113. if table.getn(craft_types) == 0 then
  114. for craft_type, recipe_list in orderedPairs(simplecrafting_lib.type) do
  115. write_craft_list(file, craft_type, recipe_list.recipes_by_out, recipe_filter)
  116. end
  117. else
  118. for _, craft_type in ipairs(craft_types) do
  119. if simplecrafting_lib.type[craft_type] then
  120. write_craft_list(file, craft_type, simplecrafting_lib.type[craft_type].recipes_by_out, recipe_filter)
  121. -- else
  122. -- TODO: error message
  123. end
  124. end
  125. end
  126. file:write("}\n")
  127. file:flush()
  128. file:close()
  129. return true
  130. end
  131. -------------------------------------------------------------------------------------------
  132. local save_recipes_graph = function(name, craft_types, recipe_filter, show_unused, save_function, extension)
  133. local path = minetest.get_worldpath()
  134. local filename = path .. "/" .. name .. "." .. extension
  135. local file, err = io.open(filename, "w")
  136. if err ~= nil then
  137. minetest.log("error", "[simplecrafting_lib] Could not save recipes to \"" .. filename .. "\"")
  138. return false
  139. end
  140. if not craft_types or table.getn(craft_types) == 0 then
  141. save_function(file, simplecrafting_lib.type, recipe_filter, show_unused)
  142. else
  143. local recipes = {}
  144. for _, craft_type in ipairs(craft_types) do
  145. recipes[craft_type] = simplecrafting_lib.type[craft_type]
  146. end
  147. save_function(file, recipes, recipe_filter, show_unused)
  148. end
  149. return true
  150. end
  151. -------------------------------------------------------------------------------------------
  152. local save_recipes_graphml = function(name, craft_types, recipe_filter, show_unused)
  153. return save_recipes_graph(name, craft_types, recipe_filter, show_unused, write_graphml_recipes, "graphml")
  154. end
  155. local read_recipes_graphml = function(name)
  156. local path = minetest.get_worldpath()
  157. local filename = path .. "/" .. name .. ".graphml"
  158. local file, err = io.open(filename, "r")
  159. if err ~= nil then
  160. minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
  161. return false
  162. end
  163. local myxml = file:read('*all')
  164. local parse_error
  165. myxml, parse_error = parse_graphml_recipes(myxml)
  166. if parse_error then
  167. minetest.log("error", "Failed to parse graphml " .. filename .. " with error: " .. parse_error)
  168. return false
  169. end
  170. return myxml
  171. end
  172. -------------------------------------------------------------
  173. local save_recipes_gv = function(name, craft_types, recipe_filter)
  174. return save_recipes_graph(name, craft_types, recipe_filter, false, write_gv_recipes, "gv")
  175. end
  176. -------------------------------------------------------------
  177. -- registers all recipes in the provided filename, which is usually a file generated by save_recipes and then perhaps modified by the developer.
  178. local load_recipes = function(param, craft_set, recipe_filter)
  179. local path = minetest.get_worldpath()
  180. local filename = path .. "/" .. param .. ".lua"
  181. local new_recipes = loadfile(filename)
  182. if new_recipes == nil then
  183. minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
  184. return false
  185. end
  186. new_recipes = new_recipes()
  187. for crafting_type, recipes in pairs(new_recipes) do
  188. if craft_set == nil or craft_set[crafting_type] then
  189. for _, recipe in pairs(recipes) do
  190. if recipe_filter.filter(recipe) then
  191. simplecrafting_lib.register(crafting_type, recipe)
  192. end
  193. end
  194. end
  195. end
  196. return true
  197. end
  198. -- What the function name says it does
  199. local get_recipes_that_are_in_first_recipe_list_but_not_in_second_recipe_list = function(first_recipe_list, second_recipe_list)
  200. if first_recipe_list == nil then
  201. return nil
  202. elseif second_recipe_list == nil then
  203. return first_recipe_list
  204. end
  205. local returns
  206. for _, first_recipe in pairs(first_recipe_list) do
  207. local found = false
  208. for _, second_recipe in pairs(second_recipe_list) do
  209. if simplecrafting_lib.recipe_equals(first_recipe, second_recipe) then
  210. found = true
  211. break
  212. end
  213. end
  214. if found ~= true then
  215. returns = returns or {}
  216. table.insert(returns, first_recipe)
  217. end
  218. end
  219. return returns
  220. end
  221. -- Used in diff_recipes for writing lists of recipes
  222. local write_recipe_lists = function(file, recipe_lists)
  223. for craft_type, recipe_list in orderedPairs(recipe_lists) do
  224. file:write("-- Craft Type " .. craft_type .. "--------------------------------------------------------\n[\"" .. craft_type .. "\"] = {\n")
  225. for _, recipe in ipairs(recipe_list) do
  226. write_recipe(file, recipe)
  227. end
  228. file:write("},\n")
  229. end
  230. end
  231. -- 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
  232. local diff_recipes = function(infile, outfile)
  233. local path = minetest.get_worldpath()
  234. local filename = path .. "/" .. infile .. ".lua"
  235. local new_recipes = loadfile(filename)
  236. if new_recipes == nil then
  237. minetest.log("error", "[simplecrafting_lib] Could not read recipes from \"" .. filename .. "\"")
  238. return false
  239. end
  240. new_recipes = new_recipes()
  241. local new_only_recipes = {}
  242. local existing_only_recipes = {}
  243. for craft_type, recipe_lists in pairs(simplecrafting_lib.type) do
  244. if new_recipes[craft_type] ~= nil then
  245. 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)
  246. else
  247. existing_only_recipes[craft_type] = recipe_lists.recipes
  248. end
  249. end
  250. for craft_type, recipe_lists in pairs(new_recipes) do
  251. local existing_recipes = simplecrafting_lib.type[craft_type]
  252. if existing_recipes ~= nil then
  253. existing_only_recipes[craft_type] = get_recipes_that_are_in_first_recipe_list_but_not_in_second_recipe_list(existing_recipes.recipes, recipe_lists)
  254. else
  255. new_only_recipes[craft_type] = recipe_lists
  256. end
  257. end
  258. filename = path .. "/" .. outfile .. ".txt"
  259. local file, err = io.open(filename, "w")
  260. if err ~= nil then
  261. minetest.log("error", "[simplecrafting_lib] Could not save recipe diffs to \"" .. filename .. "\"")
  262. return false
  263. end
  264. file:write("-- Recipes found only in the external file:\n--------------------------------------------------------\n")
  265. write_recipe_lists(file, new_only_recipes)
  266. file:write("\n")
  267. file:write("-- Recipes found only in the existing crafting database:\n--------------------------------------------------------\n")
  268. write_recipe_lists(file, existing_only_recipes)
  269. file:write("\n")
  270. file:flush()
  271. file:close()
  272. return true
  273. end
  274. ---------------------------------------------------------------
  275. function split(inputstr, seperator)
  276. if inputstr == nil then return {} end
  277. if seperator == nil then
  278. seperator = "%s"
  279. end
  280. local out={}
  281. local i=1
  282. for substring in string.gmatch(inputstr, "([^"..seperator.."]+)") do
  283. out[i] = substring
  284. i = i + 1
  285. end
  286. return out
  287. end
  288. local saveoptparse = OptionParser{usage="[options] file"}
  289. saveoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
  290. saveoptparse.add_option{"-l", "--lua", action="store_true", dest="lua", help="saves recipes as \"(world folder)/<file>.lua\""}
  291. saveoptparse.add_option{"-d", "--dot", action="store_true", dest="dot", help="saves recipes as \"(world folder)/<file>.gv\""}
  292. saveoptparse.add_option{"-g", "--graphml", action="store_true", dest="graphml", help="saves recipes as \"(world folder)/<file>.graphml\""}
  293. 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."}
  294. 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."}
  295. 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)."}
  296. minetest.register_chatcommand("recipesave", {
  297. params = saveoptparse.print_help(),
  298. description = "Saves recipes to external files",
  299. func = function(name, param)
  300. if not minetest.check_player_privs(name, {server = true}) then
  301. minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
  302. return
  303. end
  304. local success, options, args = saveoptparse.parse_args(param)
  305. if not success then
  306. minetest.chat_send_player(name, options)
  307. return
  308. end
  309. if options.help then
  310. minetest.chat_send_player(name, saveoptparse.print_help())
  311. return
  312. end
  313. if table.getn(args) ~= 1 then
  314. minetest.chat_send_player(name, "A filename argument is needed.")
  315. return
  316. end
  317. if not (options.lua or options.graphml or options.dot) then
  318. minetest.chat_send_player(name, "Neither lua nor graphml nor DOT output was selected, defaulting to lua.")
  319. options.lua = true
  320. end
  321. if options.unused and not options.graphml then
  322. minetest.chat_send_player(name, "Unused items are only included in graphml output, which was not selected.")
  323. end
  324. local craft_types = split(options.types, ",")
  325. local recipe_filter = create_mod_filter(split(options.mods, ","))
  326. if options.lua then
  327. if save_recipes(args[1], craft_types, recipe_filter) then
  328. minetest.chat_send_player(name, "Lua recipes saved to "..args[1]..".lua", false)
  329. else
  330. minetest.chat_send_player(name, "Failed to save lua recipes", false)
  331. end
  332. end
  333. if options.graphml then
  334. if save_recipes_graphml(args[1], craft_types, recipe_filter, options.unused) then
  335. minetest.chat_send_player(name, "Graphml recipes saved to " .. args[1]..".graphml", false)
  336. else
  337. minetest.chat_send_player(name, "Failed to save graphml recipes", false)
  338. end
  339. end
  340. if options.dot then
  341. if save_recipes_gv(args[1], craft_types, recipe_filter) then
  342. minetest.chat_send_player(name, "DOT recipes saved to " .. args[1]..".gv", false)
  343. else
  344. minetest.chat_send_player(name, "Failed to save DOT recipes", false)
  345. end
  346. end
  347. end,
  348. })
  349. -- TODO: combine the load commands too. Include an option to clear craft types being loaded.
  350. local loadoptparse = OptionParser{usage="[options] file"}
  351. loadoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
  352. loadoptparse.add_option{"-l", "--lua", action="store_true", dest="lua", help="loads recipes from \"(world folder)/<file>.lua\""}
  353. loadoptparse.add_option{"-g", "--graphml", action="store_true", dest="graphml", help="loads recipes from \"(world folder)/<file>.graphml\""}
  354. 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."}
  355. 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."}
  356. --loadoptparse.add_option{"-c", "--clear", action="store_true", dest="clear", help="Clears existing recipes of the craft_types being loaded before loading."}
  357. minetest.register_chatcommand("recipeload", {
  358. params = loadoptparse.print_help(),
  359. description = "Loads recipes from external files",
  360. func = function(name, param)
  361. if not minetest.check_player_privs(name, {server = true}) then
  362. minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
  363. return
  364. end
  365. local success, options, args = loadoptparse.parse_args(param)
  366. if not success then
  367. minetest.chat_send_player(name, options)
  368. return
  369. end
  370. if options.help then
  371. minetest.chat_send_player(name, loadoptparse.print_help())
  372. return
  373. end
  374. if table.getn(args) ~= 1 then
  375. minetest.chat_send_player(name, "A single filename argument is needed.")
  376. return
  377. end
  378. if not (options.lua or options.graphml) or (options.lua and options.graphml) then
  379. minetest.chat_send_player(name, "One of lua or graphml output formats should be selected. Defaulting to lua.")
  380. options.lua = true
  381. options.graphml = false
  382. end
  383. local craft_types = split(options.types, ",")
  384. local craft_set
  385. if table.getn(craft_types) > 0 then
  386. craft_set = {}
  387. for _, craft_type in pairs(craft_types) do
  388. craft_set[craft_type] = true
  389. end
  390. end
  391. local recipe_filter = create_mod_filter(split(options.mods, ","))
  392. if options.graphml then
  393. local read_recipes = read_recipes_graphml(args[1])
  394. if read_recipes then
  395. for _, recipe in pairs(read_recipes) do
  396. local craft_type = recipe.craft_type
  397. if (craft_set == nil or craft_set[craft_type]) and recipe_filter.filter(recipe) then
  398. recipe.craft_type = nil
  399. simplecrafting_lib.register(craft_type, recipe)
  400. end
  401. end
  402. minetest.chat_send_player(name, "Recipes read from graphml", false)
  403. else
  404. minetest.chat_send_player(name, "Failed to read recipes from graphml", false)
  405. end
  406. else
  407. if load_recipes(args[1], craft_set, recipe_filter) then
  408. minetest.chat_send_player(name, "Recipes loaded from lua", false)
  409. else
  410. minetest.chat_send_player(name, "Failed to load recipes from lua", false)
  411. end
  412. end
  413. end,
  414. })
  415. local clearoptparse = OptionParser{usage="[options]"}
  416. clearoptparse.add_option{"-h", "--help", action="store_true", dest="help", help = "displays help text"}
  417. 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."}
  418. minetest.register_chatcommand("recipeclear", {
  419. params = "",
  420. description = "Clear all recipes from simplecrafting_lib",
  421. func = function(name, param)
  422. if not minetest.check_player_privs(name, {server = true}) then
  423. minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
  424. return
  425. end
  426. local success, options, args = clearoptparse.parse_args(param)
  427. if not success then
  428. minetest.chat_send_player(name, options)
  429. return
  430. end
  431. if options.help then
  432. minetest.chat_send_player(name, clearoptparse.print_help())
  433. return
  434. end
  435. local craft_types = split(options.types, ",")
  436. if table.getn(craft_types) > 0 then
  437. for _, craft_type in pairs(craft_types) do
  438. if simplecrafting_lib.type[craft_type] == nil then
  439. minetest.chat_send_player(name, "Craft type " .. craft_type .. " was already clear.", false)
  440. else
  441. simplecrafting_lib.type[craft_type] = nil
  442. end
  443. end
  444. else
  445. simplecrafting_lib.type = {}
  446. end
  447. minetest.chat_send_player(name, "Recipes cleared", false)
  448. end,
  449. })
  450. minetest.register_chatcommand("recipecompare", {
  451. params="<infile> <outfile>",
  452. description="Compares existing recipe data to the data in \"(world folder)/<infile>.lua\", outputting the differences to \"(world folder)/<outfile>.txt\"",
  453. func = function(name, param)
  454. if not minetest.check_player_privs(name, {server = true}) then
  455. minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
  456. return
  457. end
  458. local params = split(param)
  459. if #params ~= 2 then
  460. minetest.chat_send_player(name, "Invalid usage, two filename parameters separted by a space are needed", false)
  461. return
  462. end
  463. if diff_recipes(params[1], params[2]) then
  464. minetest.chat_send_player(name, "Recipes diffed", false)
  465. else
  466. minetest.chat_send_player(name, "Failed to diff recipes", false)
  467. end
  468. end,
  469. })
  470. minetest.register_chatcommand("recipestats", {
  471. params="",
  472. description="Outputs stats about registered recipes",
  473. func = function(name, param)
  474. if not minetest.check_player_privs(name, {server = true}) then
  475. minetest.chat_send_player(name, "You need the \"server\" priviledge to use this command.", false)
  476. return
  477. end
  478. for craft_type, recipe_lists in pairs(simplecrafting_lib.type) do
  479. minetest.chat_send_player(name, "recipe type: "..craft_type)
  480. minetest.chat_send_player(name, tostring(table.getn(recipe_lists.recipes)) .. " recipes")
  481. local max_inputs = 0
  482. for _, recipe in pairs(recipe_lists.recipes) do
  483. local itemcount = 0
  484. for item, count in pairs(recipe.input) do
  485. itemcount = itemcount + 1
  486. end
  487. max_inputs = math.max(max_inputs, itemcount)
  488. end
  489. minetest.chat_send_player(name, "Largest number of input types: " .. tostring(max_inputs))
  490. end
  491. end,
  492. })
  493. -- TODO: need a recipestats command to get general information about recipes