init.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. -- This is a mod where functions shared by most machines (tools) are located.
  2. -- This avoides excessive code duplication. Machine tools include centrifuge,
  3. -- electric furnace, extractor, grinder, and others. These machines behave
  4. -- generally in similar ways.
  5. machines = machines or {}
  6. machines.modpath = minetest.get_modpath("machines")
  7. local NEED_LOG = true
  8. local SINGLEPLAYER = minetest.is_singleplayer()
  9. -- Localize for performance.
  10. local math_floor = math.floor
  11. -- API function, for logging machine updates. Needed during debugging.
  12. machines.log_update =
  13. function(pos, name)
  14. if SINGLEPLAYER == true and NEED_LOG == true then
  15. minetest.chat_send_all("# Server: " .. name .. " updates @ " .. rc.pos_to_namestr(pos) .. ".")
  16. end
  17. end
  18. -- Typedata is used when traversing the network, without touching the node.
  19. -- It must contain as much data as needed to get the node even if unloaded.
  20. -- This must be done after node construction.
  21. -- This should also be done when punched, to allow old nodes to be upgraded.
  22. machines.initialize_typedata =
  23. function(pos, name, tier)
  24. local meta = minetest.get_meta(pos)
  25. meta:set_string("technic_machine", "yes")
  26. meta:set_string("technic_type", "tool")
  27. meta:set_string("technic_tier", tier)
  28. meta:set_string("technic_name", name)
  29. end
  30. machines.get_energy =
  31. function(from, to, wanted_charge)
  32. local meta = minetest.get_meta(from)
  33. local node = minetest.get_node(to)
  34. local def = minetest.reg_ns_nodes[node.name]
  35. -- Make sure energy can actually be extracted from the node.
  36. if def and def.on_machine_execute then
  37. local table_in = {
  38. purpose = "retrieve_eu",
  39. }
  40. local table_out = {
  41. wanted_eu = wanted_charge,
  42. }
  43. local traversal = {}
  44. -- Do not process self.
  45. local hash = minetest.hash_node_position(from)
  46. traversal[hash] = 0
  47. def.on_machine_execute(to, table_in, table_out, traversal)
  48. return (table_out.gotten_eu or 0)
  49. end
  50. return 0
  51. end
  52. -- API function. Try to get cooktime for a machine by draining power off the network.
  53. machines.get_cooktime =
  54. function(pos, name_cfg, name_ugp, wanted_time, eu_demand)
  55. local meta = minetest.get_meta(pos)
  56. local inv = meta:get_inventory()
  57. -- By default, machine wants this many EUs per/sec.
  58. local eups = eu_demand
  59. local upglist = inv:get_list(name_ugp) -- Name configurable.
  60. -- Old machines might not have the upgrade inventory.
  61. if upglist then
  62. for k, v in ipairs(upglist) do
  63. if v:get_name() == "battery:battery" and v:get_count() == 1 then
  64. -- Each battery upgrade reduces energy needed by 1/3 of previous.
  65. eups = math_floor((eups / 3) * 2)
  66. end
  67. end
  68. end
  69. local wanted_eu = wanted_time * eups
  70. local tried_once = false
  71. ::try_again::
  72. -- If enough energy is buffered we can grab it & run.
  73. local eu_buffered = meta:get_int("eu_buffered")
  74. if wanted_eu <= eu_buffered then
  75. eu_buffered = eu_buffered - wanted_eu
  76. meta:set_int("eu_buffered", eu_buffered)
  77. return wanted_time
  78. end
  79. if tried_once == false then
  80. -- Otherwise, we have to refill the buffer.
  81. -- Get location of network access.
  82. local netpos = machines.get_adjacent_network_hubs(pos, {"mv"})
  83. -- Energy source(s) found.
  84. for k, v in ipairs(netpos) do
  85. -- Should be enough for at least 1 of even the longest cooking item.
  86. -- If not, how could we fix this? Maybe just reduce the cooktime for the
  87. -- offending item ....
  88. local charge = machines.get_energy(pos, v, 10000)
  89. eu_buffered = eu_buffered + charge
  90. meta:set_int("eu_buffered", eu_buffered)
  91. tried_once = true
  92. goto try_again
  93. end
  94. end
  95. -- No energy obtained. (Machine should fallback to fuel, if any.)
  96. return 0
  97. end
  98. -- API function. Most machines share this function in common.
  99. machines.allow_metadata_inventory_put =
  100. function(pos, listname, index, stack, player, fueltype, fuelname, srcname, dstname, cfgname, upgname)
  101. local pname = player:get_player_name()
  102. if minetest.test_protection(pos, pname) then return 0 end
  103. if listname == fuelname then
  104. if type(fueltype) == "string" then
  105. local cresult = minetest.get_craft_result({
  106. method = fueltype,
  107. width = 1,
  108. items = {stack},
  109. })
  110. if cresult.time ~= 0 then -- Valid fuel.
  111. return stack:get_count()
  112. end
  113. elseif type(fueltype) == "table" then
  114. for k, v in ipairs(fueltype) do
  115. local cresult = minetest.get_craft_result({
  116. method = v,
  117. width = 1,
  118. items = {stack},
  119. })
  120. if cresult.time ~= 0 then -- Valid fuel.
  121. return stack:get_count()
  122. end
  123. end
  124. end
  125. elseif listname == srcname then
  126. return stack:get_count()
  127. elseif dstname and listname == dstname then
  128. return 0
  129. elseif listname == cfgname and stack:get_name() == "cfg:dev" then
  130. return stack:get_count()
  131. elseif listname == upgname and stack:get_name() == "battery:battery" then
  132. return stack:get_count()
  133. end
  134. return 0
  135. end
  136. machines.on_machine_execute =
  137. function(pos, table_in, table_out, traversal)
  138. -- We do not check for recursion depth for crafting machines because
  139. -- these machines cannot be chains, so there is no problem with network size.
  140. -- Do not process this node more than once.
  141. local hash = minetest.hash_node_position(pos)
  142. if traversal[hash] then return end
  143. traversal[hash] = 0
  144. local purpose = table_in.purpose
  145. if purpose == "autostart_trigger" then
  146. local timer = minetest.get_node_timer(pos)
  147. if not timer:is_started() then
  148. timer:start(1.0)
  149. end
  150. end
  151. end
  152. -- API function to get locations of network hubs adjacent to a machine.
  153. -- This function must return a table of all adjacent hubs.
  154. machines.get_adjacent_network_hubs =
  155. function(pos, tiers)
  156. -- If `tiers` is omitted or nil, then all tiers are allowed.
  157. if not tiers then tiers = {"lv", "mv", "hv"} end
  158. -- Return list of discovered network hubs.
  159. local hubs = {}
  160. -- List of valid adjacent locations.
  161. local targets = {
  162. {x=pos.x+1, y=pos.y, z=pos.z},
  163. {x=pos.x-1, y=pos.y, z=pos.z},
  164. {x=pos.x, y=pos.y, z=pos.z+1},
  165. {x=pos.x, y=pos.y, z=pos.z-1},
  166. {x=pos.x, y=pos.y-1, z=pos.z},
  167. {x=pos.x, y=pos.y+1, z=pos.z},
  168. }
  169. -- Get all adjacent nodes once.
  170. local nodes = {}
  171. for k, v in ipairs(targets) do
  172. local node = minetest.get_node(v)
  173. nodes[#nodes+1] = {node=node, pos=v}
  174. end
  175. -- Scan through adjacent nodes and find valid ones.
  176. for j, t in ipairs(tiers) do
  177. for k, v in ipairs(nodes) do
  178. if v.node.name == "switching_station:" .. t then
  179. hubs[#hubs+1] = v.pos
  180. end
  181. end
  182. end
  183. return hubs
  184. end
  185. -- Public API function.
  186. -- Swap node only if current node is not the wanted node.
  187. machines.swap_node = function(pos, name)
  188. local node = minetest.get_node(pos)
  189. if node.name == name then
  190. -- Node not swapped.
  191. return false
  192. end
  193. node.name = name
  194. minetest.swap_node(pos, node)
  195. -- Node swapped.
  196. return true
  197. end
  198. -- Public API function.
  199. -- This function implements unified operation for all machine tools.
  200. -- It is based on furnace cooking code, but concept applies broadly.
  201. machines.on_machine_timer =
  202. function(pos, elapsed, data)
  203. -- Get or inizialize metadata.
  204. local meta = minetest.get_meta(pos)
  205. local fuel_time = (meta:get_float("fuel_time") or 0)
  206. local src_time = (meta:get_float("src_time") or 0)
  207. local fuel_totaltime = (meta:get_float("fuel_totaltime") or 0)
  208. local inv = meta:get_inventory()
  209. local srclist = inv:get_list("src")
  210. local fuellist = inv:get_list("fuel")
  211. -- Begin cooking logic.
  212. -- Check if we have cookable content.
  213. local cooked, aftercooked = minetest.get_craft_result({
  214. method = data.method,
  215. width = 1,
  216. items = srclist,
  217. })
  218. local cookable = true
  219. if cooked.time == 0 then
  220. cookable = false
  221. end
  222. -- Check if we have enough fuel to burn.
  223. if fuel_time < fuel_totaltime then
  224. -- The furnace is currently active and has enough fuel.
  225. fuel_time = fuel_time + 1
  226. -- If there is a cookable item then check if it is ready yet.
  227. if cookable then
  228. src_time = src_time + 1
  229. if src_time >= cooked.time then
  230. -- Place result in dst list if possible.
  231. -- Note that separating recipes have 2 outputs, not 1.
  232. -- Alloying recipes have 2 inputs and 1 output.
  233. if data.method == "separating" then
  234. if inv:room_for_item("dst", cooked.item[1]) and
  235. inv:room_for_item("dst", cooked.item[2]) then
  236. inv:add_item("dst", cooked.item[1])
  237. inv:add_item("dst", cooked.item[2])
  238. inv:set_stack("src", 1, aftercooked.items[1])
  239. end
  240. elseif data.method == "alloying" then
  241. if inv:room_for_item("dst", cooked.item) then
  242. inv:add_item("dst", cooked.item)
  243. inv:set_stack("src", 1, aftercooked.items[1])
  244. inv:set_stack("src", 2, aftercooked.items[2])
  245. end
  246. else
  247. if inv:room_for_item("dst", cooked.item) then
  248. inv:add_item("dst", cooked.item)
  249. inv:set_stack("src", 1, aftercooked.items[1])
  250. end
  251. end
  252. src_time = 0
  253. end
  254. end
  255. else
  256. -- Machine ran out of fuel/energy.
  257. if cookable then
  258. -- Try to get cooktime from energy.
  259. local gotten_cooktime = machines.get_cooktime(pos, 'cfg', 'upg', cooked.time, data.demand)
  260. if gotten_cooktime >= cooked.time then
  261. fuel_totaltime = gotten_cooktime
  262. fuel_time = 0
  263. else
  264. -- We need to get new fuel.
  265. local fuel, afterfuel = minetest.get_craft_result({
  266. method = data.fuel,
  267. width = 1,
  268. items = fuellist,
  269. })
  270. if fuel.time == 0 then
  271. -- No valid fuel in fuel list.
  272. fuel_totaltime = 0
  273. fuel_time = 0
  274. src_time = 0
  275. else
  276. -- Take fuel from fuel list.
  277. inv:set_stack("fuel", 1, afterfuel.items[1])
  278. fuel_totaltime = fuel.time
  279. fuel_time = 0
  280. end
  281. end
  282. else
  283. -- We don't need to get new fuel since there is no cookable item.
  284. fuel_totaltime = 0
  285. fuel_time = 0
  286. src_time = 0
  287. end
  288. end
  289. -- Update formspec, infotext and node.
  290. local formspec = data.form.inactive()
  291. local item_state
  292. local item_percent = 0
  293. if cookable then
  294. item_percent = math_floor(src_time / cooked.time * 100)
  295. if item_percent > 100 then
  296. item_state = "100% (Output Full)"
  297. else
  298. item_state = item_percent .. "%"
  299. end
  300. else
  301. if srclist[1]:is_empty() then
  302. item_state = "Empty"
  303. else
  304. item_state = "Not " .. data.processable
  305. end
  306. end
  307. local fuel_state = "Empty"
  308. local active = "Standby"
  309. local result = false
  310. if fuel_time <= fuel_totaltime and fuel_totaltime ~= 0 then
  311. active = "Active"
  312. local fuel_percent = math_floor(fuel_time / fuel_totaltime * 100)
  313. fuel_state = fuel_percent .. "%"
  314. formspec = data.form.active(fuel_percent, item_percent)
  315. machines.swap_node(pos, data.swap.active)
  316. -- Make sure timer restarts automatically.
  317. result = true
  318. else
  319. if not fuellist[1]:is_empty() then
  320. fuel_state = "0%"
  321. end
  322. machines.swap_node(pos, data.swap.inactive)
  323. -- Stop timer on the inactive furnace.
  324. local timer = minetest.get_node_timer(pos)
  325. timer:stop()
  326. end
  327. -- Compose infotext.
  328. local infotext = data.name .. " (" .. active .. ")\n" ..
  329. "Item: " .. item_state .. "\n" .. "Fuel Burn: " .. fuel_state .. "\n" ..
  330. "Buffered: " .. meta:get_int("eu_buffered") .. " EUs"
  331. -- Set meta values.
  332. meta:set_float("fuel_totaltime", fuel_totaltime)
  333. meta:set_float("fuel_time", fuel_time)
  334. meta:set_float("src_time", src_time)
  335. meta:set_string("formspec", formspec)
  336. meta:set_string("infotext", infotext)
  337. return result
  338. end
  339. -- API constructor for standard cooking/grinding/extracting/etc. machines.
  340. machines.after_place_machine =
  341. function(pos, placer, name, size, form)
  342. local pname = placer:get_player_name()
  343. local meta = minetest.get_meta(pos)
  344. local inv = meta:get_inventory()
  345. inv:set_size('src', size)
  346. inv:set_size('fuel', 1)
  347. inv:set_size('dst', 4)
  348. inv:set_size('cfg', 2)
  349. inv:set_size('upg', 2)
  350. -- Compose infotext.
  351. local infotext = name .. " (Standby)\n" ..
  352. "Item: Empty\nFuel Burn: Empty\n" ..
  353. "Buffered: 0 EUs"
  354. meta:set_string("formspec", form())
  355. meta:set_string("infotext", infotext)
  356. end
  357. -- Deliver EUs to batteries on the network. Return EUs left over.
  358. -- This would be called by machines that produce EUs.
  359. machines.deliver_charge_to_network =
  360. function(from, to, charge)
  361. local node = minetest.get_node(to)
  362. local def = minetest.reg_ns_nodes[node.name]
  363. if def and def.on_machine_execute then
  364. local table_in = {
  365. purpose = "store_eu",
  366. }
  367. local table_out = {
  368. amount_eu = charge,
  369. }
  370. local traversal = {}
  371. -- Do not process self.
  372. local hash = minetest.hash_node_position(from)
  373. traversal[hash] = 0
  374. def.on_machine_execute(to, table_in, table_out, traversal)
  375. return (table_out.amount_eu or 0)
  376. end
  377. -- No network, so all EUs are left over.
  378. return charge
  379. end
  380. if not machines.run_once then
  381. local c = "machines:core"
  382. local f = machines.modpath .. "/init.lua"
  383. reload.register_file(c, f, false)
  384. -- A 'dummy' item that represents 1 unit of atomic energy.
  385. -- This item is never supposed to appear in a player's inventory.
  386. -- It cannot be crafted and should never be loose in the world.
  387. -- We register it so we can use it in machine inventory slots.
  388. minetest.register_craftitem(":atomic:energy", {
  389. description = "Energy",
  390. inventory_image = "electric_ball.png",
  391. -- Engine limit.
  392. stack_max = 65535,
  393. })
  394. dofile(machines.modpath .. "/common.lua")
  395. dofile(machines.modpath .. "/solar.lua")
  396. dofile(machines.modpath .. "/reactor.lua")
  397. dofile(machines.modpath .. "/windy.lua")
  398. dofile(machines.modpath .. "/tide.lua")
  399. dofile(machines.modpath .. "/panel.lua")
  400. dofile(machines.modpath .. "/leecher.lua")
  401. dofile(machines.modpath .. "/charger.lua")
  402. dofile(machines.modpath .. "/workshop.lua")
  403. machines.run_once = true
  404. end