View difference between Paste ID: pL3VDYNA and LSwUP7QB
SHOW: | | - or go back to the newest paste.
1
local helper = {}
2
--#region CONFIG----
3
local cfg = {}
4
cfg.localConfig = {
5
	--if true, the program will attempt to download and use the config from remoteConfigPath
6
	--useful if you have many turtles and you don't want to change the config of each one manually
7
	useRemoteConfig = false,
8
	remoteConfigPath = "http://localhost:33344/config.lua",
9
	--this command will be used when the program is started as "mine def" (mineLoop overrides this command)
10
	defaultCommand = "cube 3 3 8",
11
	--false: build walls/floor/ceiling everywhere, true: only where there is fluid
12
	plugFluidsOnly = true,
13
	--maximum taxicab distance from enterance point when collecting ores, 0 = disable ore traversal
14
	oreTraversalRadius = 0,
15
	--layer mining order, use "z" for branch mining, "y" for anything else
16
	--"y" - mine top to bottom layer by layer, "z" - mine forward in vertical slices
17
	layerSeparationAxis="y",
18
	--false: use regular chests, true: use entangled chests
19
	--if true, the turtle will place a single entangled chest to drop off items and break it afterwards.
20
	--tested with chests from https://www.curseforge.com/minecraft/mc-mods/kibe
21
	useEntangledChests = false,
22
	--false: refuel from inventory, true: refuel from (a different) entangled chest
23
	--if true, the turtle won't store any coal. Instead, when refueling, it will place the entangled chest, grab fuel from it, refuel, and then break the chest.
24
	useFuelEntangledChest = false,
25
	--true: use two chuck loaders to mine indefinitely without moving into unloaded chunks. 
26
	--This doesn't work with chunk loaders from https://www.curseforge.com/minecraft/mc-mods/kibe, but might work with some other mod.
27
	--After an area is mined, the turtle will shift by mineLoopOffset and execute mineLoopCommand
28
	--mineLoopCommand is used in place of defaultCommand when launching as "mine def"
29
	mineLoop = false,
30
	mineLoopOffset = {x=0, y=0, z=8},
31
	mineLoopCommand = "rcube 1 1 1"
32
}
33
34
cfg.getRemoteConfig = function(remotePath)
35
	local handle = http.get(remotePath)
36
	if not handle then
37
		helper.printError("Server not responding, using local")
38
		return nil
39
	end
40
	local data = handle.readAll()
41
	handle.close()
42
	local deser = textutils.unserialise(data)
43
	if not deser then
44
		helper.printError("Couldn't parse remote config, using local")
45
		return nil
46
	end
47
	for key,_ in pairs(cfg.localConfig) do
48
		if deser[key] == nil then
49
			helper.printError("No key", key, "in remote config, using local")
50
			return nil
51
		end
52
	end
53
	return deser
54
end
55
56
cfg.processConfig = function()
57
	local config = cfg.localConfig
58
	if cfg.localConfig.useRemoteConfig then
59
		helper.print("Downloading config..")
60
		local remoteConfig = cfg.getRemoteConfig(config.remoteConfigPath)
61
		config = remoteConfig or cfg.localConfig
62
	end
63
	return config
64
end
65
66
--#endregion
67
--#region CONSTANTS----
68
69
local SOUTH = 0
70
local WEST = 1
71
local NORTH = 2
72
local EAST = 3
73
74
local CHEST_SLOT = "chest_slot"
75
local BLOCK_SLOT = "block_slot"
76
local FUEL_SLOT = "fuel_slot"
77
local FUEL_CHEST_SLOT = "fuel_chest_slot"
78
local CHUNK_LOADER_SLOT = "chunk_loader_slot"
79
local MISC_SLOT = "misc_slot"
80
81
local ACTION = 0
82
local TASK = 1
83
84
local PATHFIND_INSIDE_AREA = 0
85
local PATHFIND_OUTSIDE_AREA = 1
86
local PATHFIND_INSIDE_NONPRESERVED_AREA = 2
87
local PATHFIND_ANYWHERE_NONPRESERVED = 3
88
89
local SUCCESS = 0
90
local FAILED_NONONE_COMPONENTCOUNT = 1
91
local FAILED_TURTLE_NOTINREGION = 2
92
local FAILED_REGION_EMPTY = 4
93
94
local REFUEL_THRESHOLD = 500
95
local RETRY_DELAY = 3
96
97
local FALLING_BLOCKS = {
98
	["minecraft:gravel"] = true,
99
	["minecraft:sand"] = true
100
}
101
102
--#endregion
103
--#region WRAPPER----
104
105
local wrapt = (function()
106
	local self = {
107
		selectedSlot = 1,
108
		direction = SOUTH,
109
		x = 0,
110
		y = 0,
111
		z = 0
112
	}
113
114
	local public = {}
115
116
	--wrap everything in turtle
117
	for key,value in pairs(turtle) do
118
		public[key] = value
119
	end
120
	--init turtle selected slot
121
	turtle.select(self.selectedSlot);
122
123
	public.select = function(slot)
124
		if self.selectedSlot ~= slot then
125
			turtle.select(slot)
126
			self.selectedSlot = slot
127
		end
128
	end
129
130
	public.forward = function()
131
		local success = turtle.forward()
132
		if not success then
133
			return success
134
		end
135
		if self.direction == EAST then
136
			self.x = self.x + 1
137
		elseif self.direction == WEST then
138
			self.x = self.x - 1
139
		elseif self.direction == SOUTH then
140
			self.z = self.z + 1
141
		elseif self.direction == NORTH then
142
			self.z = self.z - 1
143
		end
144
		return success
145
	end
146
147
	public.up = function()
148
		local success = turtle.up()
149
		if not success then
150
			return success
151
		end
152
		self.y = self.y + 1
153
		return success
154
	end
155
156
	public.down = function()
157
		local success = turtle.down()
158
		if not success then
159
			return success
160
		end
161
		self.y = self.y - 1
162
		return success
163
	end
164
165
	public.turnRight = function()
166
		local success = turtle.turnRight()
167
		if not success then
168
			return success
169
		end
170
		self.direction = self.direction + 1
171
		if self.direction > 3 then
172
			self.direction = 0
173
		end
174
		return success
175
	end
176
177
	public.turnLeft = function()
178
		local success = turtle.turnLeft()
179
		if not success then
180
			return success
181
		end
182
		self.direction = self.direction - 1
183
		if self.direction < 0 then
184
			self.direction = 3
185
		end
186
		return success
187
	end
188
189
	public.getX = function() return self.x end
190
	public.getY = function() return self.y end
191
	public.getZ = function() return self.z end
192
	public.getDirection = function() return self.direction end
193
	public.getPosition = function() return {x = self.x, y = self.y, z = self.z, direction = self.direction} end
194
195
	return public
196
end)()
197
198
--#endregion
199
--#region DEBUG FUNCTIONS----
200
201
local debug = {
202
	dumpPoints = function(points, filename)
203
		local file = fs.open(filename .. ".txt","w")
204
		for key, value in pairs(points) do
205
			local line =
206
				tostring(value.x)..","..
207
				tostring(value.y)..","..
208
				tostring(value.z)..","
209
			if value.adjacent then
210
				line = line .. "0,128,0"
211
			elseif value.inacc then
212
				line = line .. "255,0,0"
213
			elseif value.triple then
214
				line = line .. "0,255,0"
215
			elseif value.checkedInFirstPass then
216
				line = line .. "0,0,0"
217
			else
218
				helper.printError("Invalid block type when dumping points")
219
			end
220
221
			if math.abs(value.z) < 100 then
222
				file.writeLine(line)
223
			end
224
		end
225
		file.close()
226
	end,
227
228
	dumpPath = function(points, filename)
229
		local file = fs.open(filename .. ".txt","w")
230
		for key, value in pairs(points) do
231
			if tonumber(key) then
232
				local line =
233
					tostring(value.x)..","..
234
					tostring(value.y)..","..
235
					tostring(value.z)
236
				file.writeLine(line)
237
			end
238
		end
239
		file.close()
240
	end,
241
242
	dumpLayers = function(layers, filename)
243
		for index, layer in ipairs(layers) do
244
			local file = fs.open(filename .. tostring(index) .. ".txt","w")
245
			for _, value in ipairs(layer) do
246
				local line =
247
					tostring(value.x)..","..
248
					tostring(value.y)..","..
249
					tostring(value.z)..","
250
				if value.adjacent then
251
					line = line .. "0,128,0"
252
				elseif value.inacc then
253
					line = line .. "255,0,0"
254
				elseif value.triple then
255
					line = line .. "0,255,0"
256
				elseif value.checkedInFirstPass then
257
					line = line .. "0,0,0"
258
				else
259
					helper.printError("Invalid block type when dumping layers")
260
				end
261
				file.writeLine(line)
262
			end
263
			file.close()
264
		end
265
	end
266
}
267
268
--#endregion
269
--#region HELPER FUNCTIONS----
270
271
helper.deltaToDirection = function(dX, dZ)
272
	if dX > 0 then
