init.lua 8.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
--[[
Minetest+specing Client-Side Mod (CSM) for automatically moving items between inventories
Copyright (C) 2021 Fedja Beader

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
--]]

-- general idea:
--   register multiple locations (node, inventory, detached) hereafter refered to as 'machines'
--   each machine can be either a converter or storage
--   each converter has an associated recipe
--   -> accepts predetermined amount of different items and produces an amount of output items
--   -> for now, we assume that output is only one. (battery recipe has two - bucket and battery)
--
-- in each step, we:
-- 1) pick one machine from the list
-- 2) go through all other machines and collect available amount of input material
-- 3) move as much input material as possible (up to max_stack), such that there will be no recipe leftovers

--[[
register_on_jumpdrive_jump(function (jump)
	l:info("translating coordinates on jump")
	for i,m in pairs(machines) do
		translate_position(     jump, m.pos)
	end
end)
--]]

local l = Logger:new("invnet")
local mod_storage = core.get_mod_storage()
local invnet = minetest.deserialize(mod_storage:get_string("network")) or {}
invnet.enable   = invnet.enable or false
invnet.machines = invnet.machines or {}


local function invnet_reset()
	invnet.enable = false
	invnet.machines = {}
end


local function invnet_list()
	for i,m in pairs(invnet.machines) do
		l:info("Machine #" .. i .. " at " .. minetest.pos_to_string(m.pos) .. ": ")
		for name,count in pairs(m.recipe or {}) do
			l:info("  recipe consumes " .. count .. " of " .. name)
		end
		for name,count in pairs(m.output or {}) do
			l:info("  recipe produces " .. count .. " of " .. name)
		end
	end
end




