Pipeworks
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

439 lines
14 KiB

  1. local S = minetest.get_translator("pipeworks")
  2. local autocrafterCache = {} -- caches some recipe data to avoid to call the slow function minetest.get_craft_result() every second
  3. local craft_time = 1
  4. local function count_index(invlist)
  5. local index = {}
  6. for _, stack in pairs(invlist) do
  7. if not stack:is_empty() then
  8. local stack_name = stack:get_name()
  9. index[stack_name] = (index[stack_name] or 0) + stack:get_count()
  10. end
  11. end
  12. return index
  13. end
  14. local function get_item_info(stack)
  15. local name = stack:get_name()
  16. local def = minetest.registered_items[name]
  17. local description = def and def.description or S("Unknown item")
  18. return description, name
  19. end
  20. local function get_craft(pos, inventory, hash)
  21. local hash = hash or minetest.hash_node_position(pos)
  22. local craft = autocrafterCache[hash]
  23. if not craft then
  24. local recipe = inventory:get_list("recipe")
  25. local output, decremented_input = minetest.get_craft_result({method = "normal", width = 3, items = recipe})
  26. craft = {recipe = recipe, consumption=count_index(recipe), output = output, decremented_input = decremented_input}
  27. autocrafterCache[hash] = craft
  28. end
  29. return craft
  30. end
  31. local function autocraft(inventory, craft)
  32. if not craft then return false end
  33. local output_item = craft.output.item
  34. -- check if we have enough room in dst
  35. if not inventory:room_for_item("dst", output_item) then return false end
  36. local consumption = craft.consumption
  37. local inv_index = count_index(inventory:get_list("src"))
  38. -- check if we have enough material available
  39. for itemname, number in pairs(consumption) do
  40. if (not inv_index[itemname]) or inv_index[itemname] < number then return false end
  41. end
  42. -- consume material
  43. for itemname, number in pairs(consumption) do
  44. for i = 1, number do -- We have to do that since remove_item does not work if count > stack_max
  45. inventory:remove_item("src", ItemStack(itemname))
  46. end
  47. end
  48. -- craft the result into the dst inventory and add any "replacements" as well
  49. inventory:add_item("dst", output_item)
  50. for i = 1, 9 do
  51. inventory:add_item("dst", craft.decremented_input.items[i])
  52. end
  53. return true
  54. end
  55. -- returns false to stop the timer, true to continue running
  56. -- is started only from start_autocrafter(pos) after sanity checks and cached recipe
  57. local function run_autocrafter(pos, elapsed)
  58. local meta = minetest.get_meta(pos)
  59. local inventory = meta:get_inventory()
  60. local craft = get_craft(pos, inventory)
  61. local output_item = craft.output.item
  62. -- NALC: existence de limitgroup ?
  63. local limitcraft = minetest.get_item_group(output_item:get_name(), "limitcraft") or 0
  64. -- only use crafts that have an actual result
  65. -- NALC: ou si l'item n'est pas dans le group limitcraft
  66. if output_item:is_empty() or limitcraft > 0 then
  67. meta:set_string("infotext", S("unconfigured Autocrafter: unknown recipe"))
  68. return false
  69. end
  70. for step = 1, math.floor(elapsed/craft_time) do
  71. local continue = autocraft(inventory, craft)
  72. if not continue then return false end
  73. end
  74. return true
  75. end
  76. local function start_crafter(pos)
  77. local meta = minetest.get_meta(pos)
  78. if meta:get_int("enabled") == 1 then
  79. local timer = minetest.get_node_timer(pos)
  80. if not timer:is_started() then
  81. timer:start(craft_time)
  82. end
  83. end
  84. end
  85. local function after_inventory_change(pos)
  86. start_crafter(pos)
  87. end
  88. -- note, that this function assumes allready being updated to virtual items
  89. -- and doesn't handle recipes with stacksizes > 1
  90. local function after_recipe_change(pos, inventory)
  91. local meta = minetest.get_meta(pos)
  92. -- if we emptied the grid, there's no point in keeping it running or cached
  93. if inventory:is_empty("recipe") then
  94. minetest.get_node_timer(pos):stop()
  95. autocrafterCache[minetest.hash_node_position(pos)] = nil
  96. meta:set_string("infotext", S("unconfigured Autocrafter"))
  97. inventory:set_stack("output", 1, "")
  98. return
  99. end
  100. local recipe_changed = false
  101. local recipe = inventory:get_list("recipe")
  102. local hash = minetest.hash_node_position(pos)
  103. local craft = autocrafterCache[hash]
  104. if craft then
  105. -- check if it changed
  106. local cached_recipe = craft.recipe
  107. for i = 1, 9 do
  108. if recipe[i]:get_name() ~= cached_recipe[i]:get_name() then
  109. autocrafterCache[hash] = nil -- invalidate recipe
  110. craft = nil
  111. break
  112. end
  113. end
  114. end
  115. craft = craft or get_craft(pos, inventory, hash)
  116. local output_item = craft.output.item
  117. local description, name = get_item_info(output_item)
  118. meta:set_string("infotext", S("'@1' Autocrafter (@2)", description, name))
  119. inventory:set_stack("output", 1, output_item)
  120. after_inventory_change(pos)
  121. end
  122. -- clean out unknown items and groups, which would be handled like unknown items in the crafting grid
  123. -- if minetest supports query by group one day, this might replace them
  124. -- with a canonical version instead
  125. local function normalize(item_list)
  126. for i = 1, #item_list do
  127. local name = item_list[i]
  128. if not minetest.registered_items[name] then
  129. item_list[i] = ""
  130. end
  131. end
  132. return item_list
  133. end
  134. local function on_output_change(pos, inventory, stack)
  135. if not stack then
  136. inventory:set_list("output", {})
  137. inventory:set_list("recipe", {})
  138. else
  139. local input = minetest.get_craft_recipe(stack:get_name())
  140. if not input.items or input.type ~= "normal" then return end
  141. local items, width = normalize(input.items), input.width
  142. local item_idx, width_idx = 1, 1
  143. for i = 1, 9 do
  144. if width_idx <= width then
  145. inventory:set_stack("recipe", i, items[item_idx])
  146. item_idx = item_idx + 1
  147. else
  148. inventory:set_stack("recipe", i, ItemStack(""))
  149. end
  150. width_idx = (width_idx < 3) and (width_idx + 1) or 1
  151. end
  152. -- we'll set the output slot in after_recipe_change to the actual result of the new recipe
  153. end
  154. after_recipe_change(pos, inventory)
  155. end
  156. -- returns false if we shouldn't bother attempting to start the timer again after this
  157. local function update_meta(meta, enabled)
  158. local state = enabled and "on" or "off"
  159. meta:set_int("enabled", enabled and 1 or 0)
  160. local fs = "size[8,12]"..
  161. "list[context;recipe;0,0;3,3;]"..
  162. "image[3,1;1,1;gui_hb_bg.png^[colorize:#141318:255]"..
  163. "list[context;output;3,1;1,1;]"..
  164. "image_button[3,2;1,0.6;pipeworks_button_" .. state .. ".png;" .. state .. ";;;false;pipeworks_button_interm.png]" ..
  165. "list[context;src;0,4.5;8,3;]"..
  166. "list[context;dst;4,0;4,3;]"..
  167. default.gui_bg..
  168. default.gui_bg_img..
  169. default.gui_slots..
  170. default.get_hotbar_bg(0,8) ..
  171. "list[current_player;main;0,8;8,4;]" ..
  172. "listring[current_player;main]"..
  173. "listring[context;src]" ..
  174. "listring[current_player;main]"..
  175. "listring[context;dst]" ..
  176. "listring[current_player;main]"
  177. if minetest.get_modpath("digilines") then
  178. fs = fs.."field[1,3.5;4,1;channel;"..S("Channel")..";${channel}]"
  179. fs = fs.."button_exit[5,3.2;2,1;save;"..S("Save").."]"
  180. end
  181. meta:set_string("formspec",fs)
  182. -- toggling the button doesn't quite call for running a recipe change check
  183. -- so instead we run a minimal version for infotext setting only
  184. -- this might be more written code, but actually executes less
  185. local output = meta:get_inventory():get_stack("output", 1)
  186. if output:is_empty() then -- doesn't matter if paused or not
  187. meta:set_string("infotext", S("unconfigured Autocrafter"))
  188. return false
  189. end
  190. local description, name = get_item_info(output)
  191. local infotext = enabled and S("'@1' Autocrafter (@2)", description, name)
  192. or S("paused '@1' Autocrafter", description)
  193. meta:set_string("infotext", infotext)
  194. return enabled
  195. end
  196. -- 1st version of the autocrafter had actual items in the crafting grid
  197. -- the 2nd replaced these with virtual items, dropped the content on update and set "virtual_items" to string "1"
  198. -- the third added an output inventory, changed the formspec and added a button for enabling/disabling
  199. -- so we work out way backwards on this history and update each single case to the newest version
  200. local function upgrade_autocrafter(pos, meta)
  201. local meta = meta or minetest.get_meta(pos)
  202. local inv = meta:get_inventory()
  203. if inv:get_size("output") == 0 then -- we are version 2 or 1
  204. inv:set_size("output", 1)
  205. -- migrate the old autocrafters into an "enabled" state
  206. update_meta(meta, true)
  207. if meta:get_string("virtual_items") == "1" then -- we are version 2
  208. -- we already dropped stuff, so lets remove the metadatasetting (we are not being called again for this node)
  209. meta:set_string("virtual_items", "")
  210. else -- we are version 1
  211. local recipe = inv:get_list("recipe")
  212. if not recipe then return end
  213. for idx, stack in ipairs(recipe) do
  214. if not stack:is_empty() then
  215. minetest.add_item(pos, stack)
  216. stack:set_count(1)
  217. stack:set_wear(0)
  218. inv:set_stack("recipe", idx, stack)
  219. end
  220. end
  221. end
  222. -- update the recipe, cache, and start the crafter
  223. autocrafterCache[minetest.hash_node_position(pos)] = nil
  224. after_recipe_change(pos, inv)
  225. end
  226. end
  227. minetest.register_node("pipeworks:autocrafter", {
  228. description = S("Autocrafter"),
  229. drawtype = "normal",
  230. tiles = {"pipeworks_autocrafter.png"},
  231. groups = {snappy = 3, tubedevice = 1, tubedevice_receiver = 1},
  232. tube = {insert_object = function(pos, node, stack, direction)
  233. local meta = minetest.get_meta(pos)
  234. local inv = meta:get_inventory()
  235. local added = inv:add_item("src", stack)
  236. after_inventory_change(pos)
  237. return added
  238. end,
  239. can_insert = function(pos, node, stack, direction)
  240. local meta = minetest.get_meta(pos)
  241. local inv = meta:get_inventory()
  242. return inv:room_for_item("src", stack)
  243. end,
  244. input_inventory = "dst",
  245. connect_sides = {left = 1, right = 1, front = 1, back = 1, top = 1, bottom = 1}},
  246. on_construct = function(pos)
  247. local meta = minetest.get_meta(pos)
  248. local inv = meta:get_inventory()
  249. inv:set_size("src", 3*8)
  250. inv:set_size("recipe", 3*3)
  251. inv:set_size("dst", 4*3)
  252. inv:set_size("output", 1)
  253. update_meta(meta, false)
  254. end,
  255. on_receive_fields = function(pos, formname, fields, sender)
  256. if not pipeworks.may_configure(pos, sender) then return end
  257. local meta = minetest.get_meta(pos)
  258. if fields.on then
  259. update_meta(meta, false)
  260. minetest.get_node_timer(pos):stop()
  261. elseif fields.off then
  262. if update_meta(meta, true) then
  263. start_crafter(pos)
  264. end
  265. elseif fields.save then
  266. meta:set_string("channel",fields.channel)
  267. end
  268. end,
  269. can_dig = function(pos, player)
  270. upgrade_autocrafter(pos)
  271. local meta = minetest.get_meta(pos)
  272. local inv = meta:get_inventory()
  273. return (inv:is_empty("src") and inv:is_empty("dst"))
  274. end,
  275. after_place_node = pipeworks.scan_for_tube_objects,
  276. after_dig_node = function(pos)
  277. pipeworks.scan_for_tube_objects(pos)
  278. end,
  279. on_destruct = function(pos)
  280. autocrafterCache[minetest.hash_node_position(pos)] = nil
  281. end,
  282. allow_metadata_inventory_put = function(pos, listname, index, stack, player)
  283. if not pipeworks.may_configure(pos, player) then return 0 end
  284. upgrade_autocrafter(pos)
  285. local inv = minetest.get_meta(pos):get_inventory()
  286. if listname == "recipe" then
  287. stack:set_count(1)
  288. inv:set_stack(listname, index, stack)
  289. after_recipe_change(pos, inv)
  290. return 0
  291. elseif listname == "output" then
  292. on_output_change(pos, inv, stack)
  293. return 0
  294. end
  295. after_inventory_change(pos)
  296. return stack:get_count()
  297. end,
  298. allow_metadata_inventory_take = function(pos, listname, index, stack, player)
  299. if not pipeworks.may_configure(pos, player) then
  300. minetest.log("action", string.format("%s attempted to take from autocrafter at %s", player:get_player_name(), minetest.pos_to_string(pos)))
  301. return 0
  302. end
  303. upgrade_autocrafter(pos)
  304. local inv = minetest.get_meta(pos):get_inventory()
  305. if listname == "recipe" then
  306. inv:set_stack(listname, index, ItemStack(""))
  307. after_recipe_change(pos, inv)
  308. return 0
  309. elseif listname == "output" then
  310. on_output_change(pos, inv, nil)
  311. return 0
  312. end
  313. after_inventory_change(pos)
  314. return stack:get_count()
  315. end,
  316. allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
  317. if not pipeworks.may_configure(pos, player) then return 0 end
  318. upgrade_autocrafter(pos)
  319. local inv = minetest.get_meta(pos):get_inventory()
  320. local stack = inv:get_stack(from_list, from_index)
  321. if to_list == "output" then
  322. on_output_change(pos, inv, stack)
  323. return 0
  324. elseif from_list == "output" then
  325. on_output_change(pos, inv, nil)
  326. if to_list ~= "recipe" then
  327. return 0
  328. end -- else fall through to recipe list handling
  329. end
  330. if from_list == "recipe" or to_list == "recipe" then
  331. if from_list == "recipe" then
  332. inv:set_stack(from_list, from_index, ItemStack(""))
  333. end
  334. if to_list == "recipe" then
  335. stack:set_count(1)
  336. inv:set_stack(to_list, to_index, stack)
  337. end
  338. after_recipe_change(pos, inv)
  339. return 0
  340. end
  341. after_inventory_change(pos)
  342. return count
  343. end,
  344. on_timer = run_autocrafter,
  345. digiline = {
  346. receptor = {},
  347. effector = {
  348. action = function(pos,node,channel,msg)
  349. local meta = minetest.get_meta(pos)
  350. if channel ~= meta:get_string("channel") then return end
  351. if type(msg) == "table" then
  352. if #msg < 3 then return end
  353. local inv = meta:get_inventory()
  354. for y=0,2,1 do
  355. for x=1,3,1 do
  356. local slot = y*3+x
  357. if minetest.registered_items[msg[y+1][x]] then
  358. inv:set_stack("recipe",slot,ItemStack(msg[y+1][x]))
  359. else
  360. inv:set_stack("recipe",slot,ItemStack(""))
  361. end
  362. end
  363. end
  364. after_recipe_change(pos,inv)
  365. elseif msg == "get_recipe" then
  366. local meta = minetest.get_meta(pos)
  367. local inv = meta:get_inventory()
  368. local recipe = {}
  369. for y=0,2,1 do
  370. local row = {}
  371. for x=1,3,1 do
  372. local slot = y*3+x
  373. table.insert(row, inv:get_stack("recipe",slot):get_name())
  374. end
  375. table.insert(recipe, row)
  376. end
  377. local setchan = meta:get_string("channel")
  378. local output = inv:get_stack("output", 1)
  379. digiline:receptor_send(pos, digiline.rules.default, setchan, {
  380. recipe = recipe,
  381. result = {
  382. name = output:get_name(),
  383. count = output:get_count(),
  384. }
  385. })
  386. elseif msg == "off" then
  387. update_meta(meta, false)
  388. minetest.get_node_timer(pos):stop()
  389. elseif msg == "on" then
  390. if update_meta(meta, true) then
  391. start_crafter(pos)
  392. end
  393. elseif msg == "single" then
  394. run_autocrafter(pos,1)
  395. end
  396. end,
  397. },
  398. },
  399. })
  400. minetest.register_craft( {
  401. output = "pipeworks:autocrafter 2",
  402. recipe = {
  403. { "default:steel_ingot", "default:mese_crystal", "default:steel_ingot" },
  404. { "basic_materials:plastic_sheet", "default:steel_ingot", "basic_materials:plastic_sheet" },
  405. { "default:steel_ingot", "default:mese_crystal", "default:steel_ingot" }
  406. },
  407. })