273
		return EAST
274
	elseif dX < 0 then
275
		return WEST
276
	elseif dZ > 0 then
277
		return SOUTH
278
	elseif dZ < 0 then
279
		return NORTH
280
	end
281
	error("Invalid delta", 2)
282
end
283
284
helper.tableLength = function(T)
285
	local count = 0
286
	for _ in pairs(T) do count = count + 1 end
287
	return count
288
end
289
290
helper.getIndex = function(x, y, z)
291
	return tostring(x) .. "," .. tostring(y) .. "," .. tostring(z)
292
end
293
294
helper.isPosEqual = function(pos1, pos2)
295
	return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z
296
end
297
298
helper.getSurroundings = function(pos)
299
	return {
300
		[helper.getIndex(pos.x + 1, pos.y, pos.z)] = {x = pos.x + 1, y = pos.y, z = pos.z},
301
		[helper.getIndex(pos.x - 1, pos.y, pos.z)] = {x = pos.x - 1, y = pos.y, z = pos.z},
302
		[helper.getIndex(pos.x, pos.y + 1, pos.z)] = {x = pos.x, y = pos.y + 1, z = pos.z},
303
		[helper.getIndex(pos.x, pos.y - 1, pos.z)] = {x = pos.x, y = pos.y - 1, z = pos.z},
304
		[helper.getIndex(pos.x, pos.y, pos.z + 1)] = {x = pos.x, y = pos.y, z = pos.z + 1},
305
		[helper.getIndex(pos.x, pos.y, pos.z - 1)] = {x = pos.x, y = pos.y, z = pos.z - 1}
306
	}
307
end
308
309
helper.getForwardPos = function(currentPos)
310
	local newPos = {x = currentPos.x, y = currentPos.y, z = currentPos.z}
311
	if currentPos.direction == EAST then
312
		newPos.x = newPos.x + 1
313
	elseif currentPos.direction == WEST then
314
		newPos.x = newPos.x - 1
315
	elseif currentPos.direction == SOUTH then
316
		newPos.z = newPos.z + 1
317
	elseif currentPos.direction == NORTH then
318
		newPos.z = newPos.z - 1
319
	end
320
	return newPos
321
end
322
323
helper.distance = function(from, to)
324
	return math.abs(from.x - to.x) + math.abs(from.y - to.y) + math.abs(from.z - to.z)
325
end
326
327
helper.sign = function(number)
328
    return number > 0 and 1 or (number == 0 and 0 or -1)
329
end
330
331
helper.inRange = function(number, a, b)
332
	return number >= a and number <= b
333
end
334
335
helper.splitString = function(string, separator)
336
	if not separator then separator = "%s" end
337
	local split = {}
338
	for str in string.gmatch(string, "([^"..separator.."]+)") do
339
		table.insert(split, str)
340
	end
341
	return split
