Guest User

Untitled

a guest
Dec 9th, 2020
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.55 KB | None | 0 0
  1. -- HandyHands. Factorio mod: Automatically start handcrafting 1 item on your quickbar whenever your crafting queue is empty. Prioritises items in your hand. It's like logistics slots for early game!
  2. -- Copyright (C) 2016 Qon
  3.  
  4. -- This program is free software: you can redistribute it and/or modify
  5. -- it under the terms of the GNU General Public License as published by
  6. -- the Free Software Foundation, either version 3 of the License, or
  7. -- (at your option) any later version.
  8.  
  9. -- This program is distributed in the hope that it will be useful,
  10. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. -- GNU General Public License for more details.
  13.  
  14. -- You should have received a copy of the GNU General Public License
  15. -- along with this program. If not, see <http://www.gnu.org/licenses/>
  16.  
  17. local mod_name = 'HandyHands'
  18.  
  19. local SHORTCUT_NAME = 'handyhands-toggle'
  20.  
  21. local debugging = false
  22.  
  23. local max_arr = {0, .05, .1, .2, .5, .8, 1, 2, 3, 4}
  24. local work_tick = 5
  25.  
  26.  
  27. local map = require('lib.functional').map
  28. local filter = require('lib.functional').filter
  29. local fnn = require('lib.functional').fnn
  30. local range = require('lib.functional').range
  31. local list_iter_filt_map = require('lib.functional').list_iter_filt_map
  32. local iterlist_iter = require('lib.functional').iterlist_iter
  33.  
  34. -- If we complete an autostarted craft then mark it as done so that the next
  35. -- crafting queue size check knows it wasn't because the player canceled it.
  36. script.on_event(defines.events.on_player_crafted_item, function(event)
  37. -- How can an item stack that I recieve in an event handler and used immediatly be invalid? Apparently it can...
  38. -- This fix will pause HandyHands instead of crashing, at least slightly better.
  39. if not event.item_stack.valid_for_read then return nil end
  40.  
  41. local p = game.players[event.player_index]
  42. local d = global.players_data[event.player_index]
  43. if d.current_job == event.item_stack.name then
  44. d.current_job = nil
  45. d.hh_request_tick = game.tick
  46. if p.crafting_queue and p.crafting_queue[1].count == 1 and p.mod_settings["autocraft-sound-enabled"].value then
  47. p.play_sound{path = 'handyhands-core-crafting_finished'--[[, volume_modifier = 1--]]}
  48. end
  49. elseif --[[p.crafting_queue_size == 1 and--]] p.crafting_queue and p.crafting_queue[1].count == 1 and d.current_job == nil then
  50. -- elseif event.item_stack.count == 1 then
  51. p.play_sound{path = 'handyhands-core-crafting_finished'--[[, volume_modifier = 1--]]}
  52. end
  53. global.players_data[event.player_index] = d
  54. end)
  55.  
  56. -- Init all new joining players.
  57. script.on_event(defines.events.on_player_joined_game, function(event)
  58. local p = game.players[event.player_index]
  59. if debugging then p.print('joined!') end
  60. local d = global.players_data[event.player_index]
  61. if d == nil then
  62. d = init_player(event.player_index)
  63. end
  64. global.players_data[event.player_index] = d
  65. end)
  66.  
  67. -- The mod core logic
  68. script.on_event(defines.events.on_tick, function(event)
  69. for player_index = game.tick % work_tick + 1, #game.players, work_tick do
  70. local p = game.players[player_index]
  71. if p.connected and p.controller_type == defines.controllers.character then
  72. local d = global.players_data[player_index]
  73. local canceled_autocraft = false
  74. if d.current_job ~= nil and p.crafting_queue_size > 0 then
  75. if d.current_job ~= p.crafting_queue[#p.crafting_queue].recipe or p.crafting_queue[#p.crafting_queue].count > 1 then
  76. d.current_job = nil
  77. elseif p.crafting_queue[d.current_job] ~= nil and p.crafting_queue[d.current_job].recipe == nil then
  78. canceled_autocraft = true
  79. enabled(player_index, false)
  80. -- d.paused = true
  81. end
  82. end
  83. if p.crafting_queue_size == 0 then
  84. if d.current_job ~= nil then
  85. canceled_autocraft = true
  86. enabled(player_index, false)
  87. -- d.paused = true
  88. end
  89. -- end
  90. -- if p.crafting_queue_size == 0 then
  91. if not d.paused and (d.hh_request_tick + work_tick >= game.tick or game.tick % 200 == game.tick % 5) then
  92. d.hh_request_tick = d.hh_request_tick - work_tick
  93. hh_player(player_index)
  94. end
  95. end
  96. if canceled_autocraft then
  97. p.print(mod_name..' is now paused until you hit increase or decrease key (Options > Controls > Mods).')
  98. d.current_job = nil
  99. end
  100. end
  101. end
  102. end)
  103.  
  104. local stack_size_cache = {}
  105. function stack_size(item)
  106. stack_size_cache[item] = stack_size_cache[item] or game.item_prototypes[item].stack_size
  107. return stack_size_cache[item]
  108. end
  109.  
  110. -- For better interaction with stack size changing mods: Pretend that the max stack size is only 1/10 as big.
  111. function pretend_stack_size(stack_size)
  112. if stack_size >= 500 then return stack_size / 10 else return stack_size end
  113. end
  114.  
  115. function get_request_count(p, d, item)
  116. local setting = d.settings[item]
  117. if setting == nil then
  118. setting = d.settings['Default']
  119. if setting == nil then
  120. p.print(mod_name..' Error: Uninitialised Default setting.')
  121. end
  122. end
  123. local stsz = pretend_stack_size(stack_size(item))
  124. local mi = math.ceil(stsz*setting)
  125.  
  126.  
  127. local pl = p.mod_settings['logistics-requests-are-autocraft-requests'].value
  128. if p.character and
  129. (pl == 'Always' or pl == 'When personal logistics requests are enabled' and p.character_personal_logistic_requests_enabled) and
  130. d.personal_logistics_requests[item] then
  131. if d.settings[item] == nil then
  132. mi = d.personal_logistics_requests[item].min
  133. -- else
  134. -- mi = math.max(mi, d.personal_logistics_requests[item].min)
  135. end
  136. end
  137.  
  138. -- Don't keep autocrafting this item if it will just end up in logistics trash slots.
  139. if p.character and p.get_inventory(defines.inventory.character_trash).get_item_count(item) > 0 then -- if p.character and p.auto_trash_filters[item] then
  140. mi = math.min(mi, p.get_inventory(defines.inventory.character_trash).get_item_count(item)) -- mi = math.min(mi, p.auto_trash_filters[item])
  141. end
  142.  
  143.  
  144. return mi
  145. end
  146.  
  147. function build_request_iterator(p, d)
  148. -- List is an iterator over all items that are candidates for autocrafting.
  149. -- qb: The quickbar items. We filter away items which are not "filtered" (blue item lock on quickbar) and pick the "filter_" instead of the item stack.
  150. local list = {}
  151.  
  152. if p.mod_settings['quickbar-slots-are-autocraft-requests'].value then
  153. local aqb = fnn(map(range(1,4), function(q) return p.get_active_quick_bar_page(q) end))
  154. local slots = iterlist_iter(map(aqb, function(page)
  155. return map(range(1,10), function(index)
  156. return (page-1)*10+index
  157. end)
  158. end))
  159. slots = filter(map(slots, function(q)
  160. local slot = p.get_quick_bar_slot(q)
  161. if slot ~= nil then return slot.name end
  162. return ''
  163. end), function(q) return q ~= '' end)
  164. table.insert(list, slots)
  165.  
  166. -- All non-empty ammo_bar stacks are autocrafting candidates.
  167. local ammo_bar = p.get_inventory(defines.inventory.character_ammo)
  168. if p.character then
  169. table.insert(list, list_iter_filt_map(ammo_bar, function(q,i,a) return q.valid_for_read end, function(q,i,a) return q.name end))
  170. end
  171. end
  172.  
  173. -- Manual settings allows autocrafting in inventory.
  174. local inventory_crafting = {}
  175. for key, value in pairs(d.settings) do
  176. table.insert(inventory_crafting, key)
  177. end
  178. table.insert(list, list_iter_filt_map(inventory_crafting, function(q) return q ~= 'Default' end))
  179.  
  180. -- Personal logistics requests
  181. local logistics_requests = {}
  182. local pl = p.mod_settings['logistics-requests-are-autocraft-requests'].value
  183. if pl == 'Always' or pl == 'When personal logistics requests are enabled' and p.character_personal_logistic_requests_enabled then
  184. d.personal_logistics_requests = d.personal_logistics_requests or {}
  185. for i = 1, p.character.request_slot_count do -- for i = 1, p.character_logistic_slot_count do
  186. local logi_request = p.get_personal_logistic_slot(i)
  187. if logi_request.min > 0 then
  188. table.insert(logistics_requests, logi_request.name)
  189. d.personal_logistics_requests[logi_request.name] = logi_request
  190. end
  191. end
  192. end
  193. table.insert(list, list_iter_filt_map(logistics_requests))
  194.  
  195. return list
  196. end
  197.  
  198. local a_recipe_memo
  199. function a_recipe(item_name, player)
  200. local cheat = player == nil or player.cheat_mode
  201.  
  202. local recipe = player.force.recipes[item_name]
  203. if recipe ~= nil then
  204. if cheat or (recipe.enabled and player.get_craftable_count(recipe) > 0) then return recipe end
  205. end
  206.  
  207. if a_recipe_memo == nil then
  208. a_recipe_memo = {}
  209. for recipe_name, recipe_prot in pairs(game.get_filtered_recipe_prototypes({{filter = "has-product-item"}})) do
  210. for _, product in ipairs(recipe_prot.products) do
  211. if product.name ~= recipe_name then
  212. a_recipe_memo[product.name] = a_recipe_memo[product.name] or {}
  213. table.insert(a_recipe_memo[product.name], recipe_prot)
  214. end
  215. end
  216. end
  217. end
  218. local multi_product_allowed = player.mod_settings['handyhands-autocraft-multi-product-recipes'].value
  219. local recipe_prots = a_recipe_memo[item_name]
  220. if recipe_prots ~= nil then
  221. for _, recipe_prot in ipairs(recipe_prots) do
  222. if (cheat or
  223. (player.force.recipes[recipe_prot.name].enabled and player.get_craftable_count(recipe_prot.name) > 0))
  224. and (multi_product_allowed or #recipe_prot.products <= 1) then
  225. return recipe_prot.name
  226. end
  227. end
  228. end
  229. end
  230.  
  231. function craft(p, d, item, count)
  232. if p.cheat_mode == false then
  233. p.begin_crafting{count=1, recipe=a_recipe(item, p), silent=true}
  234. d.current_job = item
  235. else
  236. p.begin_crafting{count=count, recipe=a_recipe(item, p), silent=true}
  237. end
  238. end
  239.  
  240. function hh_player(player_index)
  241. local p = game.players[player_index]
  242. if p.connected and p.controller_type ~= defines.controllers.character then return nil end
  243. if p.crafting_queue_size ~= 0 then return nil end
  244. local d = global.players_data[player_index]
  245. if d.paused then return nil end
  246. -- local mi = p.get_main_inventory()
  247.  
  248. local selected_item = nil
  249. local count_selected = nil
  250. local max_selected = nil
  251. local cs = p.cursor_stack
  252. local item
  253.  
  254. list = iterlist_iter(build_request_iterator(p, d))
  255. for item in list do
  256. -- Check that we can craft this item. If not, skip.
  257. local ci = p.get_item_count(item)
  258. local mi = get_request_count(p, d, item)
  259. if debugging then p.print('debug '..item..' '..ci..' '..mi) end
  260. local recipe = a_recipe(item, p) -- p.force.recipes[item]
  261. if ci < mi and recipe ~= nil then
  262. local prio_selected = 1
  263. if selected_item ~= nil then
  264. prio_selected = count_selected/max_selected
  265. end
  266. -- prio is a bit backwards. Lower prio number values for prioritized items.
  267. local prio_current = ci/mi
  268. local item_held = cs.valid_for_read and cs.name == item
  269. -- Prioritise items held in cursor above all. Prioritise less fulfilled autocraft requests.
  270. local prioritised = prio_current < prio_selected or item_held
  271. -- Check that we can craft the item
  272. if prio_current < 1 and prioritised then
  273. selected_item = item
  274. count_selected = ci
  275. max_selected = mi
  276. if cs.valid_for_read and cs.name == item then
  277. break
  278. end
  279. end
  280. end
  281. end
  282. if selected_item ~= nil then
  283. craft(p, d, selected_item, max_selected - count_selected)
  284. end
  285. end
  286.  
  287. -- Handle hotkey presses.
  288. script.on_event('handyhands-increase', function(event) change(event, true) end)
  289. script.on_event('handyhands-decrease', function(event) change(event, false) end)
  290.  
  291.  
  292. -- local event_handlers = {
  293.  
  294. -- on_lua_shortcut = function(event)
  295. -- -- if event.prototype_name ~= SHORTCUT_NAME then return end
  296. -- local p = game.players[event.player_index]
  297. -- printOrFly(p, '1 '..event.prototype_name)
  298. -- remotes.paused(event.player_index, not remotes.paused(event.player_index))
  299. -- p.set_shortcut_toggled(SHORTCUT_NAME, remotes.paused(event.player_index))
  300. -- end,
  301.  
  302. -- -- ['handyhands-increase'] = function(event) change(event, true) end,
  303. -- -- ['handyhands-decrease'] = function(event) change(event, false) end
  304.  
  305. -- -- [SHORTCUT_NAME] = function(event)
  306. -- -- local p = game.players[event.player_index]
  307. -- -- printOrFly(p, '2')
  308. -- -- set_enabled(p, not p.is_shortcut_toggled(SHORTCUT_NAME))
  309. -- -- end,
  310. -- }
  311. -- for event_name, handler in pairs (event_handlers) do
  312. -- script.on_event(defines.events[event_name] or event_name, handler)
  313. -- end
  314.  
  315.  
  316. function enabled(player_index, set)
  317. if set ~= nil then
  318. global.players_data[player_index].paused = not set
  319. local p = game.players[player_index]
  320. -- printOrFly(p, set)
  321. p.set_shortcut_toggled(SHORTCUT_NAME, enabled(player_index))
  322. end
  323. return not global.players_data[player_index].paused
  324. end
  325.  
  326. script.on_event(defines.events.on_lua_shortcut, function(event)
  327. -- game.print(game.table_to_json(event))
  328. if event.prototype_name ~= SHORTCUT_NAME then return end
  329. local p = game.players[event.player_index]
  330. enabled(event.player_index, not enabled(event.player_index))
  331. end)
  332.  
  333. function change(event, positive)
  334. local p = game.players[event.player_index]
  335. -- init_player(event.player_index, false)
  336. local d = global.players_data[event.player_index]
  337. d.hh_request_tick = game.tick
  338. -- global.players_data[event.player_index] = d
  339. if d.paused == true then
  340. enabled(event.player_index, true)
  341. -- d = global.players_data[event.player_index]
  342. printOrFly(p, mod_name..' is now running again!')
  343. return
  344. end
  345. local item = 'Default'
  346. if p.cursor_stack.valid_for_read == true then
  347. item = p.cursor_stack.name
  348. if d.settings[item] == nil then
  349. d.settings[item] = d.settings['Default']
  350. end
  351. end
  352. local changed = false
  353. if positive then
  354. for i = 1, #max_arr do
  355. if max_arr[i] > d.settings[item] then
  356. d.settings[item] = max_arr[i]
  357. changed = true
  358. break
  359. end
  360. end
  361. else
  362. for i = 1, #max_arr do
  363. if max_arr[#max_arr-i+1] < d.settings[item] then
  364. d.settings[item] = max_arr[#max_arr-i+1]
  365. changed = true
  366. break
  367. end
  368. end
  369. end
  370. if changed then
  371. function settingsmessage(item)
  372. local trash_warning = ''
  373. if p.character and p.get_inventory(defines.inventory.character_trash).get_item_count(item) ~= nil and p.get_inventory(defines.inventory.character_trash).get_item_count(item) < math.ceil(game.item_prototypes[item].stack_size*d.settings[item]) then
  374. trash_warning = ' [Auto trash: '..p.get_inventory(defines.inventory.character_trash).get_item_count(item)..']'
  375. end
  376. -- Add the / 10 on bigger stacksize
  377. return '[item='..item..']: '..d.settings[item]..' stacks ('..math.ceil(pretend_stack_size(game.item_prototypes[item].stack_size)*d.settings[item])..' items)'..trash_warning
  378. end
  379. function printall()
  380. p.print('Changed default autocraft stack size: '..d.settings['Default']..' stacks.')
  381. for k in pairs(d.settings) do
  382. if k ~= 'Default' then p.print(settingsmessage(k)) end
  383. end
  384. end
  385. if item == 'Default' then printall()
  386. else printOrFly(p, settingsmessage(item)) end
  387. elseif positive == false and d.settings[item] == 0 then
  388. if item == 'Default' then
  389. init_player_settings(event.player_index, true)
  390. d = global.players_data[event.player_index]
  391. d.settings['Default'] = 0
  392. p.print('All your HandyHands settings were deleted.')
  393. else
  394. d.settings[item] = nil
  395. printOrFly(p, 'Your HandyHands setting for [item='..item..'] was deleted [Default is '..d.settings['Default']..' stacks]')
  396. end
  397. end
  398. global.players_data[event.player_index] = d
  399. end
  400.  
  401. function printOrFly(p, text)
  402. if p.character ~= nil then
  403. p.create_local_flying_text({
  404. ['text'] = text,
  405. ['position'] = p.character.position
  406. })
  407. else
  408. p.print(text)
  409. end
  410. end
  411.  
  412. function init(event, force)
  413. -- Might be called with nil event and force
  414. if global.players_data == nil or force then
  415. global.players_data = {}
  416. end
  417. for i = 1, #game.players do
  418. init_player(i, force)
  419. end
  420. end
  421.  
  422. function init_player(player_index, force)
  423. local player = game.players[player_index]
  424. if player.mod_settings['logistics-requests-are-autocraft-requests'].value ~= 'Never' and not player.force.character_logistic_requests then
  425. player.force.character_logistic_requests = true
  426. end
  427. return init_player_settings(player_index, force)
  428. end
  429.  
  430. function init_player_settings(player_index, force)
  431. local wasnil = global.players_data[player_index] == nil
  432. if wasnil or force then
  433. global.players_data[player_index] = {}
  434. global.players_data[player_index].hh_request_tick = 0
  435. global.players_data[player_index].hh_last_exec_tick = 0
  436. global.players_data[player_index].personal_logistics_requests = {}
  437. global.players_data[player_index].settings = {}
  438. global.players_data[player_index].settings['Default'] = 0.2
  439. -- Only for beginners, so that you don't lose your starting iron to ammo before you have your pick axe ;>
  440. if wasnil then
  441. global.players_data[player_index].settings['firearm-magazine'] = 0.05*100/game.item_prototypes['firearm-magazine'].stack_size
  442. game.players[player_index].print(
  443. 'HandyHands autocrafting enabled for quickbar filtered items and ammo. Default amount: '..global.players_data[player_index].settings['Default']..' stacks.'
  444. )
  445. game.players[player_index].print('Change HandyHands autocrafting limits with hotkeys (Options > Controls > Mods) or personal logistics requests.')
  446. game.players[player_index].print('Empty cursor: change Default. Forget all HandyHands settings by deleting Default setting.')
  447. game.players[player_index].print('Individual item settings are modified when held in cursor and deleted when decreased below 0.')
  448. end
  449. end
  450. return global.players_data[player_index]
  451. end
  452.  
  453.  
  454.  
  455. script.on_init(init)
  456. script.on_event(defines.events.on_player_joined_game, function(event) init_player(event.player_index, false) end)
  457. script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
  458. if event.setting == 'logistics-requests-are-autocraft-requests' and event.player_index then
  459. local player = game.players[event.player_index]
  460. if player.mod_settings['logistics-requests-are-autocraft-requests'].value ~= 'Never' and not player.force.character_logistic_requests then
  461. player.force.character_logistic_requests = true
  462. end
  463. end
  464. end)
  465.  
  466. -- Data migration
  467. script.on_configuration_changed(function(event)
  468. for i, p in pairs(game.players) do
  469. if global.players_data and global.players_data[i] then
  470. local d = global.players_data[i]
  471. for item, value in pairs(d.settings) do
  472. if item ~= 'Default' and not game.item_prototypes[item] then d.settings[item] = nil end
  473. end
  474. end
  475. end
  476. end)
  477.  
  478. remote.add_interface('handyhands', {
  479. -- call change(player_index, positive) to simulate increase/decrease hotkey events. (positive: true for increase, false for decrease)
  480. change = function(player_index, positive) change({player_index = player_index}, positive) end,
  481. -- set(player_index, item, limit) sets the autocraft limit for a specific item to the provided limit. limit == nil to remove the setting. Limits are in stacks (float).
  482. set = function(player_index, item, limit) global.players_data[player_index].settings[item] = limit end,
  483. -- call settings(player_index) to get the Key/Value pairs to get the stack size limits
  484. -- call settings(player_index, limits) to set the Key/Value pairs stack size limits (limits is a Key[item_name] --> Value (stacks_to_craft) object)
  485. settings = function(player_index, limits)
  486. if limits ~= nil then
  487. global.players_data[player_index].settings = limits
  488. end
  489. return global.players_data[player_index].settings
  490. end,
  491. -- call enabled(player_index) to get enabled state for the player
  492. -- call enabled(player_index, set) to set enabled state for the player (set is a boolean)
  493. enabled = enabled,
  494. })
Add Comment
Please, Sign In to add comment