local function invnet_setup_item_move()
	-- Pick a machine
	local machines = invnet.machines
	local machine = nil

	for _ = 1,100 do
		local m_index = math.random(1, #machines)
		machine = machines[m_index]
		l:debug("Testing machine " .. m_index)
		if machine.recipe then -- skip chests
			break
		end
	end
	if not machine or not machine.recipe then return false end
	l:info("Selected machine at " .. minetest.pos_to_string(machine.pos))

	-- for each input item adjust total max_stack to minimum of them all, then
	-- try to move at once as many stacks as there are items in the recipe
	local sources = {}
	local max_stack = math.huge -- max transfer size
	local total_stacks = 0

	for item_name,_ in pairs(machine.recipe) do
		max_stack = math.min(max_stack, ItemStack(item_name):get_stack_max())
		--l:debug("  corrected stack max to " .. max_stack .. " for " .. item_name)
	end
	l:debug("  corrected stack max to " .. max_stack)

	for item_name,count in pairs(machine.recipe) do
		local from = ItemLocations:new()
		for _,src_machine in pairs(machines) do
			from:collect(src_machine.pos, src_machine.output_list_name, item_name)
		end

		from:sort_by_count_dsc() -- larger stacks first
		for i = 1,count do
			if i > #from.locations or from.locations[i].count < max_stack then
				l:debug("  insufficient stacks for item " .. item_name .. ", got only " .. (i-1))
				return false
			end
		end
		sources[item_name] = from
		total_stacks = total_stacks + count
	end

	-- Do we have enough free slots to deposit items to?
	local to = ItemLocations:new()
	to:collect(machine.pos, machine.input_list_name, "")
	if #to.locations < total_stacks then
		l:debug("  insufficient space to deposit " .. total_stacks .. " stacks")
		return false
	end

	-- If we made it to here, then we have enough items to craft the recipe => setup moving
	-- list of from_loc & to_loc & count
	local move_list = {}
	local dst_i = 1

	for item_name,from in pairs(sources) do
		local count = machine.recipe[item_name]

		for i = 1,count do
			local src_loc = from.locations[i]
			local dst_loc = to.locations[dst_i]

			dst_i = dst_i + 1
			l:debug("  will move " .. max_stack .. "*" .. item_name
			        .. " from " .. src_loc:to_string() .. " to " .. dst_loc:to_string())
			table.insert(move_list, { src = src_loc, dst = dst_loc, count = max_stack })
		end
	end

	-- schedule actual moves
	for i,item in pairs(move_list) do
		queue:prepend(function() return queued_move_stack(item.src, item.dst, item.count) end)
		if math.mod(i,5) == 4 then -- add a small delay
			queue:prepend(function() return 0.2 end)
		end
	end

	return true
end

function invnet_step()
	if not invnet.enable then return end
	--l:debug("inventory network step")

	local my_pos = core.localplayer:get_pos()
	for _,m in pairs(invnet.machines) do
		local distance = vector.distance(my_pos, m.pos)
		if distance > 8 then
			l:info("Too far (" .. distance .. ") from inventory " .. minetest.pos_to_string(m.pos) .. ", waiting")
			core.after(15, invnet_step) -- use after as it takes forever
			return
		end
	end

	if invnet_setup_item_move() then -- setup as many hops as possible
		--queue:append(invnet_step)
		core.after(2, invnet_step) -- use after as it takes forever
		queue:walk()
	else
		core.after(4, invnet_step) -- use after as it takes forever
	end
end


local function get_machine_in_list(pos)
	for i,m in pairs(invnet.machines) do
		if vector.distance(m.pos, pos) < 0.1 then
			return i
		end
	end
	return #invnet.machines+1
end


local wait_for_punch = false
core.register_on_punchnode(function(pos, node)
	if not wait_for_punch then return end

	local machine_index = get_machine_in_list(pos)

	if node.name == "biofuel:refinery" or node.name == "biofuel:refinery_active" then
		local m = {}
		m.recipe = {
			["default:stick"] = 4,
		}
		m.input_list_name = "src"
		m.output = {
			["biofuel:bottle_fuel"] = 1
		}
		m.output_list_name = "dst"
		m.pos = pos
		invnet.machines[machine_index] = m
		l:info("Added refinery")

	elseif node.name == "pipeworks:autocrafter" then
		local inv = core.get_inventory(pos)
		local item_string = inv["output"][1]:get_name()

		if item_string == "" then
			l:err("autocrafter unconfigured")
		else
			local output_gain = inv["output"][1]:get_count()
			local m = {}
			m.recipe = {}
			for _,stack in pairs(inv["recipe"]) do
				local name = stack:get_name()
				if name ~= "" then
					m.recipe[name] = (m.recipe[name] or 0) + 1
				end
			end
			m.input_list_name = "src"
			m.output = {
				[item_string] = output_gain
			}
			m.output_list_name = "dst"
			m.pos = pos
			invnet.machines[machine_index] = m
			l:info("Added autocrafter for: " .. output_gain .. " of " .. item_string)
		end
	else
		l:info("Unknown node punched: " .. node.name .. " adding it as source chest")
		local inv = core.get_inventory(pos)
		local m = {}
234
235
236
237
238
239
		if inv["src"] then
			m.input_list_name = "src"
		elseif inv["main"] then
			m.input_list_name = "main"
		end

240
241
242
243
244
245
		if inv["dst"] then
			m.output_list_name = "dst"
		elseif inv["main"] then
			m.output_list_name = "main"
		end

246
		if m.input_list_name and m.output_list_name then
247
248
			m.pos = pos
			invnet.machines[machine_index] = m
249
250
		else
			l:err("  no inventory lists? Skipped adding.")
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
		end

	end
	mod_storage:set_string("network", minetest.serialize(invnet))
end)


local function help()
	return ".invnet: inventory networks (automatic item moving between inventories), usage:\n"
	    .. ".invnet add -- toggles adding nodes to the network\n"
	    .. ".invnet list -- lists all inventories in the network\n"
	    .. ".invnet reset -- clears the entire network\n"
	    .. ".invnet start -- make other players jealous\n"
	    .. ".invnet stop  -- regain use of your chat\n"
end


core.register_chatcommand("invnet", {
	func = function(param)
		local args = string.split(param, ' ')

		if args[1] == "add" then
			if wait_for_punch then
				wait_for_punch = false
			else
				l:info("punch nodes to add them into inventory network. repeat this command to stop")
				wait_for_punch = true
			end
		elseif args[1] == "list" then
			invnet_list()
		elseif args[1] == "reset" then
			invnet_reset()
		elseif args[1] == "start" then
			invnet.enable = true
			invnet_step()
		elseif args[1] == "stop" then
			invnet.enable = false
		else
			l:info(help())
		end
	end,
})