342
end
343
344
helper.stringEndsWith = function (str, ending)
345
	return ending == "" or str:sub(-#ending) == ending
346
end
347
348
helper.tableContains = function(table, element, comparison)
349
	for _, value in pairs(table) do
350
		if comparison(value, element) then
351
			return true
352
		end
353
	end
354
	return false
355
end
356
357
helper.readOnlyTable = function(t)
358
	local mt = {
359
		__index = t,
360
		__newindex = function(t,k,v)
361
			error("Cannot write into a read-only table", 2)
362
		end
363
	}
364
	local proxy = {}
365
	setmetatable(proxy, mt)
366
	return proxy
367
end
368
369
helper.osYield = function()
370
---@diagnostic disable-next-line: undefined-field
371
	os.queueEvent("fakeEvent");
372
---@diagnostic disable-next-line: undefined-field
373
	os.pullEvent();
374
end
375
376
helper.printError = function(...)
377
	term.setTextColor(colors.red)
378
	print(...)
379
end
380
381
helper.printWarning = function(...)
382
	term.setTextColor(colors.yellow)
383
	print(...)
384
end
385
386
helper.print = function(...)
387
	term.setTextColor(colors.white)
388
	print(...)
389
end
390
391
helper.write = function(...)
392
	term.setTextColor(colors.white)
393
	term.write(...)
394
end
395
396
helper.read = function()
397
	term.setTextColor(colors.lightGray)
398
	local data = read()
399
	term.setTextColor(colors.white)
400
	return data
401
end
402
403
--#endregion
404
--#region SHAPE LIBRARY----
405
406
local shapes = {}
407
408
shapes.custom = {
409
	command = "custom",
410
	shortDesc = "custom <filename>",
411
	longDesc = "Executes a function named \"generate\" from the specified file and uses the data it returns as a shape",
412
	args = {"str"},
413
	generate = function(filename)
414
		local env = {
415
			table=table,
416
			fs=fs,
417
			http=http,
418
			io=io,
419
			math=math,
420
			os=os,
421
			parallel=parallel,
422
			string=string,
423
			vector=vector,
424
			textutils=textutils
425
		}
426
		local chunk, err = loadfile(filename, "bt", env)
427
		if err then
428
			helper.printError("Couldn't load file:", err);
429
			return {}
430
		end
431
		chunk();
432
		if type(env.generate) ~= "function" then
433
			helper.printError("File does not contain generate function")
434
			return {}
435
		end
436
		local generated = env.generate()
437
		if type(generated) ~= "table" then
438
			helper.printError("Generate function didn't return a table")
439
			return {}
440
		end
441
		local blocks = {}
442
		for _, value in ipairs(generated) do
443
			if type(value.x) ~= "number" or type(value.y) ~= "number" or type(value.z) ~= "number" then
444
				helper.printError("Invalid coordinates entry:", textutils.serialize(value))
445
				return {}
446
			end
447
			blocks[helper.getIndex(value.x, value.y, value.z)] = {x = value.x, y = value.y, z = value.z}
448
		end
449
		return blocks
450
	end
451
}
452
453
shapes.sphere = {
454
	command = "sphere",
455
	shortDesc = "sphere <diameter>",
456
	longDesc = "Mine a sphere of diameter <diameter>, starting from it's bottom center",
457
	args = {"2.."},
458
	generate = function(diameter)
459
		local radius = math.ceil(diameter / 2.0)
460
		local radiusSq = (diameter / 2.0) * (diameter / 2.0)
461
		local blocks = {}
462
		local first = nil
463
		for j=-radius,radius do
464
			for i=-radius,radius do
465
				for k=-radius,radius do
466
					if diameter % 2 == 0 then
467
						if math.pow(i+0.5, 2) + math.pow(j+0.5, 2) + math.pow(k+0.5, 2) < radiusSq then
468
							if not first then
469
								first = j
470
							end
471
							blocks[helper.getIndex(i,j-first,k)] = {x = i, y = j-first, z = k}
472
						end
473
					else
474
						if math.pow(i, 2) + math.pow(j, 2) + math.pow(k, 2) < radiusSq then
475
							if not first then
476
								first = j
477
							end
478
							blocks[helper.getIndex(i,j-first,k)] = {x = i, y = j-first, z = k}
479
						end
480
					end
481
				end
482
			end
483
			helper.osYield()
484
		end
485
		return blocks
486
	end
487
}
488
489
shapes.cuboid = {
490
	command="cube",
491
	shortDesc = "cube <left> <up> <forward>",
492
	longDesc = "Mine a cuboid of a specified size. Use negative values to dig in an opposite direction",
493
	args= {"..-2 2..", "..-2 2..", "..-2 2.."},
494
	generate = function(x, y, z)
495
		local blocks = {}
496
		local sX = helper.sign(x)
497
		local sY = helper.sign(y)
498
		local sZ = helper.sign(z)
499
		local tX = sX * (math.abs(x) - 1)
500
		local tY = sY * (math.abs(y) - 1)
501
		local tZ = sZ * (math.abs(z) - 1)
502
		for i=0,tX,sX do
503
			for j=0,tY,sY do
504
				for k=0,tZ,sZ do
505
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
506
				end
507
			end
508
			helper.osYield()
509
		end
510
		return blocks
511
	end
512
}
513
514
shapes.centeredCuboid = {
515
	command="rcube",
516
	shortDesc = "rcube <leftR> <upR> <forwardR>",
517
	longDesc = "Mine a cuboid centered on the turtle. Each dimension is a \"radius\", so typing \"rcube 1 1 1\" will yield a 3x3x3 cube",
518
	args={"1..", "1..", "1.."},
519
	generate = function(rX, rY, rZ)
520
		local blocks = {}
521
		for i=-rX,rX do
522
			for j=-rY,rY do
523
				for k=-rZ,rZ do
524
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
525
				end
526
			end
527
			helper.osYield()
528
		end
529
		return blocks
530
	end
531
}
532
533
shapes.branch = {
534
	command="branch",
535
	shortDesc = "branch <branchLen> <shaftLen>",
536
	longDesc = "Branch-mining. <branchLen> is the length of each branch, <shaftLen> is the length of the main shaft",
537
	args={"0..", "3.."},
538
	generate = function(xRadius, zDepth)
539
		local blocks = {}
540
		--generate corridor
541
		for x=-1,1 do
542
			for y=0,2 do
543
				for z=0,zDepth-1 do
544
					blocks[helper.getIndex(x,y,z)] = {x = x, y = y, z = z}
545
				end
546
			end
547
		end
548
		--generate branches
549
		for z=2,zDepth-1,2 do
550
			local y = (z % 4 == 2) and 0 or 2
551
			for x=0,xRadius-1 do
552
				blocks[helper.getIndex(x+2,y,z)] = {x = x+2, y = y, z = z}
553
				blocks[helper.getIndex(-x-2,y,z)] = {x = -x-2, y = y, z = z}
554
			end
555
		end
556
		return blocks
557
	end
558
}
559
560
--#endregion
561
--#region REGION PROCESSING----
562
563
local region = {}
564
region.createShape = function()
565
	local blocks = {}
566
	for i=-7,7 do
567
		for j=0,7 do
568
			for k=-7,7 do
569
				if i*i + j*j + k*k < 2.5*2.5 then
570
					blocks[helper.getIndex(i,j,k)] = {x = i, y = j, z = k}
571
				end
572
			end
573
		end
574
	end
575
	return blocks
576
end
577
578
region.cloneBlocks = function(allBlocks)
579
	local cloned = {}
580
	for key,value in pairs(allBlocks) do
581
		local blockClone = {
582
			x = value.x,
583
			y = value.y,
584
			z = value.z
585
		}
586
		cloned[key] = blockClone
587
	end
588
	return cloned
589
end
590
591
--mark blocks that are next to walls
592
region.markAdjacentInPlace = function(allBlocks)
593
	for key,value in pairs(allBlocks) do
594
		local xMinus = allBlocks[helper.getIndex(value.x - 1, value.y, value.z)]
595
		local xPlus = allBlocks[helper.getIndex(value.x + 1, value.y, value.z)]
596
		local yMinus = allBlocks[helper.getIndex(value.x, value.y - 1, value.z)]
597
		local yPlus = allBlocks[helper.getIndex(value.x, value.y + 1, value.z)]
598
		local zMinus = allBlocks[helper.getIndex(value.x, value.y, value.z - 1)]
599
		local zPlus = allBlocks[helper.getIndex(value.x, value.y, value.z + 1)]
600
		if not xMinus or not xPlus or not yMinus or not yPlus or not zMinus or not zPlus then
601
			value.adjacent = true
602
			if yMinus then yMinus.checkedInFirstPass = true end
603
			if yPlus then yPlus.checkedInFirstPass = true end
604
		end
605
	end
606
end
607
608
--mark positions where the turtle can check both the block above and the block below
609
region.markTripleInPlace = function(allBlocks)
610
	local minY = 9999999;
611
	for key,value in pairs(allBlocks) do
612
		if not value.checkedInFirstPass and not value.adjacent then
613
			minY = math.min(minY, value.y)
614
		end
615
	end
616
	for key,value in pairs(allBlocks) do
617
		if not value.checkedInFirstPass and not value.adjacent then
618
			local offset = (value.y - minY) % 3;
619
			if offset == 0 then
620
				local blockAbove = allBlocks[helper.getIndex(value.x, value.y+1, value.z)]
621
				if blockAbove ~= nil and blockAbove.checkedInFirstPass ~= true then
622
					value.checkedInFirstPass = true
623
				else
624
					value.inacc = true
625
				end
626
			elseif offset == 1 then
627
				value.triple = true
628
			elseif offset == 2 and allBlocks[helper.getIndex(value.x, value.y-1, value.z)] ~= nil then
629
				local blockBelow = allBlocks[helper.getIndex(value.x, value.y-1, value.z)]
630
				if blockBelow ~= nil and blockBelow.checkedInFirstPass ~= true then
631
					value.checkedInFirstPass = true
632
				else
633
					value.inacc = true
634
				end
635
			end
636
		end
637
	end
638
end
639
640
region.findConnectedComponents = function(allBlocks)
641
	local visited = {}
642
	local components = {}
643
	local counter = 0
644
	local lastTime = os.clock()
645
	for key,value in pairs(allBlocks) do
646
		if not visited[key] then
647
			local component = {}
648
			local toVisit = {[key] = value}
649
			while true do
650
				local newToVisit = {}
651
				local didSomething = false
652
				for currentKey,current in pairs(toVisit) do
653
					didSomething = true
654
					visited[currentKey] = true
655
					component[currentKey] = current
656
					local minusX = helper.getIndex(current.x-1, current.y, current.z)
657
					local plusX = helper.getIndex(current.x+1, current.y, current.z)
658
					local minusY = helper.getIndex(current.x, current.y-1, current.z)
659
					local plusY = helper.getIndex(current.x, current.y+1, current.z)
660
					local minusZ = helper.getIndex(current.x, current.y, current.z-1)
661
					local plusZ = helper.getIndex(current.x, current.y, current.z+1)
662
					if allBlocks[minusX] and not visited[minusX] then newToVisit[minusX] = allBlocks[minusX] end
663
					if allBlocks[plusX] and not visited[plusX] then newToVisit[plusX] = allBlocks[plusX] end
664
					if allBlocks[minusY] and not visited[minusY] then newToVisit[minusY] = allBlocks[minusY] end
665
					if allBlocks[plusY] and not visited[plusY] then newToVisit[plusY] = allBlocks[plusY] end
666
					if allBlocks[minusZ] and not visited[minusZ] then newToVisit[minusZ] = allBlocks[minusZ] end
667
					if allBlocks[plusZ] and not visited[plusZ] then newToVisit[plusZ] = allBlocks[plusZ] end
668
669
					counter = counter + 1
670
					if counter % 50 == 0 then
671
						local curTime = os.clock()
672
						if curTime - lastTime > 1 then
673
							lastTime = curTime
674
							helper.osYield()
675
						end
676
					end
677
				end
678
				toVisit = newToVisit
679
				if not didSomething then break end
680
			end
681
			table.insert(components, component)
682
		end
683
	end
684
	return components
685
end
686
687
region.separateLayers = function(allBlocks, direction)
688
	if direction ~= "y" and direction ~= "z" then
689
		error("Invalid direction value", 2)
690
	end
691
	local layers = {}
692
	local min = 999999
693
	local max = -999999
694
	for key,value in pairs(allBlocks) do
695
		if not (not value.adjacent and value.checkedInFirstPass) then
696
			local index = direction == "y" and value.y or value.z
697
			if not layers[index] then 
698
				layers[index] = {} 
699
			end
700
			layers[index][key] = value
701
			min = math.min(min, index)
702
			max = math.max(max, index)
703
		end
704
	end
705
	if min == 999999 then
706
		error("There should be at least one block in passed table", 2)
707
	end
708
	
709
	local reassLayers = {}
710
	for key, value in pairs(layers) do
711
		local index = direction == "y" and (max - min+1) - (key - min) or (key - min + 1)
712
		reassLayers[index] = value
713
	end
714
	return reassLayers
715
end
716
717
region.sortFunction = function(a, b) 
718
	return (a.y ~= b.y and a.y < b.y or (a.x ~= b.x and a.x > b.x or a.z > b.z))
719
end
720
721
region.findClosestPoint = function(location, points, usedPoints)
722
	local surroundings = helper.getSurroundings(location)
723
	local existingSurroundings = {}
724
	local foundClose = false
725
	for key,value in pairs(surroundings) do
726
		if points[key] and not usedPoints[key] then
727
			table.insert(existingSurroundings, value)
728
			foundClose = true
729
		end
730
	end
731
	if foundClose then
732
		table.sort(existingSurroundings, region.sortFunction)
733
		local closest = table.remove(existingSurroundings)
734
		return points[helper.getIndex(closest.x, closest.y, closest.z)]
735
	end
736
737
	local minDist = 999999
738
	local minValue = nil
739
	for key,value in pairs(points) do
740
		if not usedPoints[key] then
741
			local dist = helper.distance(value,location)
742
			if dist < minDist then
743
				minDist = dist
744
				minValue = value
745
			end
746
		end
747
	end
748
	if not minValue then
749
		return nil
750
	end
751
	return minValue
752
end
753
754
--travelling salesman, nearest neighbour method
755
region.findOptimalBlockOrder = function(layers)
756
	local newLayers = {}
757
	local lastTime = os.clock()
758
	for index, layer in ipairs(layers) do
759
		local newLayer = {}
760
		local usedPoints = {}
761
		local current = region.findClosestPoint({x=0,y=0,z=0}, layer, usedPoints)
762
		repeat
763
			usedPoints[helper.getIndex(current.x, current.y, current.z)] = true
764
			table.insert(newLayer, current)
765
			current = region.findClosestPoint(current, layer, usedPoints)
766
			local curTime = os.clock()
767
			if curTime - lastTime > 1 then
768
				lastTime = curTime
769
				helper.osYield()
770
			end
771
		until not current
772
		newLayers[index]=newLayer
773
	end
774
	return newLayers
775
end
776
777
region.createLayersFromArea = function(diggingArea, direction)
778
	local blocksToProcess = region.cloneBlocks(diggingArea)
779
	region.markAdjacentInPlace(blocksToProcess)
780
	region.markTripleInPlace(blocksToProcess)
781
	local layers = region.separateLayers(blocksToProcess, direction)
782
	local orderedLayers = region.findOptimalBlockOrder(layers)
783
	return orderedLayers
784
end
785
786
region.shiftRegion = function(shape, delta)
787
	local newShape = {}
788
	for key,value in pairs(shape) do
789
		local newPos = {x = value.x + delta.x, y = value.y + delta.y, z = value.z + delta.z}
790
		newShape[helper.getIndex(newPos.x, newPos.y, newPos.z)] = newPos
791
	end
792
	return newShape
793
end
794
795
region.reserveChests = function(blocks)
796
	local blocksCopy = {}
797
	local counter = 0
798
	for _,value in pairs(blocks) do
799
		counter = counter + 1
800
		blocksCopy[counter] = value
801
	end
802
	table.sort(blocksCopy, function(a,b)
803
		if a.y ~= b.y then return a.y > b.y
804
		elseif a.z~=b.z then return a.z > b.z
805
		else return a.x > b.x end
806
	end)
807
808
	return {reserved=blocksCopy}
809
end
810
811
region.validateRegion = function(blocks)
812
	local result = SUCCESS
813
	--there must be only one connected component
814
	local components = region.findConnectedComponents(blocks)
815
	if helper.tableLength(components) == 0 then
816
		result = result + FAILED_REGION_EMPTY
817
	end
818
	if helper.tableLength(components) > 1 then
819
		result = result + FAILED_NONONE_COMPONENTCOUNT
820
	end
821
	--the turtle must be inside of the region
822
	if not blocks[helper.getIndex(0,0,0)] then
823
		result = result + FAILED_TURTLE_NOTINREGION
824
	end
825
	return result
826
end
827
828
--#endregion
829
--#region PATHFINDING----
830
831
local path = {}
832
path.pathfindUpdateNeighbour = function(data, neighbour, destination, origin, neighbourIndex)
833
	local originIndex = helper.getIndex(origin.x, origin.y, origin.z)
834
	if not data[neighbourIndex] then
835
		data[neighbourIndex] = {}
836
		data[neighbourIndex].startDist = data[originIndex].startDist + 1
837
		data[neighbourIndex].heuristicDist = helper.distance(neighbour, destination)
838
		data[neighbourIndex].previous = origin
839
	elseif data[originIndex].startDist + 1 < data[neighbourIndex].startDist then
840
		data[neighbourIndex].startDist = data[originIndex].startDist + 1
841
		data[neighbourIndex].previous = origin
842
	end
843
end
844
845
path.traversable = function(area, index, pathfindingType)
846
	if pathfindingType == PATHFIND_INSIDE_AREA then
847
		return area[index] ~= nil
848
	elseif pathfindingType == PATHFIND_INSIDE_NONPRESERVED_AREA then
849
		return not not (area[index] and not area[index].preserve)
850
	elseif pathfindingType == PATHFIND_OUTSIDE_AREA then
851
		return area[index] == nil
852
	elseif PATHFIND_ANYWHERE_NONPRESERVED then
853
		return area[index] == nil or not area[index].preserve
854
	end
855
	error("Unknown pathfinding type", 3)
856
end
857
858
path.pathfind = function(from, to, allBlocks, pathfindingType)
859
	if helper.isPosEqual(from,to) then
860
		return {length = 0}
861
	end
862
	local data = {}
863
	local openSet = {}
864
	local closedSet = {}
865
866
	local current = from
867
	local curIndex = helper.getIndex(from.x, from.y, from.z)
868
	openSet[curIndex] = current
869
870
	data[curIndex] = {}
871
	data[curIndex].startDist = 0
872
	data[curIndex].heuristicDist = helper.distance(current, to)
873
874
	while true do
875
		local surroundings = helper.getSurroundings(current)
876
		for key,value in pairs(surroundings) do
877
			if path.traversable(allBlocks,key,pathfindingType) and not closedSet[key] then 
878
				path.pathfindUpdateNeighbour(data, value, to, current, key)
879
				openSet[key] = value
880
			end
881
		end
882
883
		closedSet[curIndex] = current
884
		openSet[curIndex] = nil
885
886
		local minN = 9999999
887
		local minValue = nil
888
		for key,value in pairs(openSet) do
889
			local sum = data[key].startDist + data[key].heuristicDist
890
			if sum < minN then
891
				minN = sum
892
				minValue = value
893
			end
894
		end
895
		current = minValue;
896
897
		if current == nil then
898
			helper.printWarning("No path from", from.x, from.y, from.z, "to", to.x, to.y, to.z)
899
			return false
900
		end
901
902
		curIndex = helper.getIndex(current.x, current.y, current.z)
903
		if helper.isPosEqual(current,to) then
904
			break
905
		end
906
	end
907
908
	local path = {}
909
	local counter = 1
910
	while current ~= nil do
911
		path[counter] = current
912
		counter = counter + 1
913
		current = data[helper.getIndex(current.x, current.y, current.z)].previous
914
	end
915
916
	local reversedPath = {}
917
	local newCounter = 1
918
	for i=counter-1,1,-1 do
919
		reversedPath[newCounter] = path[i]
920
		newCounter = newCounter + 1
921
	end
922
	reversedPath.length = newCounter-1;
923
	return reversedPath
924
end
925
926
--#endregion
927
--#region SLOT MANAGER----
928
local slots = (function()
929
	local slotAssignments = {}
930
	local assigned = false
931
	local public = {}
932
933
	local slotDesc = nil
934
935
	local generateDescription = function(config)
936
		local desc = {
937
			[CHEST_SLOT] = "Chests",
938
			[BLOCK_SLOT] = "Cobblestone",
939
			[FUEL_SLOT] = "Fuel (only coal supported)",
940
			[FUEL_CHEST_SLOT] = "Fuel Entangled Chest",
941
			[CHUNK_LOADER_SLOT] = "2 Chunk Loaders",
942
		}
943
		if config.useEntangledChests then
944
			desc[CHEST_SLOT] = "Entangled Chest"
945
		end
946
		return desc
947
	end
948
949
	public.assignSlots = function(config)
950
		if assigned then
951
			error("Slots have already been assigned", 2)
952
		end
953
		assigned = true
954
955
		local currentSlot = 1
956
		slotAssignments[CHEST_SLOT] = currentSlot
957
		currentSlot = currentSlot + 1
958
		if config.useFuelEntangledChest then
959
			slotAssignments[FUEL_CHEST_SLOT] = currentSlot
960
			currentSlot = currentSlot + 1
961
		else
962
			slotAssignments[FUEL_SLOT] = currentSlot
963
			currentSlot = currentSlot + 1
964
		end
965
		if config.mineLoop then
966
			slotAssignments[CHUNK_LOADER_SLOT] = currentSlot
967
			currentSlot = currentSlot + 1
968
		end
969
		slotAssignments[BLOCK_SLOT] = currentSlot
970
		currentSlot = currentSlot + 1
971
		slotAssignments[MISC_SLOT] = currentSlot
972
		currentSlot = currentSlot + 1
973
974
		slotDesc = generateDescription(config)
975
	end
976
977
	public.get = function(slotId)
978
		if slotAssignments[slotId] then
979
			return slotAssignments[slotId]
980
		else
981
			error("Slot " .. tostring(slotId) .. " was not assigned", 2)
982
		end
983
	end
984
985
	public.printSlotInfo = function()
986
		local inverse = {}
987
		for key,value in pairs(slotAssignments) do
988
			inverse[value] = key
989
		end
990
		for key,value in ipairs(inverse) do
991
			if value ~= MISC_SLOT then
992
				helper.print("\tSlot", key, "-", slotDesc[value])
993
			end
994
		end
995
	end
996
997
	return public
998
end)()
999
1000
--#endregion
1001
--#region UI----
1002
1003
local ui = {}
1004
1005
ui.printInfo = function()
1006
	helper.print("Current fuel level is", wrapt.getFuelLevel())
1007
	helper.print("Item slots, can change based on config:")
1008
	slots.printSlotInfo()
1009
end
1010
1011
ui.printProgramsInfo = function()
1012
	helper.print("Select a program:")
1013
	helper.print("\thelp <program>")
1014
	for _,value in pairs(shapes) do
1015
		helper.print("\t"..value.shortDesc)
1016
	end
1017
end
1018
1019
ui.tryShowHelp = function(input)
1020
	if not input then
1021
		return false
1022
	end
1023
	local split = helper.splitString(input)
1024
	if split[1] ~= "help" then
1025
		return false
1026
	end
1027
1028
	if #split == 1 then
1029
		ui.printProgramsInfo()
1030
		return true
1031
	end
1032
1033
	if #split ~= 2 then
1034
		return false
1035
	end
1036
1037
	local program = split[2]
1038
	local shape = nil
1039
	for key, value in pairs(shapes) do
1040
		if value.command == program then
1041
			shape = value
1042
		end
1043
	end
1044
1045
	if not shape then
1046
		helper.printError("Unknown program")
1047
		return true
1048
	end
1049
	helper.print("Usage:", shape.shortDesc)
1050
	helper.print("\t"..shape.longDesc)
1051
	return true
1052
end
1053
1054
ui.testRange = function(range, value)
1055
	if range == "str" then
1056
		return "string"
1057
	end
1058
	if type(value) ~= "number" then
1059
		return false
1060
	end
1061
1062
	local subRanges = helper.splitString(range, " ")
1063
	for _, range in ipairs(subRanges) do
1064
		local borders = helper.splitString(range, "..")
1065
		local tableLength = helper.tableLength(borders)
1066
		if tableLength == 2 then
1067
			local left = tonumber(borders[1])
1068
			local right = tonumber(borders[2])
1069
			if helper.inRange(value, left, right) then
1070
				return true
1071
			end
1072
		elseif tableLength == 1 then
1073
			local isLeft = string.sub(range, 0, 1) ~= "."
1074
			local border = tonumber(borders[1])
1075
			local good = isLeft and (value >= border) or not isLeft and (value <= border)
1076
			if good then
1077
				return true
1078
			end
1079
		end
1080
	end
1081
	return false
1082
end
1083
1084
ui.parseArgs = function(argPattern, args)
1085
	if helper.tableLength(argPattern) ~= helper.tableLength(args) then
1086
		return nil
1087
	end
1088
	
1089
	local parsed = {}
1090
	for _,value in ipairs(args) do
1091
		local number = tonumber(value)
1092
		if not number then
1093
			table.insert(parsed, value)
1094
		end
1095
		table.insert(parsed, number)
1096
	end
1097
1098
	for index,value in ipairs(argPattern) do
1099
		local result = ui.testRange(value, parsed[index])
1100
		if result == "string" then
1101
			parsed[index] = args[index]
1102
		elseif not result then
1103
			return nil
1104
		end
1105
	end
1106
	return parsed
1107
end
1108
1109
ui.parseProgram = function(string)
1110
	if not string then
1111
		return nil
1112
	end
1113
1114
	local split = helper.splitString(string)
1115
	if not split or helper.tableLength(split) == 0 then
1116
		return nil
1117
	end
1118
	
1119
	local program = split[1]
1120
	local shape = nil
1121
	for _, value in pairs(shapes) do
1122
		if value.command == program then
1123
			shape = value
1124
		end
1125
	end
1126
	if not shape then
1127
		return nil
1128
	end
1129
1130
	local args = {table.unpack(split, 2, #split)}
1131
	local parsed = ui.parseArgs(shape.args, args)
1132
	if not parsed then
1133
		return nil
1134
	end
1135
1136
	return {shape = shape, args = parsed}
1137
end
1138
1139
ui.promptForShape = function()
1140
	local shape
1141
	while true do
1142
		helper.write("> ")
1143
		local input = helper.read()
1144
		shape = ui.parseProgram(input)
1145
		if not shape then
1146
			if not ui.tryShowHelp(input) then
1147
				helper.printError("Invalid program")
1148
			end
1149
		else
1150
			break
1151
		end
1152
	end
1153
	return shape
1154
end
1155
1156
ui.showValidationError = function(validationResult) 
1157
	local error = "Invalid mining volume:";
1158
	if bit32.band(validationResult, FAILED_REGION_EMPTY) ~= 0 then
1159
		helper.printError("Invalid mining volume: \n\tVolume is empty")
1160
		return
1161
	end
1162
	if bit32.band(validationResult, FAILED_NONONE_COMPONENTCOUNT) ~= 0 then
1163
		error = error .. "\n\tVolume has multiple disconnected parts"
1164
	end
1165
	if bit32.band(validationResult, FAILED_TURTLE_NOTINREGION) ~= 0 then
1166
		error = error .. "\n\tTurtle (pos(0,0,0)) not in volume"
1167
	end
1168
	helper.printError(error)
1169
end
1170
1171
--#endregion
1172
1173
----THE REST OF THE CODE----
1174
1175
local function execWithSlot(func, slot)
1176
	wrapt.select(slots.get(slot))
1177
	local data = func()
1178
	wrapt.select(slots.get(MISC_SLOT))
1179
	return data
1180
end
1181
1182
local function isWaterSource(inspectFunction)
1183
	local success, data = inspectFunction()
1184
	if success and data.name == "minecraft:water" and data.state.level == 0 then
1185
		return true
1186
	end
1187
	return false
1188
end
1189
1190
local function isFluidSource(inspectFunction)
1191
	local success, data = inspectFunction()
1192
	if success and (data.name == "minecraft:lava" or data.name == "minecraft:water") and data.state.level == 0 then
1193
		return true
1194
	end
1195
	return false
1196
end
1197
1198
local function isFluid(inspectFunction)
1199
	local success, data = inspectFunction()
1200
	if success and (data.name == "minecraft:lava" or data.name == "minecraft:water") then
1201
		return true
1202
	end
1203
	return false
1204
end
1205
1206
local function isSand(inspectFunction)
1207
	local success, data = inspectFunction()
1208
	if success and FALLING_BLOCKS[data.name] then
1209
		return true
1210
	end
1211
	return false
1212
end
1213
1214
local function isOre(inspectFunction)
1215
	local success, data = inspectFunction()
1216
	if success and helper.stringEndsWith(data.name, "_ore") then
1217
		return true
1218
	end
1219
	return false
1220
end
1221
1222
1223
local function dropInventory(dropFunction)
1224
	local dropped = true
1225
	for i=slots.get(MISC_SLOT),16 do
1226
		if wrapt.getItemCount(i) > 0 then
1227
			wrapt.select(i)
1228
			dropped = dropFunction() and dropped
1229
		end
1230
	end
1231
	wrapt.select(slots.get(MISC_SLOT))
1232
	return dropped
1233
end
1234
1235
local function forceMoveForward()
1236
	if isWaterSource(wrapt.inspect) then
1237
		execWithSlot(wrapt.place, BLOCK_SLOT)
1238
	end
1239
	repeat
1240
		wrapt.dig()
1241
	until wrapt.forward()
1242
end
1243
1244
local function plugTop(onlyFluids)
1245
	if isSand(wrapt.inspectUp) then
1246
		wrapt.digUp()
1247
	end
1248
	if not wrapt.detectUp() and not onlyFluids or onlyFluids and isFluid(wrapt.inspectUp) then
1249
		if wrapt.getItemCount(slots.get(BLOCK_SLOT)) > 0 then
1250
			local tries = 0
1251
			repeat tries = tries + 1 until execWithSlot(wrapt.placeUp, BLOCK_SLOT) or tries > 10
1252
		end
1253
	end
1254
end
1255
1256
local function plugBottom(onlyFluids)
1257
	if not onlyFluids or isFluidSource(wrapt.inspectDown) then
1258
		execWithSlot(wrapt.placeDown, BLOCK_SLOT)
1259
	end
1260
end
1261
1262
local function digAbove()
1263
	if isFluidSource(wrapt.inspectUp) then
1264
		execWithSlot(wrapt.placeUp, BLOCK_SLOT)
1265
	end
1266
	wrapt.digUp()
1267
end
1268
1269
local function digBelow()
1270
	if isFluidSource(wrapt.inspectDown) then
1271
		execWithSlot(wrapt.placeDown, BLOCK_SLOT)
1272
	end
1273
	wrapt.digDown()
1274
end
1275
1276
local function digInFront()
1277
	if isFluidSource(wrapt.inspect) then
1278
		execWithSlot(wrapt.place, BLOCK_SLOT)
1279
	end
1280
	wrapt.dig()
1281
end
1282
1283
local function turnTowardsDirection(targetDir)
1284
	local delta = (targetDir - wrapt.getDirection()) % 4
1285
	if delta == 1 then
1286
		wrapt.turnRight()
1287
	elseif delta == 2 then
1288
		wrapt.turnRight()
1289
		wrapt.turnRight()
1290
	elseif delta == 3 then
1291
		wrapt.turnLeft()
1292
	end
1293
	if targetDir ~= wrapt.getDirection() then
1294
		error("Could not turn to requested direction")
1295
	end
1296
end
1297
1298
1299
1300
local function stepTo(tX, tY, tZ)
1301
	local dX = tX - wrapt.getX()
1302
	local dY = tY - wrapt.getY()
1303
	local dZ = tZ - wrapt.getZ()
1304
	if dY < 0 then
1305
		repeat wrapt.digDown() until wrapt.down()
1306
	elseif dY > 0 then
1307
		repeat wrapt.digUp() until wrapt.up()
1308
	else
1309
		local dir = helper.deltaToDirection(dX, dZ)
1310
		turnTowardsDirection(dir)
1311
		forceMoveForward()
1312
	end
1313
end
1314
1315
local function goToCoords(curBlock, pathfindingArea, pathfindingType)
1316
	local pos = wrapt.getPosition()
1317
	local path = path.pathfind(pos, curBlock, pathfindingArea, pathfindingType)
1318
	if not path then
1319
		return false
1320
	end
1321
1322
	for k=1,path.length do
1323
		if not (path[k].x == pos.x and path[k].y == pos.y and path[k].z == pos.z) then
1324
			stepTo(path[k].x, path[k].y, path[k].z)
1325
		end
1326
	end
1327
	return true
1328
end
1329
1330
1331
1332
local function findOre(oresTable, startPos, rangeLimit)
1333
	local ores = {}
1334
	local isForwardCloseEnough = function() 
1335
		return helper.distance(helper.getForwardPos(wrapt.getPosition()), startPos) <= rangeLimit
1336
	end
1337
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
1338
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
1339
	end
1340
	local down = {x = wrapt.getX(), y = wrapt.getY() - 1, z = wrapt.getZ()}
1341
	if helper.distance(down, startPos) <= rangeLimit and isOre(wrapt.inspectDown) then
1342
		table.insert(ores, down)
1343
	end
1344
	local up = {x = wrapt.getX(), y = wrapt.getY() + 1, z = wrapt.getZ()}
1345
	if helper.distance(up, startPos) <= rangeLimit and isOre(wrapt.inspectUp) then
1346
		table.insert(ores, up)
1347
	end
1348
	wrapt.turnLeft()
1349
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
1350
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
1351
	end
1352
	wrapt.turnLeft()
1353
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
1354
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
1355
	end
1356
	wrapt.turnLeft()
1357
	if isForwardCloseEnough() and isOre(wrapt.inspect) then
1358
		table.insert(ores,helper.getForwardPos(wrapt.getPosition()))
1359
	end
1360
	for _,value in pairs(ores) do
1361
		if not helper.tableContains(oresTable, value, helper.isPosEqual) then
1362
			table.insert(oresTable, value)
1363
		end
1364
	end
1365
end
1366
1367
--traverse an ore vein and return to original turtle position afterwards
1368
local function traverseVein(blocks, rangeLimit)
1369
	local startPos = wrapt.getPosition()
1370
	local minePath = {}
1371
	minePath[helper.getIndex(startPos.x, startPos.y, startPos.z)] = startPos
1372
1373
	local ores = {}
1374
	local searchedPositions = {}
1375
	while true do
1376
		local currentIndex = helper.getIndex(wrapt.getX(),wrapt.getY(),wrapt.getZ())
1377
		if not searchedPositions[currentIndex] then
1378
			findOre(ores, startPos, rangeLimit)
1379
			searchedPositions[currentIndex] = true
1380
		end
1381
		local targetOre = table.remove(ores)
1382
		if not targetOre then
1383
			goToCoords(startPos, minePath, PATHFIND_INSIDE_AREA)
1384
			turnTowardsDirection(startPos.direction)
1385
			break
1386
		end
1387
		local targetIndex = helper.getIndex(targetOre.x, targetOre.y, targetOre.z)
1388
		if not blocks[targetIndex] or not blocks[targetIndex].preserve then
1389
			minePath[helper.getIndex(targetOre.x, targetOre.y, targetOre.z)] = targetOre
1390
			goToCoords(targetOre, minePath, PATHFIND_INSIDE_AREA)
1391
		end
1392
	end
1393
end
1394
1395
1396
1397
--diggingOptions: plugFluidsOnly, oreTraversalRadius
1398
local function processAdjacent(allBlocks, diggingOptions)
1399
	local toPlace = {}
1400
1401
	local pos = wrapt.getPosition()
1402
1403
	local minusX = helper.getIndex(pos.x-1, pos.y, pos.z)
1404
	local plusX = helper.getIndex(pos.x+1, pos.y, pos.z)
1405
	local minusZ = helper.getIndex(pos.x, pos.y, pos.z-1)
1406
	local plusZ = helper.getIndex(pos.x, pos.y, pos.z+1)
1407
1408
	if not allBlocks[minusX] then toPlace[minusX] = {x = pos.x - 1, y = pos.y, z = pos.z} end
1409
	if not allBlocks[plusX] then toPlace[plusX] = {x = pos.x + 1, y = pos.y, z = pos.z} end
1410
	if not allBlocks[minusZ] then toPlace[minusZ] = {x = pos.x, y = pos.y, z = pos.z - 1} end
1411
	if not allBlocks[plusZ] then toPlace[plusZ] = {x = pos.x, y = pos.y, z = pos.z + 1} end
1412
1413
	for key,value in pairs(toPlace) do
1414
		local dX = value.x - pos.x
1415
		local dZ = value.z - pos.z
1416
		local dir = helper.deltaToDirection(dX, dZ)
1417
		turnTowardsDirection(dir)
1418
		if diggingOptions.oreTraversalRadius > 0 and isOre(wrapt.inspect) then
1419
			traverseVein(allBlocks, diggingOptions.oreTraversalRadius)
1420
		end
1421
		if not diggingOptions.plugFluidsOnly or isFluid(wrapt.inspect) then
1422
			execWithSlot(wrapt.place, BLOCK_SLOT)
1423
		end
1424
	end
1425
1426
	local minusY = helper.getIndex(pos.x, pos.y-1, pos.z)
1427
	local plusY = helper.getIndex(pos.x, pos.y+1, pos.z)
1428
1429
	if diggingOptions.oreTraversalRadius > 0 then
1430
		if not allBlocks[minusY] and isOre(wrapt.inspectDown)
1431
			or not allBlocks[plusY] and isOre(wrapt.inspectUp) then
1432
			traverseVein(allBlocks, diggingOptions.oreTraversalRadius)
1433
		end
1434
	end
1435
1436
	if allBlocks[minusY] then
1437
		if not allBlocks[minusY].preserve then digBelow() end
1438
	else
1439
		plugBottom(diggingOptions.plugFluidsOnly)
1440
	end
1441
1442
	if allBlocks[plusY] then
1443
		if not allBlocks[plusY].preserve then digAbove() end
1444
	else
1445
		plugTop(diggingOptions.plugFluidsOnly)
1446
	end
1447
end
1448
1449
1450
1451
1452
local function processTriple(diggingArea)
1453
	local pos = wrapt.getPosition()
1454
	local minusY = helper.getIndex(pos.x, pos.y-1, pos.z)
1455
	local plusY = helper.getIndex(pos.x, pos.y+1, pos.z)
1456
	if not diggingArea[plusY].preserve then digAbove() end
1457
	if not diggingArea[minusY].preserve then digBelow() end
1458
end
1459
1460
1461
1462
local function sortInventory(sortFuel)
1463
	--clear cobble slot
1464
	local initCobbleData = wrapt.getItemDetail(slots.get(BLOCK_SLOT))
1465
	if initCobbleData and initCobbleData.name ~= "minecraft:cobblestone" then
1466
		wrapt.select(slots.get(BLOCK_SLOT))
1467
		wrapt.drop()
1468
	end
1469
1470
	--clear fuel slot
1471
	if sortFuel then
1472
		local initFuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
1473
		if initFuelData and initFuelData.name ~= "minecraft:coal" then
1474
			wrapt.select(slots.get(FUEL_SLOT))
1475
			wrapt.drop()
1476
		end
1477
	end
1478
1479
	--search inventory for cobble and fuel and put them in the right slots
1480
	local fuelData = sortFuel and wrapt.getItemDetail(slots.get(FUEL_SLOT)) or {count=64}
1481
	local cobbleData = wrapt.getItemDetail(slots.get(BLOCK_SLOT))
1482
1483
	if fuelData and cobbleData and fuelData.count > 32 and cobbleData.count > 32 then
1484
		wrapt.select(slots.get(MISC_SLOT))
1485
		return
1486
	end
1487
1488
	for i=slots.get(MISC_SLOT),16 do
1489
		local curData = wrapt.getItemDetail(i)
1490
		if curData then
1491
			if curData.name == "minecraft:cobblestone" then
1492
				wrapt.select(i)
1493
				wrapt.transferTo(slots.get(BLOCK_SLOT))
1494
			elseif sortFuel and curData.name == "minecraft:coal" then
1495
				wrapt.select(i)
1496
				wrapt.transferTo(slots.get(FUEL_SLOT))
1497
			end
1498
		end
1499
	end
1500
1501
	wrapt.select(slots.get(MISC_SLOT))
1502
end
1503
1504
1505
1506
local function dropIntoEntangled(dropFunction)
1507
	local result = false
1508
	repeat
1509
		result = dropInventory(dropFunction)
1510
		if not result then
1511
			helper.printWarning("Entangled chest is full, retrying..")
1512
---@diagnostic disable-next-line: undefined-field
1513
			os.sleep(RETRY_DELAY)
1514
		end
1515
	until result
1516
end
1517
1518
local function findSuitableEntangledChestPos(diggingArea)
1519
	local surroundings = helper.getSurroundings(wrapt.getPosition())
1520
	local options = {}
1521
	for key,value in pairs(surroundings) do
1522
		if diggingArea[key] and not diggingArea[key].preserve then
1523
			table.insert(options, value)
1524
		end
1525
	end
1526
	local selectedOption = table.remove(options)
1527
	if not selectedOption then
1528
		error("Did the turtle just surround itself with chests?")
1529
		return nil
1530
	end
1531
	return selectedOption
1532
end
1533
1534
local function dropOffEntangledChest(diggingArea)
1535
	local selectedOption = findSuitableEntangledChestPos(diggingArea)
1536
	if not selectedOption then
1537
		return
1538
	end
1539
1540
	local curPosition = wrapt.getPosition()
1541
	local delta = {x = selectedOption.x-curPosition.x, y = selectedOption.y-curPosition.y, z = selectedOption.z-curPosition.z}
1542
	if delta.y < 0 then
1543
		repeat digBelow() until execWithSlot(wrapt.placeDown, CHEST_SLOT)
1544
		dropIntoEntangled(wrapt.dropDown)
1545
		wrapt.select(slots.get(CHEST_SLOT))
1546
		digBelow()
1547
		wrapt.select(slots.get(MISC_SLOT))
1548
	elseif delta.y > 0 then
1549
		repeat digAbove() until execWithSlot(wrapt.placeUp, CHEST_SLOT)
1550
		dropIntoEntangled(wrapt.dropUp)
1551
		wrapt.select(slots.get(CHEST_SLOT))
1552
		digAbove()
1553
		wrapt.select(slots.get(MISC_SLOT))
1554
	elseif delta.x ~= 0 or delta.y ~= 0 or delta.z ~= 0 then
1555
		local direction = helper.deltaToDirection(delta.x, delta.z)
1556
		turnTowardsDirection(direction)
1557
		repeat digInFront() until execWithSlot(wrapt.place, CHEST_SLOT)
1558
		dropIntoEntangled(wrapt.drop)
1559
		wrapt.select(slots.get(CHEST_SLOT))
1560
		digInFront()
1561
		wrapt.select(slots.get(MISC_SLOT))
1562
	else
1563
		error("Something went really wrong")
1564
	end
1565
end
1566
1567
local function makeNewChestInPlace(chestData, diggingArea)
1568
	local newPos = table.remove(chestData.reserved)
1569
	if not newPos then
1570
		error("Out of reserved chest spots")
1571
		return false
1572
	end
1573
	chestData.placed = newPos
1574
	
1575
	local chestPosIndex = helper.getIndex(chestData.placed.x, chestData.placed.y, chestData.placed.z)
1576
	chestData.reserved[chestPosIndex] = nil
1577
	local blockAbove = {x = chestData.placed.x, y = chestData.placed.y+1, z = chestData.placed.z}
1578
	if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
1579
		helper.printWarning("Could not pathfind to new chest location, trying again ignoring walls...")
1580
		if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1581
			helper.printWarning("Fallback pathfinding failed")
1582
			return false
1583
		end
1584
	end
1585
	digBelow()
1586
	while true do
1587
		local success = execWithSlot(wrapt.placeDown, CHEST_SLOT)
1588
		if success then
1589
			break
1590
		end
1591
		local chestData = wrapt.getItemDetail(slots.get(CHEST_SLOT))
1592
		if not chestData then
1593
			helper.printWarning("Out of chests. Add chests to slot", slots.get(CHEST_SLOT))
1594
		end
1595
---@diagnostic disable-next-line: undefined-field
1596
		os.sleep(RETRY_DELAY)
1597
	end
1598
	diggingArea[chestPosIndex].preserve = true
1599
	return true
1600
end
1601
1602
local function dropOffNormally(chestData, diggingArea)
1603
	if not chestData.placed then
1604
		makeNewChestInPlace(chestData, diggingArea)
1605
	end
1606
1607
	local blockAbove = {x = chestData.placed.x, y = chestData.placed.y+1, z = chestData.placed.z}
1608
	if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
1609
		helper.printWarning("Could not pathfind to chest location, trying again ignoring walls...")
1610
		if not goToCoords(diggingArea[helper.getIndex(blockAbove.x, blockAbove.y, blockAbove.z)], diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1611
			helper.printWarning("Fallback pathfinding failed too")
1612
		end
1613
	end
1614
	repeat
1615
		for i=slots.get(MISC_SLOT),16 do
1616
			wrapt.select(i)
1617
			if not wrapt.dropDown() and wrapt.getItemDetail(i) then
1618
				makeNewChestInPlace(chestData, diggingArea)
1619
				wrapt.dropDown()
1620
			end
1621
		end
1622
	until not wrapt.getItemDetail(16)
1623
	wrapt.select(slots.get(MISC_SLOT))
1624
end
1625
1626
local function tryDropOffThings(chestData, diggingArea, entangledChest, force)
1627
	if not wrapt.getItemDetail(16) and not force then
1628
		return
1629
	end
1630
1631
	if entangledChest then
1632
		dropOffEntangledChest(diggingArea)
1633
		return
1634
	end
1635
1636
	dropOffNormally(chestData, diggingArea)
1637
end
1638
1639
1640
1641
local function getFuelAndConsumeFromEntangled(suckFunction, dropFunction)
1642
	local result = false
1643
	wrapt.select(16)
1644
	repeat
1645
		repeat
1646
			if not suckFunction(32) then
1647
				helper.printWarning("Refuel chest is empty, retrying...")
1648
				break
1649
			end
1650
			if not wrapt.refuel() then
1651
				helper.printWarning("Refuel chest contains garbage, retrying...")
1652
				while not dropFunction() do
1653
					helper.printWarning("Could not return garbage back to the chest, retrying...")
1654
---@diagnostic disable-next-line: undefined-field
1655
					os.sleep(RETRY_DELAY)
1656
				end
1657
				break
1658
			end
1659
			result = wrapt.getFuelLevel() > REFUEL_THRESHOLD
1660
		until result
1661
		if not result then
1662
---@diagnostic disable-next-line: undefined-field
1663
			os.sleep(RETRY_DELAY)
1664
		end
1665
	until result
1666
end
1667
1668
local function refuelEntangled(chestData, diggingArea, dropOffEntangled)
1669
	tryDropOffThings(chestData, diggingArea, dropOffEntangled)
1670
	
1671
	local selectedOption = findSuitableEntangledChestPos(diggingArea)
1672
	if not selectedOption then return end
1673
1674
	local curPosition = wrapt.getPosition()
1675
	local delta = {x = selectedOption.x-curPosition.x, y = selectedOption.y-curPosition.y, z = selectedOption.z-curPosition.z}
1676
	if delta.y < 0 then
1677
		repeat digBelow() until execWithSlot(wrapt.placeDown, FUEL_CHEST_SLOT)
1678
		getFuelAndConsumeFromEntangled(wrapt.suckDown, wrapt.dropDown)
1679
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
1680
		digBelow()
1681
		wrapt.select(slots.get(MISC_SLOT))
1682
	elseif delta.y > 0 then
1683
		repeat digAbove() until execWithSlot(wrapt.placeUp, FUEL_CHEST_SLOT)
1684
		getFuelAndConsumeFromEntangled(wrapt.suckUp, wrapt.dropUp)
1685
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
1686
		digAbove()
1687
		wrapt.select(slots.get(MISC_SLOT))
1688
	elseif delta.x ~= 0 or delta.y ~= 0 or delta.z ~= 0 then
1689
		local direction = helper.deltaToDirection(delta.x, delta.z)
1690
		turnTowardsDirection(direction)
1691
		repeat digInFront() until execWithSlot(wrapt.place, FUEL_CHEST_SLOT)
1692
		getFuelAndConsumeFromEntangled(wrapt.suck, wrapt.drop)
1693
		wrapt.select(slots.get(FUEL_CHEST_SLOT))
1694
		digInFront()
1695
		wrapt.select(slots.get(MISC_SLOT))
1696
	else
1697
		error("Something went really wrong")
1698
	end
1699
end
1700
1701
local function refuelNormally()
1702
	repeat
1703
		local fuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
1704
		if not fuelData then
1705
			sortInventory(true)
1706
		end
1707
		repeat
1708
			local newFuelData = wrapt.getItemDetail(slots.get(FUEL_SLOT))
1709
			if not newFuelData then
1710
				helper.printWarning("Out of fuel. Put fuel in slot", slots.get(FUEL_SLOT))
1711
	---@diagnostic disable-next-line: undefined-field
1712
				os.sleep(RETRY_DELAY)
1713
				helper.osYield()
1714
			end
1715
		until newFuelData
1716
		execWithSlot(wrapt.refuel, FUEL_SLOT)
1717
	until wrapt.getFuelLevel() > REFUEL_THRESHOLD
1718
end
1719
1720
local function tryToRefuel(chestData, diggingArea, dropOffEntangledChest, refuelEntangledChest)
1721
	if wrapt.getFuelLevel() < REFUEL_THRESHOLD then
1722
		if refuelEntangledChest then 
1723
			refuelEntangled(chestData, diggingArea, dropOffEntangledChest)
1724
			return
1725
		else
1726
			refuelNormally()
1727
			return
1728
		end
1729
	end
1730
end
1731
1732
1733
1734
local function executeDigging(layers, diggingArea, chestData, config)
1735
	local counter = 0
1736
	for layerIndex, layer in ipairs(layers) do
1737
		for blockIndex, block in ipairs(layer) do
1738
			if counter % 5 == 0 or not wrapt.getItemDetail(slots.get(BLOCK_SLOT)) then
1739
				sortInventory(not config.useFuelEntangledChest)
1740
			end
1741
			if counter % 5 == 0 then
1742
				tryToRefuel(chestData, diggingArea, config.useEntangledChests, config.useFuelEntangledChest)
1743
			end
1744
			tryDropOffThings(chestData, diggingArea, config.useEntangledChests)
1745
			if not diggingArea[helper.getIndex(block.x, block.y, block.z)].preserve then
1746
				if not goToCoords(block, diggingArea, PATHFIND_INSIDE_NONPRESERVED_AREA) then
1747
					helper.printWarning("Couldn't find a path to next block, trying again ingoring walls...")
1748
					if not goToCoords(block, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1749
						helper.printWarning("Fallback pathfinding failed, skipping the block")
1750
						break
1751
					end
1752
				end
1753
				if block.adjacent then
1754
					processAdjacent(diggingArea, config)
1755
				elseif block.triple then
1756
					processTriple(diggingArea)
1757
				end
1758
			end
1759
			counter = counter + 1
1760
		end
1761
	end
1762
	tryDropOffThings(chestData, diggingArea, config.useEntangledChests, true)
1763
end
1764
1765
local function getValidatedRegion(config, default)
1766
	ui.printInfo()
1767
	ui.printProgramsInfo()
1768
	while true do
1769
		local shape = nil
1770
		if default then
1771
			shape = ui.parseProgram(config.defaultCommand)
1772
			if not shape then
1773
				helper.printError("defaultCommand is invalid")
1774
				default = false
1775
			end
1776
		end
1777
1778
		if not default then
1779
			shape = ui.promptForShape()
1780
		end
1781
1782
		local genRegion = shape.shape.generate(table.unpack(shape.args))
1783
		local validationResult = region.validateRegion(genRegion)
1784
		if validationResult == SUCCESS then
1785
			return genRegion
1786
		end
1787
		ui.showValidationError(validationResult)
1788
	end
1789
end
1790
1791
local function launchDigging(config, default)
1792
	local diggingArea = getValidatedRegion(config, default)
1793
	local layers = region.createLayersFromArea(diggingArea, config.layerSeparationAxis)
1794
	local chestData = region.reserveChests(diggingArea)
1795
	executeDigging(layers, diggingArea, chestData, config)
1796
end
1797
1798
1799
local function executeMineLoop(config)
1800
	local cumDelta = {x = 0, y = 0, z = 0}
1801
	local prevPos = nil
1802
	while true do
1803
		--create a region to dig
1804
		local shape = ui.parseProgram(config.mineLoopCommand)
1805
		local diggingArea = shape.shape.generate(table.unpack(shape.args))
1806
		diggingArea = region.shiftRegion(diggingArea, cumDelta)
1807
		local layers = region.createLayersFromArea(diggingArea, config.layerSeparationAxis)
1808
		local chestData = region.reserveChests(diggingArea)
1809
1810
		--place chunk loader
1811
		local chunkLoaderPos = table.remove(chestData.reserved)
1812
		local chunkLoaderIndex = helper.getIndex(chunkLoaderPos.x, chunkLoaderPos.y, chunkLoaderPos.z)
1813
		local blockAbove = {x = chunkLoaderPos.x, y = chunkLoaderPos.y+1, z = chunkLoaderPos.z}
1814
		if prevPos then
1815
			prevPos.preserve = true
1816
			diggingArea[helper.getIndex(prevPos.x, prevPos.y, prevPos.z)] = prevPos
1817
		end
1818
		if not goToCoords(blockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1819
			helper.printError("Could not navigate to new chunk loader position, aborting...")
1820
			return
1821
		end
1822
		repeat digBelow() until execWithSlot(wrapt.placeDown, CHUNK_LOADER_SLOT)
1823
		diggingArea[chunkLoaderIndex].preserve = true
1824
1825
		--remove old chunk loader
1826
		if prevPos then
1827
			local prevBlockAbove = {x = prevPos.x, y = prevPos.y+1, z = prevPos.z}
1828
			if not goToCoords(prevBlockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1829
				helper.printError("Could not navigate to previous chunkloader, aborting")
1830
			end
1831
			execWithSlot(digBelow, CHUNK_LOADER_SLOT)
1832
			if not goToCoords(blockAbove, diggingArea, PATHFIND_ANYWHERE_NONPRESERVED) then
1833
				helper.printError("Could not navigate back from old loader, aborting...")
1834
				return
1835
			end
1836
		end
1837
1838
		--dig the region
1839
		executeDigging(layers, diggingArea, chestData, config)
1840
1841
		prevPos = chunkLoaderPos
1842
		cumDelta = {x = cumDelta.x + config.mineLoopOffset.x,y = cumDelta.y + config.mineLoopOffset.y,z = cumDelta.z + config.mineLoopOffset.z}
1843
	end
1844
end
1845
1846
local function launchMineLoop(config, autostart)
1847
	helper.print("Verifying mineLoopCommand...")
1848
	local shape = ui.parseProgram(config.mineLoopCommand)
1849
	if not shape then
1850
		helper.printError("mineLoopCommand is invalid")
1851
		return
1852
	end
1853
	local areaToValidate = shape.shape.generate(table.unpack(shape.args))
1854
	local validationResult = region.validateRegion(areaToValidate)
1855
	if validationResult ~= SUCCESS then
1856
		ui.showValidationError(validationResult)
1857
		return
1858
	end
1859
1860
	if not autostart then
1861
		ui.printInfo()
1862
		helper.print("Press Enter to start the loop")
1863
		helper.read()
1864
	end
1865
	executeMineLoop(config)
1866
end
1867
1868
1869
1870
local function main(...)
1871
	local args = {...}
1872
	local config = helper.readOnlyTable(cfg.processConfig())
1873
	slots.assignSlots(config)
1874
1875
	local default = args[1] == "def"
1876
1877
	if config.mineLoop then
1878
		launchMineLoop(config, default)
1879
		return
1880
	end
1881
	
1882
	launchDigging(config, default)
1883
end
1884
1885
main(...)