Compare commits

...

126 Commits

Author SHA1 Message Date
Leijurv
4bea9dfb42 started on simultaneous dijkstra scaffolder, saving progress here for now 2023-04-10 00:14:13 -07:00
Leijurv
f039a9cff8 belongs at bottom 2023-04-07 22:12:57 -07:00
Leijurv
06d18c4500 shouldnt have been separated 2023-04-07 20:45:08 -07:00
Leijurv
cc6228dced better test 2023-04-07 20:44:04 -07:00
Leijurv
0ed1c31106 more test 2023-04-07 20:37:32 -07:00
Leijurv
8c118401f3 blip staircases work 2023-04-07 17:47:29 -07:00
Leijurv
3e17352e99 navigablesurface now relies on full playerphysics and somehow it's FASTER??? 2023-04-06 23:43:41 -07:00
Leijurv
d594b15e40 abstract away the specific attachment from the impl of a player physics tracking navigable surface 2023-04-06 22:08:54 -07:00
Leijurv
c48d47ff2c no longer required 2023-04-06 21:25:53 -07:00
Leijurv
361c769650 more testing 2023-04-05 23:32:40 -07:00
Leijurv
294e847f19 refactors 2023-04-05 00:22:15 -07:00
Leijurv
c9bfcb417e compare with/without random splays before/during connection tests 2023-04-04 01:44:43 -07:00
Leijurv
1fc3155e9c more explanation 2023-04-02 23:19:38 -07:00
Leijurv
a9d6aaebf3 misc 2023-04-02 15:12:07 -07:00
Leijurv
b2ffa7de1b improved playerphysicstest actually caught a real bug 2023-03-27 23:17:12 +09:00
Leijurv
5ad90997ff player physics with bidirectional movement with tests, intended for use in navigable surface (because euler tour forests require an undirected graph) 2023-03-27 09:14:30 +09:00
Leijurv
b3afa98958 got a little carried away testing navigable surface 2023-03-23 21:43:37 -07:00
Leijurv
d2a5199383 unzobrist test just to make sure 2023-03-23 21:19:49 -07:00
Leijurv
d32973cae4 equals and hashcode for navigable surface tree attachment 2023-03-21 00:42:09 -07:00
Leijurv
f5dc6a034b refactor conngraphtest since i wrote part of it 2023-03-21 00:37:51 -07:00
Leijurv
06590f487b refactor and integrate redblacknode repo 2023-03-21 00:36:43 -07:00
Leijurv
3549a55f1e Merge remote-tracking branch 'rbn/trimmed' into builder-2 2023-03-21 00:26:43 -07:00
Leijurv
b8608c98e7 deleting these files to make way for merge of unrelated git history 2023-03-21 00:26:11 -07:00
Leijurv
e15cf933aa don't need tree list 2023-03-21 00:25:23 -07:00
Leijurv
8812f3027b trimmed for merge 2023-03-21 00:22:45 -07:00
Leijurv
69b36eea4b only run once 2023-03-21 00:11:27 -07:00
Leijurv
af320bd052 refactor and integrate 2023-03-21 00:11:04 -07:00
Leijurv
317f07283e integrate betterblockpos 2023-03-21 00:00:53 -07:00
Leijurv
bce0d47f95 tweak for older version of fastutil 2023-03-20 23:58:25 -07:00
Leijurv
b8d4c72a92 Merge remote-tracking branch 'dyncon/trimmed' into builder-2 2023-03-20 23:55:09 -07:00
Leijurv
0f5ee85e19 trim for merge 2023-03-20 23:54:02 -07:00
Leijurv
e7f54ab81d switch from integer to custom tree attachment 2023-03-20 23:51:19 -07:00
Leijurv
6fea22dc9f this is incredibly cool and it does figure out the staircase pattern like i hoped 2023-03-16 00:42:23 -07:00
Leijurv
2cb46e0540 explanatory readme 2023-03-16 00:18:38 -07:00
Leijurv
79113cf9a0 Merge branch 'long-map' into baritone-testing 2023-03-15 22:41:46 -07:00
Leijurv
fe808a5f83 test 2023-03-15 22:41:42 -07:00
Leijurv
200e68a1b9 import from baritone builder-2 2023-03-15 22:35:40 -07:00
Leijurv
59be6b4606 dead link 2023-03-15 14:56:13 -07:00
Leijurv
236d171d15 just in case anyone is curious, check in my partial impl from last year of euler tour forests over splay nodes (it was too intimidating to fully reimpl on top of a red-black or avl tree) 2023-03-15 14:54:13 -07:00
Leijurv
7ee6b40815 and i guess this should also use long instead of connvertex 2023-03-15 14:12:14 -07:00
Leijurv
49b620a7cf just checking if a long map could be faster, doesnt seem to matter (yet?) 2023-03-15 13:59:57 -07:00
Leijurv
52f795c3ae ram usage of empty hashmap buckets doesn't affect my use case 2023-03-15 13:43:39 -07:00
Leijurv
c177d6c708 need more heap 2023-03-15 13:39:27 -07:00
Leijurv
9870cbddc3 supposedly need to bump junit 2023-03-15 13:33:22 -07:00
Leijurv
8ee36bcd46 reformat all code 2023-03-15 13:30:23 -07:00
Leijurv
2af6dec3df switch from maven to gradle 2023-03-15 13:26:44 -07:00
Leijurv
802c81d766 bring in red black node 2023-03-15 13:23:39 -07:00
Leijurv
3a8c6712b1 satisfied with benchmark 2023-03-15 13:16:14 -07:00
Leijurv
f378578ada misc intellij 2023-03-15 13:16:08 -07:00
Leijurv
92447c0b5c dijkstra tweaks 2023-03-14 18:13:21 -07:00
Leijurv
023bebee66 more tests and sanity checks around collapsing components 2023-03-08 16:17:18 -08:00
Leijurv
cadf7a06d0 refactor per state with scaffolding into place order dependency graph 2023-03-08 13:49:20 -08:00
Leijurv
19c9238ea2 comments and thoughts about component merge order 2023-03-08 00:59:00 -08:00
Leijurv
19eda1bfe5 scaffolder output 2023-03-07 00:52:39 -08:00
Leijurv
1ffadf0242 refactor out scaffolder strategy 2023-03-07 00:37:53 -08:00
Leijurv
5d0904b46e refactor dependencygraphscaffoldingoverlay to be only mutable inside scaffolder (this was bugging me) 2023-03-04 22:00:18 -08:00
William Jacobs
3eda7a57e8 Added assessment of ancestor pointers to optimization_ideas.txt
This adds my assessment of the performance of the ancestor pointers optimization in practice to optimization_ideas.txt.
2022-08-02 19:27:45 -04:00
William Jacobs
2031ed4bc3 Added ancestor pointers to optimization_ideas.txt
This adds the idea of using ancestor pointers to optimization_ideas.txt.
2022-07-19 12:12:57 -04:00
Leijurv
a448d97ec9 dont remember, misc changes 2021-11-01 17:41:29 -07:00
Leijurv
1a59427957 refactor out sneak position 2021-09-03 13:47:00 -07:00
Leijurv
217f6ecf28 refactor out zobrist world state cache 2021-09-03 13:29:29 -07:00
Leijurv
0fd91edb97 column system, sneaking nodes, connected up raytrace system, misc refactors 2021-09-01 16:22:59 -07:00
Leijurv
35c2d8ba24 refactors and begin on sneak nodes 2021-08-31 17:40:01 -07:00
Leijurv
b26d10e26e jump physics 2021-08-27 23:56:46 -07:00
Leijurv
9da3e09062 snow sucks 2021-08-27 19:01:31 -07:00
Leijurv
9fce9ef5e3 precompute scaffolding variants to remove branch in dependency graph lookup 2021-08-27 16:13:29 -07:00
Leijurv
e5773da108 that's what the mask is 2021-08-24 22:21:41 -07:00
Leijurv
554c0de188 misc progress hooking everything up into solver engine harness 2021-08-24 20:26:38 -07:00
Leijurv
bea31fe77d Merge branch 'master' into builder-2 2021-08-24 15:16:30 -07:00
Leijurv
bcd22dc91c debug 2021-08-24 10:51:38 -07:00
Leijurv
33082428f3 some progress and reworking of greedy solver 2021-08-24 00:17:55 -07:00
Leijurv
0b4e5f753a tweaked physics and begun core pathfinder loop 2021-08-20 16:27:36 -07:00
Leijurv
5aa0dfc98c started on greedy solver, using concept from the zobrist testing 2021-08-19 00:16:52 -07:00
Leijurv
164d0e07cd bounds refactor and start on solver engine interface 2021-08-18 21:50:41 -07:00
Leijurv
b608a8a1e0 misc 2021-08-17 16:04:23 -07:00
Leijurv
02f5d4efbe misc bounds benchmark, property extractor, scaffolder 2021-05-24 17:16:25 -07:00
Leijurv
a9b7b91a3c misc tweaks, forgot to commit last week 2021-05-24 12:52:34 -07:00
Leijurv
d501359857 progress on properties and entity oriented blocks 2021-05-18 00:16:30 -07:00
Leijurv
d9591e089d basic impl of place options 2021-05-17 02:12:51 -07:00
Leijurv
4848e901c9 progress on reachability cache and raytracer tweaks 2021-05-17 01:53:16 -07:00
Leijurv
65a650c1a6 readme 2021-05-15 22:30:51 -07:00
Leijurv
ca2e7c958a remove zoomy branchy 2021-05-15 21:41:47 -07:00
Leijurv
c97b089c61 rework placeagainstdata 2021-05-13 00:37:40 -07:00
Leijurv
5170b34962 blip 2021-05-12 01:10:25 -07:00
Leijurv
0a8b6e9427 many more blocks, physics attempt 2021-05-11 23:27:52 -07:00
Leijurv
efb0afcd6c switch to builder pattern for block state cached data 2021-05-10 02:18:59 -07:00
Leijurv
5c85d0cfd1 fixed up raytracer and placement option 2021-05-10 00:23:10 -07:00
Leijurv
fdf899eabf progress on raytracer 2021-04-21 11:42:56 -07:00
Leijurv
e62f480836 started on scaffolding 2021-04-16 20:43:24 -07:00
Leijurv
6ef6651ed6 more optim 2021-04-13 19:08:44 -07:00
Leijurv
21aaab1b9e optims 2021-04-13 18:03:33 -07:00
Leijurv
8450675401 saving progress here, incremental scaffolding works and is fast 2021-04-13 16:08:30 -07:00
Leijurv
96fd72e5b0 testing 2021-04-03 15:49:30 -07:00
btrekkie
5cfefbb9ec Merge pull request #2 from btrekkie/dependabot/maven/junit-junit-4.13.1
Bump junit from 4.12 to 4.13.1
2020-10-15 19:20:39 -04:00
btrekkie
1c8ccd4646 Merge pull request #1 from btrekkie/dependabot/maven/junit-junit-4.13.1
Bump junit from 4.12 to 4.13.1
2020-10-15 19:17:39 -04:00
dependabot[bot]
6f69316966 Bump junit from 4.12 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 12:15:30 +00:00
dependabot[bot]
5e77ef97e4 Bump junit from 4.12 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 12:15:26 +00:00
William Jacobs
0fed0d4371 Removed outdated pointer to documentation
This removes the suggestion to check the source code for the documentation from README.md. Now that there is a website with the documentation, there is no need to look through the source code.
2020-06-26 10:51:11 -04:00
William Jacobs
1069dc9700 Increased minimum Java version and updated documentation
This increases the minimum Java version to 7.0, because Maven doesn't like Java 6.0, and it makes assorted improvements to the documentation.
2020-05-30 10:36:45 -04:00
William Jacobs
515a28dcfd Added reference to documentation webpage
This changes README.md to refer to the webpage containing the documentation for the project.
2020-05-30 10:33:13 -04:00
William Jacobs
426838f33e Added optimization_ideas.txt
This adds optimization_ideas.txt, a text file containing my thoughts on how to make ConnGraph faster.
2019-03-23 20:06:47 -04:00
William Jacobs
458cdd3ddd Fixed size calculation in optimizeForestEdges()
This fixes the code in ConnGraph.optimizeForestEdges() that computes the resulting size after combining two Euler tour trees into one. Technically, the old behavior was still correct, but only by accident; this change is still kind of a bug fix.
2019-03-18 13:21:26 -04:00
William Jacobs
05d678a189 Split removeEdge and optimize() into smaller methods
This moves a bit of functionality from ConnGraph.removeEdge and ConnGraph.optimize() into new methods, in order to improve readability.
2019-03-16 14:42:36 -04:00
William Jacobs
454a2b8ae7 Minor readability improvements
This makes various minor improvements to readability and implementation.
2019-03-16 14:17:40 -04:00
William Jacobs
1536209eb3 Optimization: don't push forest edges if there are no non-forest edges
This optimizes the process of searching for a replacement edge to refrain from pushing forest edges down when a tree does not have any same-level non-forest edges. If a tree doesn't have any such edges, then there definitely isn't a replacement edge at that level, so we can avoid doing extra work.

Interestingly, with this optimization, ConnGraph reduces to a single Euler tour forest if the graph is a forest, with addEdge and removeEdge taking O(log N) (non-amortized) time with high probability.
2019-03-06 22:24:50 -05:00
William Jacobs
a60eac5b6c Initial commit
This adds the initial contents of the repository.
2019-03-06 16:46:45 -05:00
btrekkie
7d95d5f991 Initial commit 2019-03-06 16:36:08 -05:00
William Jacobs
8cf1eb9230 Converted to Maven project
This changes RedBlackNode into a Maven project, by adding pom.xml and changing the directory structure. This should make it easier for other projects to include RedBlackNode as a dependency.
2019-03-06 15:20:01 -05:00
William Jacobs
148e9247d3 Clarified main documentation
This changes README.md and the comment for RedBlackNode to more clearly explain what the project is all about. It emphasizes the fact that RedBlackNode provides public access to the tree's structure. It changes the usage example in README.md from a short RedBlackNode subclass highlighting how easy augmentation is to a medium-length pair of tree and node classes that show how to use insertion, removal, and augmentation.

This change also makes minor improvements to comments for RedBlackNode methods.
2019-03-04 17:01:46 -05:00
Bill Jacobs
6d6b968fd6 Simplified if statements
This simplifies this:

if (a) {
    c;
}
if (b) {
    c;
}

to this:

if (a || b) {
    c;
}
2017-05-13 18:19:03 -05:00
Bill Jacobs
3c75a5e39e Moved TreeList.addAll test to TreeListTest.testAddAll
This moves some code that tests TreeList.addAll from TreeListTest.testAdd to TreeListTest.testAddAll
2016-07-22 17:13:19 -07:00
Bill Jacobs
9534c4ae06 Fixed RedBlackNode.concatenate on two one-node trees
This fixes RedBlackNode.concatenate to work on two one-node trees.  The check for determining which tree had the greater red-black height was incorrect in that case.
2016-07-22 17:03:36 -07:00
Bill Jacobs
5e23bd9c81 Changed SubArrayMinTest to use Integer.bitCount
This changes SubArrayMinTest to use the library method Integer.bitCount rather than a hand-rolled bit counting implementation.
2016-06-24 14:46:37 -07:00
Bill Jacobs
91b5ae633a Fixed SubArrayMin to check children
This fixes SubArrayMin to check the appropriate children of the endpoint nodes, in addition to the children of their ancestors.
2016-06-17 21:54:19 -07:00
Bill Jacobs
8c98d5cc42 Changed createTree to set root's parent to null
This changes createTree to set the "parent" field of the root node to null.
2016-06-06 14:55:58 -07:00
Bill Jacobs
cd4424b94a Fixed calls to augment() in fixSiblingDeletion()
This fixes fixSiblingDeletion() to call augment() in certain cases where augment() returns false.
2016-06-02 20:40:35 -07:00
Bill Jacobs
deb397ed1e Changed fixInsertion to always augment parent
This changes fixInsertion to always augment the node's parent, even if the initial call to augment() returns false, assuming "augment" is true.  When we insert a node, we are supposed to ignore its initial state; thus, we ignore the return value of augment().
2016-05-31 23:07:52 -07:00
Bill Jacobs
4e1fe67095 Updated RedBlackNode.jar to clear links after removing node
This updates RedBlackNode.jar to include the changes in dd0fd1959c.
2016-05-28 17:35:32 -07:00
Bill Jacobs
dd0fd1959c Clear links after removing node
This changes the remove methods to set the parent and child links to be null, so that we're more likely to encounter an exception if we attempt to access the node.
2016-05-28 10:56:08 -07:00
Bill Jacobs
a1dba8a58c Added test for "lca"
This adds SubArrayMinTest, which tests RedBlackNode.lca.
2016-05-26 14:47:27 -07:00
Bill Jacobs
f355e1ed2b Added LCA
This adds a RedBlackNode method for computing the lowest common ancestor of two nodes.
2016-05-26 12:22:32 -07:00
Bill Jacobs
82c1fbdc7b Made fixInsertion return the new root
This changes RedBlackNode.fixInsertion to return the root of the resulting tree.
2016-05-26 10:37:25 -07:00
Bill Jacobs
f7f14ff852 Added RedBlackNode.jar
This adds RedBlackNode.jar for using RedBlackNode in binary form.
2016-05-23 14:00:40 -07:00
Bill Jacobs
754e11ade7 Escaped "<" and ">"
This escapes the "<" and ">" characters in README.md.
2016-05-23 13:57:28 -07:00
Bill Jacobs
389f8ea4e2 Initial commit
This adds the initial contents of the repository.
2016-05-23 13:51:21 -07:00
btrekkie
d3e6f9ae02 Initial commit 2016-05-23 13:43:59 -07:00
95 changed files with 15168 additions and 22 deletions

View File

@@ -17,6 +17,7 @@
package baritone.api.utils;
import it.unimi.dsi.fastutil.HashCommon;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
@@ -79,25 +80,98 @@ public final class BetterBlockPos extends BlockPos {
return longHash(pos.x, pos.y, pos.z);
}
public static final int NUM_X_BITS = 26;
public static final int NUM_Z_BITS = NUM_X_BITS;
public static final int NUM_Y_BITS = 9; // note: even though Y goes from 0 to 255, that doesn't mean 8 bits will "just work" because the deserializer assumes signed. i could change it for just Y to assume unsigned and leave X and Z as signed, however, we know that in 1.17 they plan to add negative Y. for that reason, the better approach is to give the extra bits to Y and leave it as signed.
// also, if 1.17 sticks with the current plan which is -64 to +320, we could have 9 bits for Y and a constant offset of -64 to change it to -128 to +256.
// that would result in the packed long representation of any valid coordinate still being a positive integer
// i like that property, so i will keep num_y_bits at 9 and plan for an offset in 1.17
// it also gives 1 bit of wiggle room in case anything else happens in the future, so we are only using 63 out of 64 bits at the moment
public static final int Z_SHIFT = 0;
public static final int Y_SHIFT = Z_SHIFT + NUM_Z_BITS + 1; // 1 padding bit to make twos complement not overflow
public static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS + 1; // and also here too
public static final long X_MASK = (1L << NUM_X_BITS) - 1L; // X doesn't need padding as the overflow carry bit is just discarded, like a normal long (-1) + (1) = 0
public static final long Y_MASK = (1L << NUM_Y_BITS) - 1L;
public static final long Z_MASK = (1L << NUM_Z_BITS) - 1L;
public static final long POST_ADDITION_MASK = X_MASK << X_SHIFT | Y_MASK << Y_SHIFT | Z_MASK << Z_SHIFT; // required to "manually inline" toLong(-1, -1, -1) here so that javac inserts proper ldc2_w instructions at usage points instead of getstatic
// what's this ^ mask for?
// it allows for efficient offsetting and manipulation of a long packed coordinate
// if we had three ints, x y z, it would be easy to do "y += 1" or "x -= 1"
// but how do you do those things if you have a long with x y and z all stuffed into one primitive?
// adding together two long coordinates actually works perfectly if both sides have X, Y, and Z as all positive, no issues at all
// but when Y or Z is negative, we run into an issue. consider 8 bits: negative one is 11111111 and one is 00000001
// adding them together gives 00000000, zero, **but only because there isn't a 9th bit to carry into**
// if we had, instead, 00000000 11111111 + 00000000 00000001 we would rightly get 00000001 00000000 with the 1 being carried into the 9th position there
// this is exactly what happens. "toLong(0, 1, 0) + toLong(0, -1, 0)" ends up equaling toLong(1, 0, 0) while we'd rather it equal toLong(0, 0, 0)
// so, we simply mask out the unwanted result of the carry by inserting 1 bit of padding space (as added above) between each
// it used to be 000XXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYZZZZZZZZZZZZZZZZZZZZZZZZZZ
// and now it is 0XXXXXXXXXXXXXXXXXXXXXXXXXX0YYYYYYYYY0ZZZZZZZZZZZZZZZZZZZZZZZZZZ
// we simply place the X Y and Z in slightly different sections of the long, putting a bit of space between each
// the mask ^ is 0111111111111111111111111110111111111011111111111111111111111111
// using that example of (0,1,0) + (0,-1,0), here's what happens
// 0000000000000000000000000000000000001000000000000000000000000000 (this is X=0 Y=1 Z=0)
// + 0000000000000000000000000000111111111000000000000000000000000000 (this is X=0 Y=-1 Z=0)
// = 0000000000000000000000000001000000000000000000000000000000000000
// the unwanted carry bit here ^ is no longer corrupting the least significant bit of X and making it 1!
// now it's just turning on the unused padding bit that we don't care about
// using the mask and bitwise and, we can easily and branchlessly turn off the padding bits just in case something overflow carried into them!
// 0000000000000000000000000001000000000000000000000000000000000000 (the result of the addition from earlier)
// & 0111111111111111111111111110111111111011111111111111111111111111 (this is POST_ADDITION_MASK)
// = 0000000000000000000000000000000000000000000000000000000000000000
// POST_ADDITION_MASK retains the bits that actually form X, Y, and Z, but intentionally turns off the padding bits
// so, we can simply do "(toLong(0, 1, 0) + toLong(0, -1, 0)) & POST_ADDITION_MASK" and correctly get toLong(0, 0, 0)
// which is incredibly fast and efficient, an add then a bitwise AND against a constant
// and it doesn't require us to pull out X, Y, and Z, modify one of them, and put them all back into the long
// that's what the point of the mask is
static {
if (POST_ADDITION_MASK != toLong(-1, -1, -1)) {
throw new IllegalStateException(POST_ADDITION_MASK + " " + toLong(-1, -1, -1)); // sanity check
}
}
public long toLong() {
return toLong(this.x, this.y, this.z);
}
public static BetterBlockPos fromLong(long serialized) {
return new BetterBlockPos(XfromLong(serialized), YfromLong(serialized), ZfromLong(serialized));
}
public static int XfromLong(long serialized) {
return (int) (serialized << (64 - X_SHIFT - NUM_X_BITS) >> (64 - NUM_X_BITS));
}
public static int YfromLong(long serialized) {
return (int) (serialized << (64 - Y_SHIFT - NUM_Y_BITS) >> (64 - NUM_Y_BITS));
}
public static int ZfromLong(long serialized) {
return (int) (serialized << (64 - Z_SHIFT - NUM_Z_BITS) >> (64 - NUM_Z_BITS));
}
public static long toLong(final int x, final int y, final int z) {
return ((long) x & X_MASK) << X_SHIFT | ((long) y & Y_MASK) << Y_SHIFT | ((long) z & Z_MASK) << Z_SHIFT;
}
public static long offsetBy(long pos, int x, int y, int z) {
return (pos + toLong(x, y, z)) & BetterBlockPos.POST_ADDITION_MASK;
}
public static final long HASHCODE_MURMUR_MASK = murmur64(-1);
public static final long ZOBRIST_MURMUR_MASK = murmur64(-2);
public static long longHash(int x, int y, int z) {
// TODO use the same thing as BlockPos.fromLong();
// invertibility would be incredibly useful
/*
* This is the hashcode implementation of Vec3i (the superclass of the class which I shall not name)
*
* public int hashCode() {
* return (this.getY() + this.getZ() * 31) * 31 + this.getX();
* }
*
* That is terrible and has tons of collisions and makes the HashMap terribly inefficient.
*
* That's why we grab out the X, Y, Z and calculate our own hashcode
*/
long hash = 3241;
hash = 3457689L * hash + x;
hash = 8734625L * hash + y;
hash = 2873465L * hash + z;
return hash;
return longHash(toLong(x, y, z));
}
public static long longHash(long packed) {
return murmur64(HASHCODE_MURMUR_MASK ^ packed);
}
public static long murmur64(long h) {
return HashCommon.murmurHash3(h);
}
@Override
@@ -206,10 +280,10 @@ public final class BetterBlockPos extends BlockPos {
@Nonnull
public String toString() {
return String.format(
"BetterBlockPos{x=%s,y=%s,z=%s}",
SettingsUtil.maybeCensor(x),
SettingsUtil.maybeCensor(y),
SettingsUtil.maybeCensor(z)
"BetterBlockPos{x=%d,y=%d,z=%d}",
x,
y,
z
);
}
}

View File

@@ -24,6 +24,7 @@ import baritone.api.event.listener.IEventBus;
import baritone.api.utils.Helper;
import baritone.api.utils.IPlayerContext;
import baritone.behavior.*;
import baritone.builder.Main;
import baritone.cache.WorldProvider;
import baritone.command.manager.CommandManager;
import baritone.event.GameEventHandler;
@@ -120,6 +121,15 @@ public class Baritone implements IBaritone {
this.worldProvider = new WorldProvider();
this.selectionManager = new SelectionManager(this);
this.commandManager = new CommandManager(this);
try {
Main.main();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Throwable th) {
th.printStackTrace();
throw th;
}
}
@Override

View File

@@ -0,0 +1,56 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.IPlayerContext;
/**
* 1/16th of a block
* <p>
* Why do this? Several reasons:
* <p>
* • No floating point inaccuracy. I got incredibly annoyed with simple stuff like >1 and <1 being randomly wrong. It helps with stuff like slab calculations, 0.5 block steps, etc
* <p>
* • It's obscenely fast
*/
public class Blip {
public static final int FULL_BLOCK = 16;
public static final double RATIO = 0.0625;
public static final int HALF_BLOCK = 8;
public static final int PLAYER_HEIGHT_SLIGHT_UNDERESTIMATE = 28;
public static final int PLAYER_HEIGHT_SLIGHT_OVERESTIMATE = 29;
public static final int TWO_BLOCKS = 2 * FULL_BLOCK;
public static final int FEET_TO_EYE_APPROX = (int) (IPlayerContext.eyeHeight(false) / RATIO);
public static final int JUMP = 20; // 1.25
public static final int TALLEST_BLOCK = FULL_BLOCK + HALF_BLOCK; // 24, 1.5 blocks tall, the fence / wall has the highest collision box
public static double playerEyeFromFeetBlips(int feetBlips, boolean sneaking) {
return feetBlips * RATIO + IPlayerContext.eyeHeight(sneaking);
}
static {
double realPlayerHeight = 1.8;
if (PLAYER_HEIGHT_SLIGHT_OVERESTIMATE * RATIO <= realPlayerHeight || PLAYER_HEIGHT_SLIGHT_UNDERESTIMATE * RATIO >= realPlayerHeight || PLAYER_HEIGHT_SLIGHT_OVERESTIMATE != PLAYER_HEIGHT_SLIGHT_UNDERESTIMATE + 1) {
throw new IllegalStateException();
}
if (FULL_BLOCK * RATIO != 1) {
throw new IllegalStateException();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class BlockData {
private final BlockStateCachedData[] PER_STATE;
public BlockData(IBlockStateDataProvider provider) {
PER_STATE = provider.allNullable();
}
public BlockStateCachedData get(int state) {
return PER_STATE[state];
}
public List<BlockStateCachedData> getAllStates() {
return Collections.unmodifiableList(Arrays.asList(PER_STATE));
}
}

View File

@@ -0,0 +1,85 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.Collections;
import java.util.List;
/**
* Information about an IBlockState
* <p>
* There will be exactly one of these per valid IBlockState in the game
*/
public final class BlockStateCachedData {
public final boolean fullyWalkableTop;
private final int collisionHeightBlips;
public final boolean isAir;
public final boolean collidesWithPlayer;
public final boolean mustSneakWhenPlacingAgainstMe;
public final List<BlockStatePlacementOption> placeMe; // list because of unknown size with no obvious indexing
public final PlaceAgainstData[] placeAgainstMe; // array because of fixed size with obvious indexing (no more than one per face, so, index per face)
public BlockStateCachedData(BlockStateCachedDataBuilder builder) {
builder.sanityCheck();
this.isAir = builder.isAir();
this.fullyWalkableTop = builder.isFullyWalkableTop();
this.collidesWithPlayer = builder.isCollidesWithPlayer();
if (collidesWithPlayer) {
this.collisionHeightBlips = builder.collisionHeightBlips();
} else {
this.collisionHeightBlips = -1;
}
this.mustSneakWhenPlacingAgainstMe = builder.isMustSneakWhenPlacingAgainstMe();
this.placeMe = Collections.unmodifiableList(builder.howCanIBePlaced());
this.placeAgainstMe = builder.placeAgainstMe();
}
public int collisionHeightBlips() {
if (Main.DEBUG && !collidesWithPlayer) { // confirmed and tested: when DEBUG is false, proguard removes this if in the first pass, then inlines the calls in the second pass, making this just as good as a field access in release builds
throw new IllegalStateException();
}
return collisionHeightBlips;
}
public boolean possibleAgainstMe(BlockStatePlacementOption placement) {
PlaceAgainstData against = againstMe(placement);
return against != null && possible(placement, against);
}
public PlaceAgainstData againstMe(BlockStatePlacementOption placement) {
return placeAgainstMe[placement.against.oppositeIndex];
}
public static boolean possible(BlockStatePlacementOption placement, PlaceAgainstData against) {
if (placement.against != against.against.opposite()) {
throw new IllegalArgumentException();
}
if (placement.against.vertical) {
return true;
}
return
(against.presentsAnOptionStrictlyInTheBottomHalfOfTheStandardVoxelPlane() && placement.half != Half.TOP) ||
(against.presentsAnOptionStrictlyInTheTopHalfOfTheStandardVoxelPlane() && placement.half != Half.BOTTOM);
}
}

View File

@@ -0,0 +1,326 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class BlockStateCachedDataBuilder {
// should all these be optionals? like maybe Boolean? that start as null? and each has to be set explicitly?
private boolean isAir;
private boolean canPlaceAgainstMe;
private boolean fullyWalkableTop;
private boolean collidesWithPlayer;
private boolean mustSneakWhenPlacingAgainstMe;
private boolean mustBePlacedBottomToTop;
/**
* Examples:
* <p>
* Upside down stairs must be placed against TOP
* <p>
* Bottom slabs must be placed against BOTTOM
* <p>
* Normal blocks must be placed against EITHER
*/
private Half mustBePlacedAgainst = Half.EITHER;
private Face playerMustBeHorizontalFacingInOrderToPlaceMe;
private Integer collisionHeightBlips;
private Face canOnlyPlaceAgainst;
/**
* Blocks that have a collision height lower than their placement bounding box height.
* aka snow layers and soul sand.
* e.g. soul sand only collides with the player 0.875 high, but the BLOCK is 1.000 high, i.e. you can place against it at height 0.9 even though the player stands at height 0.875
*/
private boolean fakeLessThanFullHeight;
private boolean placementLogicNotImplementedYet;
private Face playerMustBeEntityFacingInOrderToPlaceMe;
public BlockStateCachedDataBuilder() {
}
/**
* Really just air. This is a fully open block that won't collide with object mouse over raytrace.
*/
public BlockStateCachedDataBuilder setAir() {
isAir = true;
mustBePlacedAgainst = null;
return this;
}
public boolean isAir() {
return isAir;
}
/**
* does the top face of this block fully support the player from 0.0,0.0 to 1.0,1.0? true for most normal blocks. false for, for example, fences
*/
public BlockStateCachedDataBuilder fullyWalkableTop() {
fullyWalkableTop = true;
return this;
}
public boolean isFullyWalkableTop() {
return fullyWalkableTop;
}
/**
* The highest collision extension of this block possible
* <p>
* For example, should be 1 for stairs, even though part of the top face is really 0.5
* <p>
* Should be 1 for top slabs
* <p>
* Should be 1 for trapdoors because when they're open, they touch the top face of the voxel
*/
public BlockStateCachedDataBuilder collisionHeight(double y) {
for (int h = 0; h <= Blip.TALLEST_BLOCK; h++) { // max height of
if (y == h * Blip.RATIO) {
collisionHeightBlips = h;
return this;
}
}
throw new IllegalStateException();
}
public Integer collisionHeightBlips() { // e.g. slabs are 0.5, soul sand is 0.875, normal blocks are 1, fences are 1.5
return collisionHeightBlips;
}
public BlockStateCachedDataBuilder mustSneakWhenPlacingAgainstMe() {
mustSneakWhenPlacingAgainstMe = true;
return this;
}
public boolean isMustSneakWhenPlacingAgainstMe() {
return mustSneakWhenPlacingAgainstMe;
}
public BlockStateCachedDataBuilder canPlaceAgainstMe() {
canPlaceAgainstMe = true;
return this;
}
public boolean isCollidesWithPlayer() {
return collidesWithPlayer;
}
public BlockStateCachedDataBuilder collidesWithPlayer(boolean val) {
collidesWithPlayer = val;
return this;
}
public BlockStateCachedDataBuilder playerMustBeHorizontalFacingInOrderToPlaceMe(Face face) {
playerMustBeHorizontalFacingInOrderToPlaceMe = face;
return this;
}
public BlockStateCachedDataBuilder playerMustBeEntityFacingInOrderToPlaceMe(Face face) {
playerMustBeEntityFacingInOrderToPlaceMe = face;
return this;
}
public BlockStateCachedDataBuilder mustBePlacedAgainst(Half half) {
if (half == null) {
throw new IllegalArgumentException();
}
mustBePlacedAgainst = half;
return this;
}
public BlockStateCachedDataBuilder mustBePlacedBottomToTop() {
mustBePlacedBottomToTop = true;
return this;
}
public BlockStateCachedDataBuilder canOnlyPlaceAgainst(Face face) {
canOnlyPlaceAgainst = face;
return this;
}
public BlockStateCachedDataBuilder placementLogicNotImplementedYet() {
placementLogicNotImplementedYet = true;
return this;
}
public BlockStateCachedDataBuilder fakeLessThanFullHeight() {
fakeLessThanFullHeight = true;
return this;
}
public List<BlockStatePlacementOption> howCanIBePlaced() {
if (mustBePlacedAgainst == null || placementLogicNotImplementedYet) {
return Collections.emptyList();
}
List<BlockStatePlacementOption> ret = new ArrayList<>();
for (Face face : Face.VALUES) {
if (Main.STRICT_Y && face == Face.UP) {
continue; // TODO don't do this...
}
if (playerMustBeHorizontalFacingInOrderToPlaceMe == face.opposite()) { // obv, this won't happen if playerMustBeHorizontalFacing is null
continue;
}
if (playerMustBeEntityFacingInOrderToPlaceMe == face) {
continue;
}
if (mustBePlacedBottomToTop && face != Face.DOWN) {
continue;
}
if (canOnlyPlaceAgainst != null && face != canOnlyPlaceAgainst) {
continue;
}
Half overrideHalf = mustBePlacedAgainst;
if (face == Face.DOWN) {
if (mustBePlacedAgainst == Half.TOP) {
continue;
} else {
overrideHalf = Half.EITHER;
}
}
if (face == Face.UP) {
if (mustBePlacedAgainst == Half.BOTTOM) {
continue;
} else {
overrideHalf = Half.EITHER;
}
}
ret.add(BlockStatePlacementOption.get(face, overrideHalf, Optional.ofNullable(playerMustBeHorizontalFacingInOrderToPlaceMe), Optional.ofNullable(playerMustBeEntityFacingInOrderToPlaceMe)));
}
return ret;
}
public PlaceAgainstData[] placeAgainstMe() {
PlaceAgainstData[] data = new PlaceAgainstData[Face.NUM_FACES];
if (!canPlaceAgainstMe) {
return data;
}
for (int i = 0; i < Face.NUM_FACES; i++) {
data[i] = placeAgainstFace(Face.VALUES[i]);
}
return data;
}
protected PlaceAgainstData placeAgainstFace(Face face) {
// TODO this makes the stair/slab assumption that the same half is the mustBePlacedAgainst as the faces offered for placement... counterexample is daylight sensor
// note that this is actually correct behavior for stairs - you can place against the top half of a upside down stair and against the bottom half of a normal stair
if (mustBePlacedAgainst == Half.TOP && face == Face.DOWN) {
return null;
}
if (mustBePlacedAgainst == Half.BOTTOM && face == Face.UP) {
return null;
}
return new PlaceAgainstData(face, face.vertical ? Half.EITHER : mustBePlacedAgainst, mustSneakWhenPlacingAgainstMe);
}
/**
* The idea here is that I codify all my assumptions in one place instead of having ad hoc checks absolutely everywhere
* <p>
* Example: in PlayerPhysics, I made an assumption that a block will never have a collision block taller than 1.5 blocks (e.g. like a fence)
* When I wrote the code that assumed that, I also added a check here to make sure every block is like that.
* If, in some future update to Minecraft, mojang adds a block that's even taller than a fence, it will be caught here immediately, with a comment saying "playerphysics assumes this is never true"
* This way, I'll know immediately, instead of pathing randomly trying to do something impossible with that new block and it being really confusing and annoying.
*/
public void sanityCheck() {
if (isAir()) {
if (!howCanIBePlaced().isEmpty()) {
throw new IllegalStateException();
}
if (isFullyWalkableTop()) {
throw new IllegalStateException();
}
if (collidesWithPlayer) {
throw new IllegalStateException();
}
}
if (mustBePlacedAgainst == null ^ isAir()) {
throw new IllegalStateException();
}
if (howCanIBePlaced().isEmpty()) {
if (mustBePlacedAgainst != null && !placementLogicNotImplementedYet) {
throw new IllegalStateException();
}
if (playerMustBeHorizontalFacingInOrderToPlaceMe != null || playerMustBeEntityFacingInOrderToPlaceMe != null) {
throw new IllegalStateException();
}
if (canOnlyPlaceAgainst != null) {
throw new IllegalStateException();
}
}
if (isMustSneakWhenPlacingAgainstMe() && mustBePlacedAgainst != Half.EITHER) {
throw new IllegalArgumentException();
}
if ((playerMustBeHorizontalFacingInOrderToPlaceMe != null || playerMustBeEntityFacingInOrderToPlaceMe != null) && mustBePlacedAgainst == null) {
throw new IllegalStateException();
}
if (collisionHeightBlips != null && (collisionHeightBlips > Blip.TALLEST_BLOCK || collisionHeightBlips < 0)) { // playerphysics assumes this is never true
throw new IllegalStateException();
}
if (collidesWithPlayer ^ collisionHeightBlips != null) {
throw new IllegalStateException();
}
if (fullyWalkableTop && !collidesWithPlayer) {
throw new IllegalStateException();
}
if (canPlaceAgainstMe && !collidesWithPlayer) {
throw new IllegalStateException();
}
if (playerMustBeHorizontalFacingInOrderToPlaceMe != null && playerMustBeHorizontalFacingInOrderToPlaceMe.vertical) {
throw new IllegalStateException();
}
if (Main.STRICT_Y && howCanIBePlaced().stream().anyMatch(opt -> opt.against == Face.UP)) {
throw new IllegalStateException();
}
PlaceAgainstData[] data = placeAgainstMe();
if (data.length != Face.NUM_FACES) {
throw new IllegalStateException();
}
boolean any = false;
for (int i = 0; i < Face.NUM_FACES; i++) {
if (data[i] != null) {
if (data[i].against != Face.VALUES[i]) {
throw new IllegalStateException();
}
if (!canPlaceAgainstMe) {
throw new IllegalStateException();
}
any = true;
}
}
if (canPlaceAgainstMe && !any) {
throw new IllegalStateException();
}
if (collisionHeightBlips != null && !fakeLessThanFullHeight) {
for (PlaceAgainstData d : data) {
if (d == null) {
continue;
}
d.streamRelativeToMyself().forEach(hit -> {
if (hit.y > collisionHeightBlips * Blip.RATIO) {
throw new IllegalStateException(d.against + " " + hit.y + " " + collisionHeightBlips * Blip.RATIO);
}
});
}
}
}
static {
new BlockStateCachedDataBuilder().sanityCheck();
}
}

View File

@@ -0,0 +1,236 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* A plane against which this block state can be placed
* <p>
* For a normal block, this will be a full face of a block. In that case, this class is no more than an EnumFacing
* <p>
* For a block like a slab or a stair, this will contain the information that the placement must be against the top or bottom half of the face
* <p>
* For a block like a furnace, this will contain the information that the player must be facing a specific horizontal direction in order to get the desired orientation
* <p>
* For a block like a piston, dispenser, or observer, this will contain the information that be player must pass a combination of: specific relative eye coordinate, specific relative X Z, and specific horizontal facing
*/
public class BlockStatePlacementOption {
/**
* e.g. a torch placed down on the ground is placed against the bottom of "the torch bounding box", so this would be DOWN for the torch
*/
public final Face against;
public final Half half;
public final Optional<Face> playerMustBeHorizontalFacing; // getHorizontalFacing
/**
* IMPORTANT this is the RAW getDirectionFromEntityLiving meaning that it is the OPPOSITE of getHorizontalFacing (when in the horizontal plane)
*/
public final Optional<Face> playerMustBeEntityFacing; // EnumFacing.getDirectionFromEntityLiving, used by piston, dispenser, observer
private BlockStatePlacementOption(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
Objects.requireNonNull(against);
Objects.requireNonNull(half);
this.against = against;
this.half = half;
this.playerMustBeHorizontalFacing = playerMustBeHorizontalFacing;
this.playerMustBeEntityFacing = playerMustBeEntityFacing;
validate(against, half, playerMustBeHorizontalFacing, playerMustBeEntityFacing);
}
/**
* This value must be greater than the face projections.
* <p>
* Otherwise certain stair placements would not work. This is verified in the test
*/
public static final double LOOSE_CENTER_DISTANCE = 0.15;
public List<Raytracer.Raytrace> computeTraceOptions(PlaceAgainstData placingAgainst, int playerSupportingX, int playerFeetBlips, int playerSupportingZ, PlayerVantage vantage, double blockReachDistance) {
if (!BlockStateCachedData.possible(this, placingAgainst)) {
throw new IllegalStateException();
}
if (Main.DEBUG && placingAgainst.streamRelativeToPlace().noneMatch(hit -> hitOk(half, hit))) {
throw new IllegalStateException();
}
List<Vec2d> acceptableVantages = new ArrayList<>();
Vec2d center = Vec2d.HALVED_CENTER.plus(playerSupportingX, playerSupportingZ);
switch (vantage) {
case LOOSE_CENTER: {
acceptableVantages.add(center.plus(LOOSE_CENTER_DISTANCE, 0));
acceptableVantages.add(center.plus(-LOOSE_CENTER_DISTANCE, 0));
acceptableVantages.add(center.plus(0, LOOSE_CENTER_DISTANCE));
acceptableVantages.add(center.plus(0, -LOOSE_CENTER_DISTANCE));
// no break!
} // FALLTHROUGH!
case STRICT_CENTER: {
acceptableVantages.add(center);
break;
}
case SNEAK_BACKPLACE: {
if (playerSupportingX != against.x || playerSupportingZ != against.z) {
throw new IllegalStateException();
}
// in a sneak backplace, there is exactly one location where the player will be
acceptableVantages.add(Vec2d.HALVED_CENTER.plus(0.25 * against.x, 0.25 * against.z));
break;
}
default:
throw new IllegalStateException();
}
// direction from placed block to place-against block = this.against
long blockPlacedAt = 0;
long placeAgainstPos = against.offset(blockPlacedAt);
return sanityCheckTraces(acceptableVantages
.stream()
.map(playerEyeXZ -> new Vec3d(playerEyeXZ.x, Blip.playerEyeFromFeetBlips(playerFeetBlips, placingAgainst.mustSneak), playerEyeXZ.z))
.flatMap(eye ->
placingAgainst.streamRelativeToPlace()
.filter(hit -> hitOk(half, hit))
.filter(hit -> eye.distSq(hit) < blockReachDistance * blockReachDistance)
.filter(hit -> directionOk(eye, hit))
.<Supplier<Optional<Raytracer.Raytrace>>>map(hit -> () -> Raytracer.runTrace(eye, placeAgainstPos, against.opposite(), hit))
)
.collect(Collectors.toList())
.parallelStream() // wrap it like this because flatMap forces .sequential() on the interior child stream, defeating the point
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.sorted()
.collect(Collectors.toList()));
}
public static boolean hitOk(Half half, Vec3d hit) {
if (half == Half.EITHER) {
return true;
} else if (hit.y == 0.1) {
return half == Half.BOTTOM;
} else if (hit.y == 0.5) {
return false; // ambiguous, so force it to pick either down or up
} else if (hit.y == 0.9) {
return half == Half.TOP;
} else {
throw new IllegalStateException();
}
}
/**
* In EnumFacing.getDirectionFromEntityLiving, it checks if the player feet is within 2 blocks of the center of the block to be placed.
* Normally, this is a nonissue, but a problem arises because we are considering hypothetical placements where the player stands at the exact +0.5,+0.5 center of a block.
* In that case, it's possible for our hypothetical to have the player at precisely 2 blocks away, i.e. precisely on the edge of this condition being true or false.
* For that reason, we treat those exact cases as "ambiguous". So, if the distance is within this tolerance of 2 (so, 1.99 to 2.01), we treat it as a "could go either way",
* because when we really get there in-game, floating point inaccuracy could indeed actually make it go either way.
*/
private static final double ENTITY_FACING_TOLERANCE = 0.01;
private boolean directionOk(Vec3d eye, Vec3d hit) {
if (playerMustBeHorizontalFacing.isPresent()) {
return eye.flatDirectionTo(hit) == playerMustBeHorizontalFacing.get();
}
if (playerMustBeEntityFacing.isPresent()) { // handle piston, dispenser, dropper, observer
if (!hit.inOriginUnitVoxel()) {
throw new IllegalStateException();
}
Face entFace = playerMustBeEntityFacing.get();
// see EnumFacing.getDirectionFromEntityLiving
double dx = Math.abs(eye.x - 0.5); // TODO this is changed between 1.12 and 1.19, in 1.19 this should be eye.x-hit.x
double dz = Math.abs(eye.z - 0.5);
if (dx < 2 - ENTITY_FACING_TOLERANCE && dz < 2 - ENTITY_FACING_TOLERANCE) { // < 1.99
if (eye.y < 0) { // eye below placement level = it will be facing down, so this is only okay if we want that
return entFace == Face.DOWN;
}
if (eye.y > 2) { // same for up, if y>2 then it will be facing up
return entFace == Face.UP;
}
} else if (!(dx > 2 + ENTITY_FACING_TOLERANCE || dz > 2 + ENTITY_FACING_TOLERANCE)) { // > 2.01
// this is the ambiguous case, because we are neither unambiguously both-within-2 (previous case), nor unambiguously either-above-two (this elseif condition).
// UP/DOWN are impossible, but that's caught by flat check
if (eye.y < 0 || eye.y > 2) { // this check is okay because player eye height is not an even multiple of blips, therefore there's no way for it to == 0 or == 2, so using > and < is safe
return false; // anything that could cause up/down instead of horizontal is also not allowed sadly
}
} // else we are in unambiguous either-above-two, putting us in simple horizontal mode, so fallthrough to flat condition is correct, yay
return eye.flatDirectionTo(hit) == entFace.opposite();
}
return true;
}
public static BlockStatePlacementOption get(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
BlockStatePlacementOption ret = PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()][Face.OPTS.indexOf(playerMustBeHorizontalFacing)][Face.OPTS.indexOf(playerMustBeEntityFacing)];
if (ret == null) {
throw new IllegalStateException(against + " " + half + " " + playerMustBeHorizontalFacing + " " + playerMustBeEntityFacing);
}
return ret;
}
private static final BlockStatePlacementOption[][][][] PLACEMENT_OPTION_SINGLETON_CACHE;
static {
PLACEMENT_OPTION_SINGLETON_CACHE = new BlockStatePlacementOption[Face.NUM_FACES][Half.values().length][Face.OPTS.size()][Face.OPTS.size()];
for (Face against : Face.VALUES) {
for (Half half : Half.values()) {
for (Optional<Face> horizontalFacing : Face.OPTS) {
for (Optional<Face> entityFacing : Face.OPTS) {
try {
PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()][Face.OPTS.indexOf(horizontalFacing)][Face.OPTS.indexOf(entityFacing)] = new BlockStatePlacementOption(against, half, horizontalFacing, entityFacing);
} catch (RuntimeException ex) {}
}
}
}
}
}
private void validate(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
if (playerMustBeEntityFacing.isPresent() && playerMustBeHorizontalFacing.isPresent()) {
throw new IllegalStateException();
}
if (against.vertical && half != Half.EITHER) {
throw new IllegalArgumentException();
}
if (Main.STRICT_Y && against == Face.UP) {
throw new IllegalStateException();
}
playerMustBeHorizontalFacing.ifPresent(face -> {
if (face.vertical) {
throw new IllegalArgumentException();
}
if (face == against.opposite()) {
throw new IllegalStateException();
}
});
playerMustBeEntityFacing.ifPresent(face -> {
if (half != Half.EITHER) {
throw new IllegalStateException();
}
if (against == face) { // impossible because EnumFacing inverts the horizontal facing AND because the down and up require the eye to be <0 and >2 respectively
throw new IllegalStateException();
}
});
}
private static List<Raytracer.Raytrace> sanityCheckTraces(List<Raytracer.Raytrace> traces) {
if (Main.DEBUG && traces.stream().mapToDouble(Raytracer.Raytrace::centerDistApprox).distinct().count() > 2) {
throw new IllegalStateException();
}
return traces;
}
}

View File

@@ -0,0 +1,98 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
/**
* An area.
* <p>
* More likely than not a cuboid, but who knows :)
*/
public interface Bounds {
@FunctionalInterface
interface BoundsIntsConsumer {
void consume(int x, int y, int z);
}
@FunctionalInterface
interface BoundsLongConsumer {
void consume(long pos);
}
@FunctionalInterface
interface BoundsIntAndLongConsumer {
void consume(int x, int y, int z, long pos);
}
void forEach(BoundsIntsConsumer consumer);
void forEach(BoundsLongConsumer consumer);
void forEach(BoundsIntAndLongConsumer consumer);
// there is no "forEach" for the "index" lookup, because it is always going to just be a loop from 0 to bounds.volume()-1
boolean inRange(int x, int y, int z);
default boolean inRangePos(long pos) {
return inRange(BetterBlockPos.XfromLong(pos), BetterBlockPos.YfromLong(pos), BetterBlockPos.ZfromLong(pos));
}
int volume();
// this must be implemented EXTREMELY efficiently. no integer division allowed! even a hashmap lookup is borderline.
int toIndex(int x, int y, int z); // easy to implement for cuboid, harder for more complicated shapes
default int toIndex(long pos) {
return toIndex(BetterBlockPos.XfromLong(pos), BetterBlockPos.YfromLong(pos), BetterBlockPos.ZfromLong(pos));
}
static void sanityCheckConnectedness(Bounds bounds) {
LongOpenHashSet all = new LongOpenHashSet();
bounds.forEach(all::add);
if (all.size() != bounds.volume()) {
throw new IllegalStateException();
}
long any = all.iterator().nextLong();
LongOpenHashSet reachable = new LongOpenHashSet();
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
queue.enqueue(any);
while (!queue.isEmpty()) {
long pos = queue.dequeueLong();
if (bounds.inRangePos(pos) && reachable.add(pos)) {
for (Face face : Face.VALUES) {
queue.enqueueFirst(face.offset(pos));
}
}
}
LongIterator it = all.iterator();
while (it.hasNext()) {
if (!reachable.contains(it.nextLong())) {
throw new IllegalStateException();
}
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import java.util.stream.IntStream;
import static baritone.api.utils.BetterBlockPos.Y_MASK;
import static baritone.api.utils.BetterBlockPos.Y_SHIFT;
/**
* A mutable class representing a 1x6x1 column of blocks
* <p>
* Mutable because allocations are not on the table for the core solver loop
*/
public class Column {
public long pos; // TODO this isn't set?
//public BlockStateCachedData underUnderneath;
public BlockStateCachedData underneath;
public BlockStateCachedData feet;
public BlockStateCachedData head;
public BlockStateCachedData above;
public BlockStateCachedData aboveAbove;
public PlayerPhysics.VoxelResidency voxelResidency;
public Integer feetBlips;
public void initFrom(long pos, WorldState worldState, SolverEngineInput engineInput) {
this.pos = pos;
//this.underUnderneath = engineInput.at((pos + DOWN_2) & BetterBlockPos.POST_ADDITION_MASK, worldState);
this.underneath = engineInput.at((pos + DOWN_1) & BetterBlockPos.POST_ADDITION_MASK, worldState);
this.feet = engineInput.at(pos, worldState);
this.head = engineInput.at((pos + UP_1) & BetterBlockPos.POST_ADDITION_MASK, worldState);
this.above = engineInput.at((pos + UP_2) & BetterBlockPos.POST_ADDITION_MASK, worldState);
this.aboveAbove = engineInput.at((pos + UP_3) & BetterBlockPos.POST_ADDITION_MASK, worldState);
init();
}
public void init() {
this.voxelResidency = PlayerPhysics.canPlayerStand(underneath, feet);
this.feetBlips = boxNullable(PlayerPhysics.determinePlayerRealSupportLevel(underneath, feet, voxelResidency));
if (feetBlips != null && !playerCanExistAtFootBlip(feetBlips)) { // TODO is this the correct way to handle head collision?
voxelResidency = PlayerPhysics.VoxelResidency.IMPOSSIBLE_WITHOUT_SUFFOCATING; // TODO this is a misuse of this enum value i think
feetBlips = null;
}
}
public boolean playerCanExistAtFootBlip(int blipWithinFeet) {
if (head.collidesWithPlayer) {
return false;
}
if (PlayerPhysics.protrudesIntoThirdBlock(blipWithinFeet) && above.collidesWithPlayer) {
return false;
}
return true;
}
public boolean okToSneakIntoHereAtHeight(int blips) {
return playerCanExistAtFootBlip(blips) // no collision at head level
&& PlayerPhysics.highestCollision(underneath, feet) < blips; // and at foot level, we only collide strictly below where the feet will be
}
public boolean standing() {
return feetBlips != null;
}
public static final long DOWN_3 = (Y_MASK - 2) << Y_SHIFT;
public static final long DOWN_2 = (Y_MASK - 1) << Y_SHIFT;
public static final long DOWN_1 = Y_MASK << Y_SHIFT;
public static final long UP_1 = 1L << Y_SHIFT;
public static final long UP_2 = 2L << Y_SHIFT;
public static final long UP_3 = 3L << Y_SHIFT;
static {
if (DOWN_3 != BetterBlockPos.toLong(0, -3, 0)) {
throw new IllegalStateException();
}
if (DOWN_2 != BetterBlockPos.toLong(0, -2, 0)) {
throw new IllegalStateException();
}
if (DOWN_1 != BetterBlockPos.toLong(0, -1, 0)) {
throw new IllegalStateException();
}
if (UP_1 != BetterBlockPos.toLong(0, 1, 0)) {
throw new IllegalStateException();
}
if (UP_2 != BetterBlockPos.toLong(0, 2, 0)) {
throw new IllegalStateException();
}
if (UP_3 != BetterBlockPos.toLong(0, 3, 0)) {
throw new IllegalStateException();
}
}
private static final Integer[] BLIPS = IntStream.range(-1, Blip.FULL_BLOCK).boxed().toArray(Integer[]::new);
static {
BLIPS[0] = null;
}
private static Integer boxNullable(int blips) {
return BLIPS[blips + 1]; // map -1 to [0] which is null
}
}

View File

@@ -0,0 +1,94 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import java.util.OptionalInt;
public class CountingSurface extends NavigableSurface {
public CountingSurface(int x, int y, int z) {
super(x, y, z, Attachment::new, $ -> new Attachment());
}
private static class Attachment {
public final int surfaceSize;
public Attachment(Object a, Object b) {
this((Attachment) a, (Attachment) b);
}
public Attachment(Attachment a, Attachment b) {
this.surfaceSize = a.surfaceSize + b.surfaceSize;
}
public Attachment() {
this.surfaceSize = 1;
}
@Override
public boolean equals(Object o) { // used as performance optimization in RedBlackNode to avoid augmenting unchanged attachments
if (this == o) {
return true;
}
if (!(o instanceof Attachment)) {
return false;
}
Attachment that = (Attachment) o;
return surfaceSize == that.surfaceSize;
}
@Override
public int hashCode() {
return surfaceSize;
}
}
public OptionalInt surfaceSize(BetterBlockPos pos) { // how big is the navigable surface from here? how many distinct coordinates can i walk to (in the future, the augmentation will probably have a list of those coordinates or something?)
Object data = getComponentAugmentation(pos);
if (data != null) { // i disagree with the intellij suggestion here i think it makes it worse
return OptionalInt.of(((Attachment) data).surfaceSize);
} else {
return OptionalInt.empty();
}
}
public int requireSurfaceSize(int x, int y, int z) {
return surfaceSize(new BetterBlockPos(x, y, z)).getAsInt();
}
private void placeOrRemoveBlock(BetterBlockPos where, boolean place) {
setBlock(where.toLong(), place ? FakeStates.SOLID : FakeStates.AIR);
}
public void placeBlock(BetterBlockPos where) {
placeOrRemoveBlock(where, true);
}
public void placeBlock(int x, int y, int z) {
placeBlock(new BetterBlockPos(x, y, z));
}
public void removeBlock(BetterBlockPos where) {
placeOrRemoveBlock(where, false);
}
public void removeBlock(int x, int y, int z) {
removeBlock(new BetterBlockPos(x, y, z));
}
}

View File

@@ -0,0 +1,165 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
/**
* Bounding box of a cuboid
* <p>
* Basically just a lot of helper util methods lol
*/
public class CuboidBounds implements Bounds {
public final int sizeX;
public final int sizeY;
public final int sizeZ;
private final int sizeXMinusOne;
private final int sizeYMinusOne;
private final int sizeZMinusOne;
public final int size;
private final int sizeMinusOne;
public CuboidBounds(int sizeX, int sizeY, int sizeZ) {
this.sizeX = sizeX;
this.sizeY = sizeY;
this.sizeZ = sizeZ;
this.sizeXMinusOne = sizeX - 1;
this.sizeYMinusOne = sizeY - 1;
this.sizeZMinusOne = sizeZ - 1;
this.size = sizeX * sizeY * sizeZ;
this.sizeMinusOne = size - 1;
if (Main.SLOW_DEBUG) {
sanityCheck();
}
}
@Override
public int toIndex(int x, int y, int z) {
if (Main.DEBUG && !inRange(x, y, z)) {
throw new IllegalStateException();
}
return (x * sizeY + y) * sizeZ + z;
}
@Override
public boolean inRange(int x, int y, int z) {
return inRangeBranchless(x, y, z);
}
@Override
public int volume() {
return size;
}
@Deprecated
public boolean inRangeBranchy(int x, int y, int z) { // benchmarked: approx 4x slower than branchless
return (x >= 0) && (x < sizeX) && (y >= 0) && (y < sizeY) && (z >= 0) && (z < sizeZ);
}
public boolean inRangeBranchless(int x, int y, int z) {
return (x | y | z | (sizeXMinusOne - x) | (sizeYMinusOne - y) | (sizeZMinusOne - z)) >= 0;
}
public boolean inRangeBranchless2(int x, int y, int z) {
return (x | y | z | ((sizeX - 1) - x) | ((sizeY - 1) - y) | ((sizeZ - 1) - z)) >= 0;
}
public boolean inRangeBranchless3(int x, int y, int z) {
return (x | y | z | (sizeX - (x + 1)) | (sizeY - (y + 1)) | (sizeZ - (z + 1))) >= 0;
}
public boolean inRangeBranchless4(int x, int y, int z) {
return (x | y | z | ((sizeX - x) - 1) | ((sizeY - y) - 1) | ((sizeZ - z) - 1)) >= 0;
}
public boolean inRangeIndex(int index) {
return (index | (sizeMinusOne - index)) >= 0;
}
@Override
public void forEach(BoundsIntsConsumer consumer) {
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
for (int x = 0; x < sizeX; x++) {
for (int y = 0; y < sizeY; y++) {
for (int z = 0; z < sizeZ; z++) {
consumer.consume(x, y, z);
}
}
}
}
@Override
public void forEach(BoundsLongConsumer consumer) {
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
for (int x = 0; x < sizeX; x++) {
for (int y = 0; y < sizeY; y++) {
for (int z = 0; z < sizeZ; z++) {
consumer.consume(BetterBlockPos.toLong(x, y, z));
}
}
}
}
@Override
public void forEach(BoundsIntAndLongConsumer consumer) {
int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ;
for (int x = 0; x < sizeX; x++) {
for (int y = 0; y < sizeY; y++) {
for (int z = 0; z < sizeZ; z++) {
consumer.consume(x, y, z, BetterBlockPos.toLong(x, y, z));
}
}
}
}
public void sanityCheck() {
if (sizeY > 256) {
throw new IllegalStateException();
}
long chk = ((long) sizeX) * ((long) sizeY) * ((long) sizeZ);
if (chk != (long) size) {
throw new IllegalStateException();
}
int index = 0;
for (int x = 0; x < sizeX; x++) {
for (int y = 0; y < sizeY; y++) {
for (int z = 0; z < sizeZ; z++) {
if (!inRange(x, y, z)) {
throw new IllegalStateException();
}
if (toIndex(x, y, z) != index) {
throw new IllegalStateException();
}
index++;
}
}
}
if (index != size) {
throw new IllegalStateException();
}
if (inRange(-1, 0, 0) || inRange(0, -1, 0) || inRange(0, 0, -1)) {
throw new IllegalStateException();
}
if (inRange(sizeX, 0, 0) || inRange(0, sizeY, 0) || inRange(0, 0, sizeZ)) {
throw new IllegalStateException();
}
Bounds.sanityCheckConnectedness(this);
}
}

View File

@@ -0,0 +1,126 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.*;
import java.util.ArrayList;
import java.util.List;
/**
* Some initial checks on the schematic
* <p>
* The intent is to provide reasonable error messages, which we can do by catching common cases as early as possible
* <p>
* So that it's an actual comprehensible error **that tells you where the problem is** instead of just "pathing failed"
*/
public class DependencyGraphAnalyzer {
/**
* Just a simple check to make sure that everything is placeable.
* <p>
* Mostly for my own testing because every Minecraft block is placeable, and if the schematic has something weird
* and funky, it should be caught earlier anyway.
*/
public static void prevalidate(PlaceOrderDependencyGraph graph) {
List<String> locs = new ArrayList<>();
graph.bounds().forEach(pos -> {
if (graph.airTreatedAsScaffolding(pos)) {
// completely fine to, for example, have an air pocket with non-place-against-able stuff all around it
return;
}
for (Face face : Face.VALUES) {
if (graph.incomingEdge(pos, face)) {
return;
}
}
locs.add(BetterBlockPos.fromLong(pos).toString());
});
if (!locs.isEmpty()) {
throw new IllegalStateException("Unplaceable from any side: " + cuteTrim(locs));
}
// TODO instead of cuteTrim have a like SpecificBlockPositionsImpossibleException that this throws, and then later, an enclosing function can give the option to reset those locations to air
}
/**
* Search from all exterior nodes breadth-first to ensure that, theoretically, everything is reachable.
* <p>
* This is NOT a sufficient test, because later we are going to ensure that everything is scaffold-placeable which
* requires a single root node at the bottom.
*/
public static void prevalidateExternalToInteriorSearch(PlaceOrderDependencyGraph graph) {
LongList edgeBegins = new LongArrayList();
graph.bounds().forEach(pos -> {
for (Face face : Face.VALUES) {
if (graph.incomingEdgePermitExterior(pos, face) && !graph.incomingEdge(pos, face)) {
// this block is placeable from the exterior of the schematic!
edgeBegins.add(pos); // this will intentionally put the top of the schematic at the front
}
}
});
LongSet reachable = searchGraph(edgeBegins, graph::outgoingEdge);
List<String> locs = new ArrayList<>();
graph.bounds().forEach(pos -> {
if (graph.airTreatedAsScaffolding(pos)) {
// same as previous validation
return;
}
if (!reachable.contains(pos)) {
locs.add(BetterBlockPos.fromLong(pos).toString());
}
});
if (!locs.isEmpty()) {
throw new IllegalStateException("Placeable, in theory, but in practice there is no valid path from the exterior to it: " + cuteTrim(locs));
}
}
@FunctionalInterface
interface EdgeTester {
boolean hasEdge(long from, Face dir);
}
public static LongSet searchGraph(LongCollection origin, EdgeTester edgeTester) {
LongOpenHashSet reachable = new LongOpenHashSet();
LongArrayFIFOQueue queue = new LongArrayFIFOQueue(origin.size());
LongIterator it = origin.iterator();
while (it.hasNext()) {
queue.enqueue(it.nextLong());
}
while (!queue.isEmpty()) {
long pos = queue.dequeueLong();
if (reachable.add(pos)) {
for (Face face : Face.VALUES) {
if (edgeTester.hasEdge(pos, face)) {
queue.enqueueFirst(face.offset(pos));
}
}
}
}
return reachable;
}
private static List<String> cuteTrim(List<String> pos) {
if (pos.size() <= 20) {
return pos;
}
pos = pos.subList(0, 20);
pos.set(pos.size() - 1, "...");
return pos;
}
}

View File

@@ -0,0 +1,499 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.HashCommon;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.util.*;
/**
* A mutable addition of scaffolding blocks to a schematic
* <p>
* Contains a set of coordinates that are "air" in the schematic, but we are going to put scaffolding throwaway blocks there
* <p>
* Maintains and incrementally updates a collapsed dependency graph, which is the block placement dependency graph reduced to directed acyclic graph form by way of collapsing all strongly connected components into single nodes
* <p>
* Helper class, only intended to be used within Scaffolder
*/
public class DependencyGraphScaffoldingOverlay {
private final PlaceOrderDependencyGraph delegate;
private final LongOpenHashSet scaffoldingAdded;
private final CollapsedDependencyGraph collapsedGraph;
public DependencyGraphScaffoldingOverlay(PlaceOrderDependencyGraph delegate) {
this.delegate = delegate;
this.scaffoldingAdded = new LongOpenHashSet();
this.collapsedGraph = new CollapsedDependencyGraph(TarjansAlgorithm.run(this));
//System.out.println("Num components: " + collapsedGraph.components.size());
}
public boolean outgoingEdge(long pos, Face face) {
if (overrideOff(pos)) {
return false;
}
if (overrideOff(face.offset(pos))) {
return false;
}
return delegate.outgoingEdge(pos, face);
}
public boolean incomingEdge(long pos, Face face) {
if (overrideOff(pos)) {
return false;
}
if (overrideOff(face.offset(pos))) {
return false;
}
return delegate.incomingEdge(pos, face);
}
public boolean hypotheticalScaffoldingIncomingEdge(long pos, Face face) {
return delegate.incomingEdge(pos, face);
}
public Bounds bounds() {
return delegate.bounds();
}
private boolean overrideOff(long pos) {
return bounds().inRangePos(pos) && air(pos);
}
public boolean real(long pos) {
return !air(pos);
}
public void forEachReal(Bounds.BoundsLongConsumer consumer) {
bounds().forEach(pos -> {
if (real(pos)) {
consumer.consume(pos);
}
});
}
public boolean air(long pos) {
return delegate.airTreatedAsScaffolding(pos) && !scaffoldingAdded.contains(pos);
}
public void enable(long pos) {
if (!delegate.airTreatedAsScaffolding(pos)) {
throw new IllegalArgumentException();
}
if (!scaffoldingAdded.add(pos)) {
throw new IllegalArgumentException();
}
collapsedGraph.incrementalUpdate(pos);
if (Main.SLOW_DEBUG) {
//System.out.println(collapsedGraph.posToComponent.containsKey(pos) + " " + scaffoldingAdded.contains(pos) + " " + real(pos));
recheckEntireCollapsedGraph();
}
}
public void recheckEntireCollapsedGraph() {
checkEquality(collapsedGraph, new CollapsedDependencyGraph(TarjansAlgorithm.run(this)));
//System.out.println("Checked equality");
//System.out.println("Num connected components: " + collapsedGraph.components.size());
}
public LongSets.UnmodifiableSet scaffolding() {
return (LongSets.UnmodifiableSet) LongSets.unmodifiable(scaffoldingAdded);
}
public BlockStateCachedData data(long pos) {
if (Main.DEBUG && !real(pos)) {
throw new IllegalStateException();
}
return delegate.data(pos);
}
/**
* Remember that this returns a collapsed graph that will be updated in-place as positions are enabled. It does not return a copy.
*/
public CollapsedDependencyGraph getCollapsedGraph() {
return collapsedGraph;
}
public class CollapsedDependencyGraph {
private int nextComponentID;
private final Int2ObjectOpenHashMap<CollapsedDependencyGraphComponent> components;
private final Long2ObjectOpenHashMap<CollapsedDependencyGraphComponent> posToComponent;
private CollapsedDependencyGraph(TarjansAlgorithm.TarjansResult partition) {
components = new Int2ObjectOpenHashMap<>();
for (int i = 0; i < partition.numComponents(); i++) {
addComponent();
}
posToComponent = new Long2ObjectOpenHashMap<>();
forEachReal(pos -> {
CollapsedDependencyGraphComponent component = components.get(partition.getComponent(pos));
component.positions.add(pos);
posToComponent.put(pos, component);
});
forEachReal(pos -> {
for (Face face : Face.VALUES) {
if (outgoingEdge(pos, face)) {
CollapsedDependencyGraphComponent src = posToComponent.get(pos);
CollapsedDependencyGraphComponent dst = posToComponent.get(face.offset(pos));
if (src != dst) {
src.outgoingEdges.add(dst);
dst.incomingEdges.add(src);
}
}
}
});
if (Main.SLOW_DEBUG) {
sanityCheck();
}
}
public Int2ObjectMap<CollapsedDependencyGraphComponent> getComponents() {
return Int2ObjectMaps.unmodifiable(components);
}
public Long2ObjectMap<CollapsedDependencyGraphComponent> getComponentLocations() {
return Long2ObjectMaps.unmodifiable(posToComponent);
}
public OptionalInt lastComponentID() {
return nextComponentID == 0 ? OptionalInt.empty() : OptionalInt.of(nextComponentID - 1);
}
private CollapsedDependencyGraphComponent addComponent() {
CollapsedDependencyGraphComponent component = new CollapsedDependencyGraphComponent(nextComponentID);
components.put(component.id, component);
nextComponentID++;
return component;
}
private CollapsedDependencyGraphComponent mergeInto(CollapsedDependencyGraphComponent child, CollapsedDependencyGraphComponent parent) {
if (child.deleted() || parent.deleted()) {
throw new IllegalStateException();
}
if (child == parent) {
throw new IllegalStateException();
}
if (child.positions.size() > parent.positions.size() || (child.positions.size() == parent.positions.size() && child.id < parent.id)) {
return mergeInto(parent, child);
}
if (Main.DEBUG) {
//System.out.println("Merging " + child.index + " into " + parent.index);
}
child.incomingEdges.forEach(intoChild -> {
intoChild.outgoingEdges.remove(child);
if (intoChild == parent) {
return;
}
intoChild.outgoingEdges.add(parent);
parent.incomingEdges.add(intoChild);
});
child.outgoingEdges.forEach(outOfChild -> {
outOfChild.incomingEdges.remove(child);
if (outOfChild == parent) {
return;
}
outOfChild.incomingEdges.add(parent);
parent.outgoingEdges.add(outOfChild);
});
parent.positions.addAll(child.positions);
LongIterator it = child.positions.iterator();
while (it.hasNext()) {
long pos = it.nextLong();
posToComponent.put(pos, parent);
}
components.remove(child.id);
child.deletedInto = parent;
// TODO clear and trim child.positions? maybe unnecessary because nothing should retain a reference to child for longer than a moment
return parent;
}
private void incrementalEdgeAddition(long src, long dst) { // TODO put in a param here like "bias" that determines which of the two components gets to eat the other if they are of the same size? could help with the scaffolder if we could guarantee that no new components would be added without cause
CollapsedDependencyGraphComponent srcComponent = posToComponent.get(src);
CollapsedDependencyGraphComponent dstComponent = posToComponent.get(dst);
if (srcComponent == dstComponent) { // already strongly connected
return;
}
if (srcComponent.outgoingEdges.contains(dstComponent)) { // we already know about this edge
return;
}
List<List<CollapsedDependencyGraphComponent>> paths = new ArrayList<>();
if (!srcComponent.incomingEdges.isEmpty() && pathExists(dstComponent, srcComponent, paths) > 0) {
CollapsedDependencyGraphComponent survivor = srcComponent;
for (List<CollapsedDependencyGraphComponent> path : paths) {
if (path.get(0) != srcComponent || path.get(path.size() - 1) != dstComponent) {
throw new IllegalStateException();
}
for (int i = 1; i < path.size(); i++) {
if (path.get(i).deleted() || path.get(i) == survivor) { // two different paths to the same goal, only merge the components once, so skip is already survivor or deleted
continue;
}
survivor = mergeInto(survivor, path.get(i));
}
}
// can't run sanityCheck after each mergeInto because it could leave a 2-way connection between components as an intermediary state while collapsing
if (Main.SLOW_DEBUG) {
sanityCheck();
}
return;
}
srcComponent.outgoingEdges.add(dstComponent);
dstComponent.incomingEdges.add(srcComponent);
}
private int pathExists(CollapsedDependencyGraphComponent src, CollapsedDependencyGraphComponent dst, List<List<CollapsedDependencyGraphComponent>> paths) {
if (src == dst) {
paths.add(new ArrayList<>(Collections.singletonList(src)));
return 1;
}
if (Main.DEBUG && dst.incomingEdges.isEmpty()) {
throw new IllegalStateException();
}
if (Main.STRICT_Y && src.y() > dst.y()) {
return 0; // no downward edges in strict_y mode
}
int numAdded = 0;
for (CollapsedDependencyGraphComponent nxt : src.outgoingEdges) {
int cnt = pathExists(nxt, dst, paths);
if (cnt > 0) {
for (int i = 0; i < cnt; i++) {
paths.get(paths.size() - 1 - i).add(src);
}
numAdded += cnt;
}
}
return numAdded;
}
private void incrementalUpdate(long pos) {
if (posToComponent.containsKey(pos)) {
throw new IllegalStateException();
}
CollapsedDependencyGraphComponent component = addComponent();
component.positions.add(pos);
posToComponent.put(pos, component);
if (Main.SLOW_DEBUG) {
sanityCheck();
}
//System.out.println("Incremental " + pos);
//System.out.println("Pos core " + posToComponent.get(pos).index);
for (Face face : Face.VALUES) {
if (outgoingEdge(pos, face)) {
//System.out.println("Pos outgoing edge " + face + " goes to " + posToComponent.get(face.offset(pos)).index);
incrementalEdgeAddition(pos, face.offset(pos));
}
if (incomingEdge(pos, face)) {
//System.out.println("Pos incoming edge " + face + " comes from " + posToComponent.get(face.offset(pos)).index);
incrementalEdgeAddition(face.offset(pos), pos);
}
}
if (Main.SLOW_DEBUG) {
sanityCheck();
}
}
public class CollapsedDependencyGraphComponent {
private final int id;
private final int hash;
private final LongOpenHashSet positions = new LongOpenHashSet();
private final Set<CollapsedDependencyGraphComponent> outgoingEdges = new ObjectOpenHashSet<>();
private final Set<CollapsedDependencyGraphComponent> incomingEdges = new ObjectOpenHashSet<>();
// if i change ^^ that "Set" to "ObjectOpenHashSet" it actually makes the bench about 15% SLOWER?!?!?
private int y = -1;
private CollapsedDependencyGraphComponent deletedInto;
private final Set<CollapsedDependencyGraphComponent> unmodifiableOutgoing = Collections.unmodifiableSet(outgoingEdges);
private final Set<CollapsedDependencyGraphComponent> unmodifiableIncoming = Collections.unmodifiableSet(incomingEdges);
private CollapsedDependencyGraphComponent(int id) {
this.id = id;
this.hash = HashCommon.murmurHash3(id);
}
@Override
public int hashCode() {
return hash; // no need to enter native code to get a hashCode, that saves a few nanoseconds
}
private int y() {
if (!Main.STRICT_Y || positions.isEmpty()) {
throw new IllegalStateException();
}
if (y == -1) { // TODO won't work in 1.17+ lol
y = BetterBlockPos.YfromLong(positions.iterator().nextLong());
if (y == -1) {
throw new IllegalStateException();
}
}
return y;
}
public boolean deleted() {
return deletedInto != null;
}
public CollapsedDependencyGraphComponent deletedIntoRecursive() { // what cid was this merged into that caused it to be deleted
if (!deleted()) {
return this;
}
return deletedInto = deletedInto.deletedIntoRecursive();
}
public LongSet getPositions() {
if (deleted()) {
throw new IllegalStateException();
}
return LongSets.unmodifiable(positions);
}
public Set<CollapsedDependencyGraphComponent> getIncoming() {
if (deleted()) {
throw new IllegalStateException();
}
return unmodifiableIncoming;
}
public Set<CollapsedDependencyGraphComponent> getOutgoing() {
if (deleted()) {
throw new IllegalStateException();
}
return unmodifiableOutgoing;
}
@Override
public String toString() {
if (!Main.DEBUG) {
throw new IllegalStateException();
}
return "cid" + id;
}
}
private void sanityCheck() {
LongOpenHashSet inComponents = new LongOpenHashSet();
for (int componentID : components.keySet()) {
CollapsedDependencyGraphComponent component = components.get(componentID);
if (component.id != componentID) {
throw new IllegalStateException();
}
if (component.incomingEdges.contains(component) || component.outgoingEdges.contains(component)) {
throw new IllegalStateException(component.id + "");
}
if (component.positions.isEmpty()) {
throw new IllegalStateException();
}
Integer y = Main.STRICT_Y ? component.y() : null;
for (CollapsedDependencyGraphComponent out : component.outgoingEdges) {
if (Main.STRICT_Y && out.y() < y) {
throw new IllegalStateException();
}
if (!out.incomingEdges.contains(component)) {
throw new IllegalStateException();
}
if (component.incomingEdges.contains(out)) {
throw new IllegalStateException(out.id + " is both an incoming AND and outgoing of " + component.id);
}
}
for (CollapsedDependencyGraphComponent in : component.incomingEdges) {
if (Main.STRICT_Y && in.y() > y) {
throw new IllegalStateException();
}
if (!in.outgoingEdges.contains(component)) {
throw new IllegalStateException(in.id + " is an incoming edge of " + component.id + " but it doesn't have that as an outgoing edge");
}
if (component.outgoingEdges.contains(in)) {
throw new IllegalStateException();
}
}
LongIterator it = component.positions.iterator();
while (it.hasNext()) {
long l = it.nextLong();
if (posToComponent.get(l) != component) {
throw new IllegalStateException();
}
if (Main.STRICT_Y && BetterBlockPos.YfromLong(l) != y) {
throw new IllegalStateException();
}
if (!real(l)) {
throw new IllegalStateException();
}
}
inComponents.addAll(component.positions);
}
if (!inComponents.equals(posToComponent.keySet())) {
for (long l : posToComponent.keySet()) {
if (!inComponents.contains(l)) {
System.out.println(l);
System.out.println(posToComponent.get(l).id);
System.out.println(posToComponent.get(l).positions.contains(l));
System.out.println(posToComponent.get(l).deleted());
System.out.println(components.containsValue(posToComponent.get(l)));
throw new IllegalStateException(l + " is in posToComponent but not actually in any component");
}
}
throw new IllegalStateException("impossible");
}
}
}
private void checkEquality(CollapsedDependencyGraph a, CollapsedDependencyGraph b) {
if (a.components.size() != b.components.size()) {
throw new IllegalStateException(a.components.size() + " " + b.components.size());
}
if (a.posToComponent.size() != b.posToComponent.size()) {
throw new IllegalStateException();
}
if (!a.posToComponent.keySet().equals(b.posToComponent.keySet())) {
throw new IllegalStateException();
}
a.sanityCheck();
b.sanityCheck();
Int2IntOpenHashMap aToB = new Int2IntOpenHashMap();
for (int key : a.components.keySet()) {
aToB.put(key, b.posToComponent.get(a.components.get(key).positions.iterator().nextLong()).id);
}
for (int i : a.components.keySet()) {
int bInd = aToB.get(i);
if (!a.components.get(i).positions.equals(b.components.get(bInd).positions)) {
throw new IllegalStateException();
}
for (List<Set<CollapsedDependencyGraph.CollapsedDependencyGraphComponent>> toCompare : Arrays.asList(
Arrays.asList(a.components.get(i).incomingEdges, b.components.get(bInd).incomingEdges),
Arrays.asList(a.components.get(i).outgoingEdges, b.components.get(bInd).outgoingEdges)
)) {
Set<CollapsedDependencyGraph.CollapsedDependencyGraphComponent> aEdges = toCompare.get(0);
Set<CollapsedDependencyGraph.CollapsedDependencyGraphComponent> bEdges = toCompare.get(1);
if (aEdges.size() != bEdges.size()) {
throw new IllegalStateException();
}
for (CollapsedDependencyGraph.CollapsedDependencyGraphComponent dst : aEdges) {
if (!bEdges.contains(b.components.get(aToB.get(dst.id)))) {
throw new IllegalStateException();
}
}
}
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import baritone.builder.DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph.CollapsedDependencyGraphComponent;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.stream.Collectors;
public enum DijkstraScaffolder implements IScaffolderStrategy {
INSTANCE;
@Override
public LongList scaffoldTo(CollapsedDependencyGraphComponent root, DependencyGraphScaffoldingOverlay overlayGraph) {
// TODO what if this root is unreachable, e.g. it's lower in STRICT_Y mode?
Set<CollapsedDependencyGraphComponent> exclusiveDescendents = new ObjectOpenHashSet<>();
walkAllDescendents(root, exclusiveDescendents);
exclusiveDescendents.remove(root);
PriorityQueue<ScaffoldingSearchNode> openSet = new PriorityQueue<>(Comparator.comparingInt(node -> node.costSoFar));
Long2ObjectOpenHashMap<ScaffoldingSearchNode> nodeMap = new Long2ObjectOpenHashMap<>();
LongIterator it = root.getPositions().iterator();
while (it.hasNext()) {
long l = it.nextLong();
nodeMap.put(l, new ScaffoldingSearchNode(l));
}
System.out.println(root.getPositions().stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()));
openSet.addAll(nodeMap.values());
while (!openSet.isEmpty()) {
ScaffoldingSearchNode node = openSet.poll();
CollapsedDependencyGraphComponent tentativeComponent = overlayGraph.getCollapsedGraph().getComponentLocations().get(node.pos);
if (tentativeComponent != null) {
if (exclusiveDescendents.contains(tentativeComponent)) { // TODO is exclusiveDescendants even valid? returning a route into one of the descendants, if it's on the top of the heap, is valid because it closes a loop and the next dijkstra can start from there? perhaps there's no need to treat descendant interactions differently from any other non-root component? EDIT: maybe it's good to prevent adding useless scaffolding that closes loops for no good reason?
// have gone back onto a descendent of this node
// sadly this can happen even at the same Y level even in Y_STRICT mode due to orientable blocks forming a loop
continue; // TODO does this need to be here? can I expand THROUGH an unrelated component? probably requires testing, this is quite a mind bending possibility. cost should be zero i think?
} else {
// found a path to a component that isn't a descendent of the root
if (tentativeComponent != root) { // but if it IS the root, then we're just on our first loop iteration, we are far from done
return reconstructPathTo(node); // all done! found a path to a component unrelated to this one, meaning we have successfully connected this part of the build with scaffolding back to the rest of it
// TODO scaffolder strategy should be reworked into a coroutine-like format to decomposes a persistent dijkstra that retains the openset and nodemap between scaffolder component connections. each scaffoldersearchnode would need a persistent progeny (source component) and new combined components would need to be introduced as they're created. then the search can be simultaneous. this would solve the problem of potential incorrect selection of root node, as all possible root nodes are expanded at once
}
}
}
for (Face face : Face.VALUES) {
if (overlayGraph.hypotheticalScaffoldingIncomingEdge(node.pos, face)) { // we don't have to worry about an incoming edge going into the frontier set because the root component is strongly connected and has no incoming edges from other SCCs, therefore any and all incoming edges will come from hypothetical scaffolding air locations
long neighborPos = face.offset(node.pos);
int newCost = node.costSoFar + edgeCost(face); // TODO future edge cost should include an added modifier for if neighborPos is in a favorable or unfavorable position e.g. above / under a diagonal depending on if map art or not
ScaffoldingSearchNode existingNode = nodeMap.get(neighborPos);
if (existingNode != null) {
// it's okay if neighbor isn't marked as "air" in the overlay - that's what we want to find - a path to another component
// however, we can't consider neighbors within the same component as a solution, clearly
// we can accomplish this and kill two birds with one stone by skipping all nodes already in the node map
// any position in the initial frontier is clearly in the node map, but also any node that has already been considered
// this prevents useless cycling of equivalent paths
// this is okay because all paths are equivalent, so there is no possible way to find a better path (because currently it's a fixed value for horizontal / vertical movements)
if (existingNode.costSoFar > newCost) { // initialization nodes will have costSoFar = 0 as a base case
// note that obviously there is a loopback possibility: search one block north then one block south, you'll run into the same node again. that's fine - "costSoFar < newCost" doesn't mean anything
// same for diagonals: one block north then one block down, versus one block down then one block north. that's also fine - "costSoFar == newCost" doesn't mean anything
System.out.println(BetterBlockPos.fromLong(node.pos) + " to " + BetterBlockPos.fromLong(neighborPos) + " " + existingNode.costSoFar + " " + newCost + " " + root.getPositions().contains(node.pos) + " " + root.getPositions().contains(neighborPos) + " " + reconstructPathTo(node).stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()) + " " + reconstructPathTo(existingNode).stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()));
throw new IllegalStateException();
}
// TODO if root spans more than 1 y level, then this assumption is not correct because edgeCost is different for a horizontal vs vertical face, meaning that a neighbor can have different cost routes if both sideways and up are part of the root component
continue; // nothing to do - we already have an equal-or-better path to this location
}
ScaffoldingSearchNode newNode = new ScaffoldingSearchNode(neighborPos);
newNode.costSoFar = newCost;
newNode.prev = node;
nodeMap.put(newNode.pos, newNode);
openSet.add(newNode);
}
}
}
return null;
}
private static void walkAllDescendents(CollapsedDependencyGraphComponent root, Set<CollapsedDependencyGraphComponent> set) {
set.add(root);
for (CollapsedDependencyGraphComponent component : root.getOutgoing()) {
walkAllDescendents(component, set);
}
}
private static LongList reconstructPathTo(ScaffoldingSearchNode end) {
LongList path = new LongArrayList();
while (end != null) {
path.add(end.pos);
end = end.prev;
}
return path;
}
public static int edgeCost(Face face) {
if (Main.STRICT_Y && face == Face.UP) {
throw new IllegalStateException();
}
// gut feeling: give slight bias to moving horizontally
// that will influence it to create horizontal bridges more often than vertical pillars
// horizontal bridges are easier to maneuver around and over
if (face.y == 0) {
return 1;
}
return 2;
}
private static class ScaffoldingSearchNode {
private final long pos;
private int costSoFar;
private ScaffoldingSearchNode prev;
private ScaffoldingSearchNode(long pos) {
this.pos = pos;
}
}
}

View File

@@ -0,0 +1,940 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.IntStream;
public class EulerTourForest {
static long parentWalks;
static long parentCalls;
// https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf
// https://u.cs.biu.ac.il/~rodittl/p723-holm.pdf
// https://web.archive.org/web/20180725100607/https://infoscience.epfl.ch/record/99353/files/HenzingerK99.pdf
// https://en.wikipedia.org/wiki/Dynamic_connectivity#The_Level_structure
public final BSTNode[] loopbacks; // a (v,v) fake edge is created per vertex and maintained at the appropriate location in the tree, to allow fast lookups of where "v" is, without having to rely on the presence or absence of tree edges connected to v
public EulerTourForest(int n) {
this.loopbacks = IntStream.range(0, n).mapToObj(SplayNode::new).toArray(BSTNode[]::new);
}
public TreeEdge link(int vertA, int vertB) {
if (connected(vertA, vertB)) {
throw new IllegalStateException();
}
BSTNode outgoing = new SplayNode(vertA, vertB);
BSTNode incoming = new SplayNode(vertB, vertA);
BSTNode.barrelRollToLowest(loopbacks[vertA]);
BSTNode.barrelRollToLowest(loopbacks[vertB]);
BSTNode.concatenate(loopbacks[vertA], outgoing); // (a,a) ... (a,b)
BSTNode.concatenate(outgoing, loopbacks[vertB]); // (a,a) ... (a,b) (b,b) ...
BSTNode.concatenate(loopbacks[vertB], incoming); // (a,a) ... (a,b) (b,b) ... (b,a)
return new TreeEdge(incoming, outgoing);
}
public void cut(TreeEdge edge) {
if (edge.owner() != this) {
throw new IllegalArgumentException();
}
if (edge.cut) {
return;
}
edge.cut = true;
BSTNode outgoing = edge.left;
BSTNode incoming = edge.right;
if (incoming.src != outgoing.dst || incoming.dst != outgoing.src || outgoing == incoming || incoming.src == incoming.dst) {
throw new IllegalStateException();
}
if (!connected(incoming.src, incoming.dst)) {
throw new IllegalStateException();
}
BSTNode.barrelRollToLowest(outgoing);
BSTNodePair disconnected = incoming.disconnect(Direction.RIGHT);
if (disconnected.left.walkDescendant(Direction.LEFT) != outgoing || disconnected.right.walkDescendant(Direction.LEFT) != incoming) {
throw new IllegalStateException();
}
if (loopbacks[incoming.src].walkAncestor() != disconnected.left || loopbacks[outgoing.src].walkAncestor() != disconnected.right) {
throw new IllegalStateException();
}
outgoing.remove();
incoming.remove();
}
public boolean connected(int vertA, int vertB) {
return loopbacks[vertA].walkAncestor() == loopbacks[vertB].walkAncestor();
}
public int size(int vert) {
return loopbacks[vert].walkAncestor().loopbackSize;
}
public int[] walk(int vert) {
BSTNode root = loopbacks[vert].walkAncestor();
int[] ret = new int[root.loopbackSize];
int[] idx = {0};
root.walk(node -> {
if (node.isLoopback()) {
ret[idx[0]++] = node.src;
}
});
return ret;
}
// redblacknode impl deleted here
public static class SplayNode extends BSTNode {
private SplayNode() {
super();
}
private SplayNode(int same) {
super(same);
}
private SplayNode(int src, int dst) {
super(src, dst);
}
public void splay() {
while (parent != null) {
BSTNode grandparent = parent.parent;
Direction myDir = whichChildAmI();
if (grandparent != null) {
Direction parentDir = parent.whichChildAmI();
if (myDir == parentDir) {
// see "Zig-zig step" of https://en.wikipedia.org/wiki/Splay_tree
grandparent.splayRotate(parentDir);
parent.splayRotate(myDir);
} else {
// see "Zig-zag step" of https://en.wikipedia.org/wiki/Splay_tree
parent.splayRotate(myDir);
grandparent.splayRotate(parentDir);
}
} else {
parent.splayRotate(myDir);
if (parent != null) {
throw new IllegalStateException();
}
}
}
}
@Override
protected BSTNode concatenateRoots(BSTNode right) {
if (this.parent != null || right.parent != null || this == right) {
throw new IllegalStateException();
}
SplayNode newRoot = (SplayNode) right.walkDescendant(Direction.LEFT);
newRoot.splay();
// newRoot is now the root of a splay tree containing all of right
// and, it is the LOWEST value of right, and left is assumed to be less than it, so now we can just attach left as its left child
// (since it is the lowest value of right, it cannot possibly have any left child already)
if (newRoot.getChild(Direction.LEFT) != null) {
throw new IllegalStateException();
}
newRoot.setChild(Direction.LEFT, this);
newRoot.childUpdated();
return newRoot;
}
@Override
protected BSTNodePair disconnect(Direction remainOnSide) {
splay(); // SIGNIFICANTLY easier to split the tree in half, if we are the root node
BSTNode left = this;
BSTNode right = this;
// simple detach of one side
if (remainOnSide == Direction.LEFT) {
right = rightChild;
rightChild = null;
childUpdated();
if (right != null) {
right.sizeMustBeAccurate();
right.parent = null;
}
} else {
left = leftChild;
leftChild = null;
childUpdated();
if (left != null) {
left.sizeMustBeAccurate();
left.parent = null;
}
}
return new BSTNodePair(left, right);
}
public void remove() {
splay();
if (leftChild != null) {
leftChild.parent = null;
}
if (rightChild != null) {
rightChild.parent = null;
}
if (leftChild != null && rightChild != null) {
leftChild.concatenateRoots(rightChild);
}
}
}
public abstract static class BSTNode {
protected final int src;
protected final int dst;
protected BSTNode leftChild;
protected BSTNode rightChild;
protected BSTNode parent;
protected int loopbackSize;
private BSTNode() {
this(-1);
}
private BSTNode(int same) {
this.src = same;
this.dst = same;
this.loopbackSize = 1;
}
private BSTNode(int src, int dst) {
if (src == dst) {
throw new IllegalArgumentException();
}
this.src = src;
this.dst = dst;
this.loopbackSize = 0;
}
protected void splayRotate(Direction dir) {
rotateSecret(dir);
}
protected void redBlackRotate(Direction dir) {
rotateSecret(dir.opposite()); // i guess they just use different conventions?
}
private void rotateSecret(Direction dir) {
// promote my "dir" child to my level, swap myself down to that level
// see "Zig step" of https://en.wikipedia.org/wiki/Splay_tree
BSTNode child = this.getChild(dir);
BSTNode replacementChild = child.getChild(dir.opposite()); // stays at the same level, is just rotated to the other side of the tree
this.setChild(dir, replacementChild);
if (parent == null) {
child.parent = null;
} else {
parent.setChild(whichChildAmI(), child);
}
child.setChild(dir.opposite(), this); // e.g. my left child now has me as their right child
childUpdated();
parent.childUpdated();
}
public static BSTNode concatenate(BSTNode left, BSTNode right) {
if (left == null) {
throw new IllegalStateException();
}
if (right == null) {
return left;
}
return left.walkAncestor().concatenateRoots(right.walkAncestor());
}
protected void afterSwap(BSTNode other) {
}
protected void swapLocationWith(BSTNode other) {
if (other == this) {
throw new IllegalStateException();
}
if (other.parent == this) {
other.swapLocationWith(this);
return;
}
if (parent == other) {
// grandpa
// other
// this otherChild
// left right
// and we want that to become
// grandpa
// this
// other otherChild
// left right
Direction dir = whichChildAmI(); // LEFT, in the above example
BSTNode otherChild = other.getChild(dir.opposite());
BSTNode left = leftChild;
BSTNode right = rightChild;
if (other.parent == null) { // grandpa
parent = null;
} else {
other.parent.setChild(other.whichChildAmI(), this);
}
setChild(dir, other);
setChild(dir.opposite(), otherChild);
other.setChild(Direction.LEFT, left);
other.setChild(Direction.RIGHT, right);
other.childUpdated();
childUpdated();
} else {
Direction myDir = parent == null ? null : whichChildAmI();
Direction otherDir = other.parent == null ? null : other.whichChildAmI();
BSTNode tmpLeft = leftChild;
BSTNode tmpRight = rightChild;
BSTNode tmpParent = parent;
if (other.parent == null) { // grandpa
parent = null;
} else {
other.parent.setChild(otherDir, this);
}
if (tmpParent == null) {
other.parent = null;
} else {
tmpParent.setChild(myDir, other);
}
setChild(Direction.LEFT, other.leftChild);
setChild(Direction.RIGHT, other.rightChild);
other.setChild(Direction.LEFT, tmpLeft);
other.setChild(Direction.RIGHT, tmpRight);
calcSize();
if (parent != null) {
parent.bubbleUpSize();
}
other.calcSize();
if (other.parent != null) {
other.parent.bubbleUpSize();
}
}
afterSwap(other);
}
protected abstract BSTNode concatenateRoots(BSTNode right);
public static BSTNode barrelRollToLowest(BSTNode target) {
// 1. chop the tree in half, centered at target
// 2. reattach them in the opposite order
// in other words, "cut the deck but don't riffle" - leave "target" as the first node (NOT necessarily the root node)
BSTNodePair pair = target.disconnect(Direction.RIGHT);
if ((target instanceof SplayNode && pair.right != target) || pair.right.parent != null) { // splay to root only happens with a splay tree, obviously
throw new IllegalStateException();
}
// target is now the lowest (leftmost) element of pair.right
BSTNode ret = BSTNode.concatenate(pair.right, pair.left); // target is now first, and everything else is still in order :D
// use concatenate and not concatenateRoots because pair.left could be null
if (ret == target && pair.left != null) {
throw new IllegalStateException();
}
return ret;
}
protected abstract BSTNodePair disconnect(Direction remainOnSide); // chops the tree in half, with "this" remaining on the side "remainOnSide"
public abstract void remove();
protected void bubbleUpSize() {
int ns = calcSize();
if (loopbackSize != ns) {
loopbackSize = ns;
if (parent != null) {
parent.bubbleUpSize();
}
}
}
protected void childUpdated() {
loopbackSize = calcSize();
}
protected void sizeMustBeAccurate() {
if (loopbackSize != calcSize()) {
throw new IllegalStateException();
}
}
protected int calcSize() {
int size = 0;
if (isLoopback()) {
size++;
}
if (rightChild != null) {
size += rightChild.loopbackSize;
}
if (leftChild != null) {
size += leftChild.loopbackSize;
}
return size;
}
protected BSTNode getChild(Direction dir) {
return dir == Direction.LEFT ? leftChild : rightChild;
}
protected void setChild(Direction dir, BSTNode newChild) {
if (newChild == this) {
throw new IllegalStateException();
}
if (dir == Direction.LEFT) {
leftChild = newChild;
} else {
rightChild = newChild;
}
if (newChild != null) {
newChild.parent = this;
}
}
protected Direction whichChildAmI() {
if (parent.leftChild == this) {
return Direction.LEFT;
}
if (parent.rightChild == this) {
return Direction.RIGHT;
}
throw new IllegalStateException();
}
protected BSTNode walkDescendant(Direction side) {
BSTNode child = getChild(side);
if (child == null) {
return this;
} else {
return child.walkDescendant(side);
}
}
protected BSTNode walkAncestor() {
BSTNode walk = this;
while (walk.parent != null) {
parentWalks++;
walk = walk.parent;
}
parentCalls++;
return walk;
/*if (parent == null) {
return this;
} else {
return parent.walkAncestor();
}*/
}
protected void walk(Consumer<BSTNode> consumer) {
if (leftChild != null) {
leftChild.walk(consumer);
}
consumer.accept(this);
if (rightChild != null) {
rightChild.walk(consumer);
}
}
protected BSTNode walkNext() {
if (rightChild != null) {
return rightChild.walkDescendant(Direction.LEFT);
}
BSTNode itr = this;
while (itr.parent != null && itr.whichChildAmI() == Direction.RIGHT) {
itr = itr.parent;
}
return itr.parent;
}
protected boolean isAlone() {
return parent == null && leftChild == null && rightChild == null;
}
public boolean isLoopback() {
return src == dst;
}
}
public enum Direction { // TODO check if proguard converts this to an int
LEFT, RIGHT;
public Direction opposite() {
return this == LEFT ? RIGHT : LEFT;
}
}
private static class BSTNodePair {
final BSTNode left;
final BSTNode right;
private BSTNodePair(BSTNode left, BSTNode right) {
this.left = left;
this.right = right;
if ((left != null && left.parent != null) || (right != null && right.parent != null)) {
throw new IllegalStateException();
}
}
}
public class TreeEdge {
private boolean cut;
final BSTNode left;
final BSTNode right;
private TreeEdge(BSTNode left, BSTNode right) {
this.left = left;
this.right = right;
}
private EulerTourForest owner() {
return EulerTourForest.this;
}
}
private static void mustEq(BSTNode a, BSTNode b) {
if (a != b) {
throw new IllegalStateException(a + " " + b);
}
}
public static void sanityCheck2() {
for (int i = 0; i < 9; i++) {
int mode = i % 3;
int SZ = 700;
TreeEdge[] up = new TreeEdge[SZ * SZ];
TreeEdge[] right = new TreeEdge[SZ * SZ];
EulerTourForest forest = new EulerTourForest(SZ * SZ);
for (int y = 0; y < SZ; y++) {
for (int x = 0; x < SZ; x++) {
if (y != SZ - 1) {
try {
up[x * SZ + y] = forest.link(x * SZ + y, x * SZ + (y + 1));
} catch (IllegalStateException ex) {} // ignore if already linked
}
if (x != SZ - 1) {
try {
right[x * SZ + y] = forest.link(x * SZ + y, (x + 1) * SZ + y);
} catch (IllegalStateException ex) {} // ignore if already linked
}
}
}
Random rand = new Random(5021);
for (int x = 0; x < SZ; x++) {
int y = SZ / 2;
forest.cut(up[x * SZ + y]);
//System.out.println("Sz " + forest.size(x * SZ + y));
}
if (mode == 1) {
for (int j = 0; j < SZ * SZ * 2; j++) { // *2 for a fair comparison to during connection, since that one splays both sides of each test
((EulerTourForest.SplayNode) forest.loopbacks[rand.nextInt(SZ * SZ)]).splay();
}
}
long a = System.currentTimeMillis();
parentCalls = 0; // reset metrics
parentWalks = 0;
for (int checks = 0; checks < SZ * SZ; checks++) {
int v1 = rand.nextInt(SZ * SZ);
int v2 = rand.nextInt(SZ * SZ);
forest.connected(v1, v2);
if (mode == 2) {
((SplayNode) forest.loopbacks[v1]).splay();
((SplayNode) forest.loopbacks[v2]).splay();
}
}
forest.checkForest(false);
if (mode == 0) {
System.out.println("WITHOUT random accesses");
} else if (mode == 1) {
System.out.println("WITH pre-connection random accesses");
} else {
System.out.println("WITH random accesses during connection");
}
System.out.println("Walk ancestor was called " + parentCalls + " times, and it traversed " + parentWalks + " in total, implying an average height of " + (parentWalks / (float) parentCalls));
System.out.println("Time: " + (System.currentTimeMillis() - a));
}
}
public static void sanityCheck() {
for (Direction dir : Direction.values()) {
System.out.println("Testing zig " + dir);
// see "Zig step" of https://en.wikipedia.org/wiki/Splay_tree
SplayNode p = new SplayNode();
SplayNode x = new SplayNode();
SplayNode A = new SplayNode();
SplayNode B = new SplayNode();
SplayNode C = new SplayNode();
p.setChild(dir, x);
p.setChild(dir.opposite(), C);
x.setChild(dir, A);
x.setChild(dir.opposite(), B);
x.splay();
mustEq(p.parent, x);
mustEq(p.getChild(dir), B);
mustEq(p.getChild(dir.opposite()), C);
mustEq(x.parent, null);
mustEq(x.getChild(dir), A);
mustEq(x.getChild(dir.opposite()), p);
mustEq(A.parent, x);
mustEq(A.getChild(dir), null);
mustEq(A.getChild(dir.opposite()), null);
mustEq(B.parent, p);
mustEq(B.getChild(dir), null);
mustEq(B.getChild(dir.opposite()), null);
mustEq(C.parent, p);
mustEq(C.getChild(dir), null);
mustEq(C.getChild(dir.opposite()), null);
}
for (Direction dir : Direction.values()) {
System.out.println("Testing zig-zig " + dir);
// see "Zig-zig step" of https://en.wikipedia.org/wiki/Splay_tree
SplayNode g = new SplayNode();
SplayNode p = new SplayNode();
SplayNode x = new SplayNode();
SplayNode A = new SplayNode();
SplayNode B = new SplayNode();
SplayNode C = new SplayNode();
SplayNode D = new SplayNode();
g.setChild(dir, p);
g.setChild(dir.opposite(), D);
p.setChild(dir, x);
p.setChild(dir.opposite(), C);
x.setChild(dir, A);
x.setChild(dir.opposite(), B);
x.splay();
mustEq(g.parent, p);
mustEq(g.getChild(dir), C);
mustEq(g.getChild(dir.opposite()), D);
mustEq(p.parent, x);
mustEq(p.getChild(dir), B);
mustEq(p.getChild(dir.opposite()), g);
mustEq(x.parent, null);
mustEq(x.getChild(dir), A);
mustEq(x.getChild(dir.opposite()), p);
mustEq(A.parent, x);
mustEq(A.getChild(dir), null);
mustEq(A.getChild(dir.opposite()), null);
mustEq(B.parent, p);
mustEq(B.getChild(dir), null);
mustEq(B.getChild(dir.opposite()), null);
mustEq(C.parent, g);
mustEq(C.getChild(dir), null);
mustEq(C.getChild(dir.opposite()), null);
mustEq(D.parent, g);
mustEq(D.getChild(dir), null);
mustEq(D.getChild(dir.opposite()), null);
}
for (Direction dir : Direction.values()) {
System.out.println("Testing zig-zag " + dir);
// see "Zig-zag step" of https://en.wikipedia.org/wiki/Splay_tree
SplayNode g = new SplayNode();
SplayNode p = new SplayNode();
SplayNode x = new SplayNode();
SplayNode A = new SplayNode();
SplayNode B = new SplayNode();
SplayNode C = new SplayNode();
SplayNode D = new SplayNode();
g.setChild(dir, p);
g.setChild(dir.opposite(), D);
p.setChild(dir, A);
p.setChild(dir.opposite(), x);
x.setChild(dir, B);
x.setChild(dir.opposite(), C);
x.splay();
mustEq(g.parent, x);
mustEq(g.getChild(dir), C);
mustEq(g.getChild(dir.opposite()), D);
mustEq(p.parent, x);
mustEq(p.getChild(dir), A);
mustEq(p.getChild(dir.opposite()), B);
mustEq(x.parent, null);
mustEq(x.getChild(dir), p);
mustEq(x.getChild(dir.opposite()), g);
mustEq(A.parent, p);
mustEq(A.getChild(dir), null);
mustEq(A.getChild(dir.opposite()), null);
mustEq(B.parent, p);
mustEq(B.getChild(dir), null);
mustEq(B.getChild(dir.opposite()), null);
mustEq(C.parent, g);
mustEq(C.getChild(dir), null);
mustEq(C.getChild(dir.opposite()), null);
mustEq(D.parent, g);
mustEq(D.getChild(dir), null);
mustEq(D.getChild(dir.opposite()), null);
}
for (Direction GtoP : Direction.values()) {
for (Direction PtoX : Direction.values()) {
System.out.println("Testing connected swap " + GtoP + " " + PtoX);
SplayNode g = new SplayNode();
SplayNode p = new SplayNode();
SplayNode x = new SplayNode();
SplayNode A = new SplayNode();
SplayNode B = new SplayNode();
SplayNode C = new SplayNode();
SplayNode D = new SplayNode();
g.setChild(GtoP, p);
g.setChild(GtoP.opposite(), D);
p.setChild(PtoX, x);
p.setChild(PtoX.opposite(), A);
x.setChild(Direction.LEFT, B);
x.setChild(Direction.RIGHT, C);
/*x.black = true;
p.black = false;*/
p.swapLocationWith(x);
/*if (x.black || !p.black) {
throw new IllegalStateException();
}*/
mustEq(g.parent, null);
mustEq(g.getChild(GtoP), x);
mustEq(g.getChild(GtoP.opposite()), D);
mustEq(p.parent, x);
mustEq(p.getChild(Direction.LEFT), B);
mustEq(p.getChild(Direction.RIGHT), C);
mustEq(x.parent, g);
mustEq(x.getChild(PtoX), p);
mustEq(x.getChild(PtoX.opposite()), A);
mustEq(A.parent, x);
mustEq(A.getChild(Direction.LEFT), null);
mustEq(A.getChild(Direction.RIGHT), null);
mustEq(B.parent, p);
mustEq(B.getChild(Direction.LEFT), null);
mustEq(B.getChild(Direction.RIGHT), null);
mustEq(C.parent, p);
mustEq(C.getChild(Direction.LEFT), null);
mustEq(C.getChild(Direction.RIGHT), null);
mustEq(D.parent, g);
mustEq(D.getChild(Direction.LEFT), null);
mustEq(D.getChild(Direction.RIGHT), null);
}
}
for (Direction APtoA : Direction.values()) {
for (Direction BPtoB : Direction.values()) {
System.out.println("Testing disconnected swap " + APtoA + " " + BPtoB);
SplayNode ap = new SplayNode();
SplayNode apoc = new SplayNode();
SplayNode a = new SplayNode();
SplayNode alc = new SplayNode();
SplayNode arc = new SplayNode();
SplayNode bp = new SplayNode();
SplayNode bpoc = new SplayNode();
SplayNode b = new SplayNode();
SplayNode blc = new SplayNode();
SplayNode brc = new SplayNode();
ap.setChild(APtoA, a);
ap.setChild(APtoA.opposite(), apoc);
a.setChild(Direction.LEFT, alc);
a.setChild(Direction.RIGHT, arc);
bp.setChild(BPtoB, b);
bp.setChild(BPtoB.opposite(), bpoc);
b.setChild(Direction.LEFT, blc);
b.setChild(Direction.RIGHT, brc);
/*a.black = true;
b.black = false;*/
a.swapLocationWith(b);
/*if (a.black || !b.black) {
throw new IllegalStateException();
}*/
mustEq(ap.parent, null);
mustEq(ap.getChild(APtoA), b);
mustEq(ap.getChild(APtoA.opposite()), apoc);
mustEq(apoc.parent, ap);
mustEq(apoc.getChild(Direction.LEFT), null);
mustEq(apoc.getChild(Direction.RIGHT), null);
mustEq(a.parent, bp);
mustEq(a.getChild(Direction.LEFT), blc);
mustEq(a.getChild(Direction.RIGHT), brc);
mustEq(alc.parent, b);
mustEq(alc.getChild(Direction.LEFT), null);
mustEq(alc.getChild(Direction.RIGHT), null);
mustEq(arc.parent, b);
mustEq(arc.getChild(Direction.LEFT), null);
mustEq(arc.getChild(Direction.RIGHT), null);
mustEq(bp.parent, null);
mustEq(bp.getChild(BPtoB), a);
mustEq(bp.getChild(BPtoB.opposite()), bpoc);
mustEq(bpoc.parent, bp);
mustEq(bpoc.getChild(Direction.LEFT), null);
mustEq(bpoc.getChild(Direction.RIGHT), null);
mustEq(b.parent, ap);
mustEq(b.getChild(Direction.LEFT), alc);
mustEq(b.getChild(Direction.RIGHT), arc);
mustEq(blc.parent, a);
mustEq(blc.getChild(Direction.LEFT), null);
mustEq(blc.getChild(Direction.RIGHT), null);
mustEq(brc.parent, a);
mustEq(brc.getChild(Direction.LEFT), null);
mustEq(brc.getChild(Direction.RIGHT), null);
}
}
{
Random rand = new Random(5021);
List<Supplier<BSTNode>> constructors = Arrays.asList(SplayNode::new/*, SplayNode::new*/);
for (int run = 0; run < 10; run++) {
int NODES = 10000;
Supplier<BSTNode> toUse = constructors.get(run % constructors.size());
List<BSTNode> nodes = new ArrayList<>();
{
BSTNode root = toUse.get();
nodes.add(root);
for (int i = 1; i < NODES; i++) {
nodes.add(toUse.get());
root = BSTNode.concatenate(root, nodes.get(i));
}
}
int shuffledBy = 0;
for (int ii = 0; ii < 10000; ii++) {
if (rand.nextBoolean()) {
BSTNode root = nodes.get(rand.nextInt(NODES));
if (root instanceof SplayNode) {
((SplayNode) root).splay();
if (root != nodes.get(rand.nextInt(NODES)).walkAncestor() || root.loopbackSize != NODES) {
throw new IllegalStateException();
}
} else {
throw new IllegalStateException();
}
}
if (rand.nextBoolean()) {
shuffledBy = rand.nextInt(NODES);
BSTNode root = BSTNode.barrelRollToLowest(nodes.get(shuffledBy));
if (root != nodes.get(rand.nextInt(NODES)).walkAncestor() || root.loopbackSize != NODES) {
throw new IllegalStateException();
}
}
if (rand.nextBoolean()) {
int pos = rand.nextBoolean() ? (shuffledBy + NODES + rand.nextInt(10) - 5) % NODES : rand.nextInt(NODES);
BSTNode remove = nodes.remove(pos);
NODES--;
remove.remove();
if (shuffledBy > pos) {
shuffledBy--;
}
}
List<BSTNode> order = new ArrayList<>(NODES);
nodes.get(rand.nextInt(NODES)).walkAncestor().walk(order::add);
for (int n = 0; n < NODES; n++) {
if (order.get(n) != nodes.get((n + shuffledBy) % NODES)) {
throw new IllegalStateException();
}
order.get(n).sizeMustBeAccurate();
if (order.get(n).walkNext() != (n < NODES - 1 ? order.get(n + 1) : null)) {
throw new IllegalStateException();
}
}
}
}
}
{
// slide 22 of https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf
EulerTourForest forest = new EulerTourForest(11);
forest.link(0, 1);
forest.link(1, 3);
forest.link(2, 4);
forest.link(1, 2);
TreeEdge toCut = forest.link(0, 5);
forest.link(5, 6);
forest.link(6, 9);
forest.link(9, 10);
forest.link(9, 8);
forest.link(6, 7);
BSTNode.barrelRollToLowest(forest.loopbacks[0]);
if (!forest.checkForest(true).equals("abdbcecbafgjkjijghgf")) {
throw new IllegalStateException();
}
forest.cut(toCut);
if (!forest.checkForest(true).equals("abdbcecb fgjkjijghg")) {
throw new IllegalStateException();
}
}
{
// slide 26 of https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf
EulerTourForest forest = new EulerTourForest(11);
forest.link(2, 4);
TreeEdge toCut = forest.link(2, 1);
forest.link(1, 0);
forest.link(1, 3);
forest.link(2, 6);
forest.link(6, 9);
forest.link(9, 10);
forest.link(9, 8);
forest.link(6, 7);
forest.link(5, 6);
BSTNode.barrelRollToLowest(forest.loopbacks[2]);
if (!forest.checkForest(true).equals("cecbabdbcgjkjijghgfg")) {
throw new IllegalStateException();
}
forest.cut(toCut);
if (!forest.checkForest(true).equals("babd cgjkjijghgfgce")) {
throw new IllegalStateException();
}
}
}
public String checkForest(boolean verbose) {
boolean[] seen = new boolean[loopbacks.length];
StringBuilder ret = new StringBuilder();
for (int vert = 0; vert < loopbacks.length; vert++) {
if (seen[vert]) {
continue;
}
List<BSTNode> order = new ArrayList<>();
loopbacks[vert].walkAncestor().walk(order::add);
for (int i = 0; i < order.size(); i++) {
if (verbose) {
System.out.print("(" + (char) ('a' + order.get(i).src) + "," + (char) ('a' + order.get(i).dst) + ") ");
}
if (order.get(i).dst != order.get((i + 1) % order.size()).src) {
throw new IllegalStateException();
}
if (order.get(i).isLoopback()) {
seen[order.get(i).src] = true;
} else {
ret.append((char) ('a' + order.get(i).src));
}
}
if (verbose) {
System.out.println();
}
ret.append(" ");
if (!seen[vert]) {
throw new IllegalStateException();
}
}
if (verbose) {
System.out.println(ret);
}
return ret.toString().trim();
}
}

View File

@@ -0,0 +1,94 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import net.minecraft.util.EnumFacing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* I hate porting things to new versions of Minecraft
* <p>
* So just like BetterBlockPos, we now have Face
*/
public enum Face {
DOWN, UP, NORTH, SOUTH, WEST, EAST;
public final int index = ordinal();
public final int oppositeIndex = opposite(index);
public final int x = toMC().getXOffset();
public final int y = toMC().getYOffset();
public final int z = toMC().getZOffset();
public final long offset = BetterBlockPos.toLong(x, y, z); // both the previous three lines (avoidably) and this one (unavoidably due to BlockPos superclass of BetterBlockPos) mess up test timing / profiling because it calls the <clinit> of both EnumFacing and BlockPos which does some Log4j bs lol
public final int[] vec = new int[]{x, y, z};
public final boolean vertical = y != 0;
public final int horizontalIndex = x & 1 | (x | z) & 2;
public static final int NUM_FACES = 6;
public static final Face[] VALUES = new Face[NUM_FACES];
public static final Face[] HORIZONTALS = new Face[4];
public static final List<Optional<Face>> OPTS;
public static final long[] OFFSETS = new long[NUM_FACES];
static {
List<Optional<Face>> lst = new ArrayList<>();
for (Face face : values()) {
VALUES[face.index] = face;
OFFSETS[face.index] = face.offset;
lst.add(Optional.of(face));
HORIZONTALS[face.horizontalIndex] = face;
}
lst.add(Optional.empty());
OPTS = Collections.unmodifiableList(lst);
}
public final EnumFacing toMC() {
return EnumFacing.byIndex(index);
}
public final Face opposite() {
return VALUES[oppositeIndex];
}
public final long offset(long pos) {
return (pos + offset) & BetterBlockPos.POST_ADDITION_MASK;
}
public static long offset(long pos, int face) {
return (pos + OFFSETS[face]) & BetterBlockPos.POST_ADDITION_MASK;
}
public static int opposite(int face) {
return face ^ 1;
}
public static int oppositeHorizontal(int horizontalIndex) {
return horizontalIndex ^ 2;
}
public static Face between(long from, long to) {
for (int i = 0; i < NUM_FACES; i++) {
if (offset(from, i) == to) {
return VALUES[i];
}
}
return null;
}
}

View File

@@ -0,0 +1,88 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* There will be a BlockStateCachedData for every IBlockState in the game, yes.
* <p>
* But, we need some additional BlockStateCachedDatas. For example, we need one that represents hypothetical scaffolding, and another for air, for properly computing place order dependency graphs. Some other places need a placeholder for out of bounds.
* <p>
* We could just say that scaffolding is always, like, dirt, or something. But that would be inelegant.
* <p>
* And beyond the needs at runtime, unit tests shouldn't need to bootstrap and boot up the entire Minecraft game. So, let's have some fake BlockStateCachedData for testing purposes too!
*/
public class FakeStates {
// the three aformentioned placeholders for runtime
public static final BlockStateCachedData SCAFFOLDING = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().collisionHeight(1).canPlaceAgainstMe());
// need a second solid block so that "== FakeStates.SCAFFOLDING" doesn't get tricked
public static final BlockStateCachedData SOLID = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().collisionHeight(1).canPlaceAgainstMe());
public static final BlockStateCachedData AIR = new BlockStateCachedData(new BlockStateCachedDataBuilder().setAir());
public static final BlockStateCachedData OUT_OF_BOUNDS = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).collisionHeight(1));
// and some for testing
public static final BlockStateCachedData[] BY_HEIGHT;
static {
BY_HEIGHT = new BlockStateCachedData[Blip.FULL_BLOCK + 1];
for (int height = 1; height <= Blip.FULL_BLOCK; height++) {
BY_HEIGHT[height] = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().collisionHeight(height * Blip.RATIO));
}
BY_HEIGHT[0] = AIR;
}
private static List<BlockStateCachedData> PROBABLE_BLOCKS = null;
private static List<BlockStateCachedData> getProbableBlocks() {
if (PROBABLE_BLOCKS == null) {
//long a = System.currentTimeMillis();
Random rand = new Random(5021);
PROBABLE_BLOCKS = IntStream.range(0, 10000).mapToObj($ -> {
List<BlockStatePlacementOption> ret = new ArrayList<>(SCAFFOLDING.placeMe);
ret.removeIf($$ -> rand.nextInt(10) < 2);
BlockStateCachedDataBuilder builder = new BlockStateCachedDataBuilder() {
@Override
public List<BlockStatePlacementOption> howCanIBePlaced() {
return ret;
}
};
if (ret.isEmpty()) {
builder.placementLogicNotImplementedYet();
}
return new BlockStateCachedData(builder
.fullyWalkableTop()
.collisionHeight(1)
.canPlaceAgainstMe()
.collidesWithPlayer(true));
}).collect(Collectors.toList());
//System.out.println("Took " + (System.currentTimeMillis() - a));
}
return PROBABLE_BLOCKS;
}
public static BlockStateCachedData probablyCanBePlaced(Random rand) {
return getProbableBlocks().get(rand.nextInt(getProbableBlocks().size()));
}
// probably more will go here such as for making sure that slabs and stairs work right (like there'll be a fake slab and a fake stair i guess?)
}

View File

@@ -0,0 +1,298 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
public class GreedySolver {
private final SolverEngineInput engineInput;
private final NodeBinaryHeap heap = new NodeBinaryHeap();
private final Long2ObjectOpenHashMap<Node> nodes = new Long2ObjectOpenHashMap<>();
private final ZobristWorldStateCache zobristCache;
private final long allCompleted;
private final Bounds bounds;
private Column scratchpadExpandNode1 = new Column();
private Column scratchpadExpandNode2 = new Column();
private Column scratchpadExpandNode3 = new Column();
public GreedySolver(SolverEngineInput input) {
this.engineInput = input;
this.bounds = engineInput.graph.bounds();
this.zobristCache = new ZobristWorldStateCache(new WorldState.WorldStateWrappedSubstrate(engineInput));
Node root = new Node(engineInput.player, null, 0L, -1L, 0);
nodes.put(root.nodeMapKey(), root);
heap.insert(root);
this.allCompleted = WorldState.predetermineGoalZobrist(engineInput.allToPlaceNow);
}
synchronized SolverEngineOutput search() {
while (!heap.isEmpty()) {
Node node = heap.removeLowest();
if (!node.sneaking() && node.worldStateZobristHash == allCompleted) {
return backwards(node);
}
expandNode(node);
}
throw new UnsupportedOperationException();
}
private SolverEngineOutput backwards(Node node) {
ArrayDeque<SolvedActionStep> steps = new ArrayDeque<>();
while (node.previous != null) {
steps.addFirst(step(node, node.previous));
node = node.previous;
}
return new SolverEngineOutput(new ArrayList<>(steps));
}
private SolvedActionStep step(Node next, Node prev) {
if (next.worldStateZobristHash == prev.worldStateZobristHash) {
return new SolvedActionStep(next.pos());
} else {
return new SolvedActionStep(next.pos(), WorldState.unzobrist(prev.worldStateZobristHash ^ next.worldStateZobristHash));
}
}
private boolean wantToPlaceAt(long blockGoesAt, Node vantage, int blipsWithinVoxel, WorldState worldState) {
if (worldState.blockExists(blockGoesAt)) {
return false;
}
long vpos = vantage.pos();
int relativeX = BetterBlockPos.XfromLong(vpos) - BetterBlockPos.XfromLong(blockGoesAt);
int relativeY = blipsWithinVoxel + Blip.FULL_BLOCK * (BetterBlockPos.YfromLong(vpos) - BetterBlockPos.YfromLong(blockGoesAt));
int relativeZ = BetterBlockPos.ZfromLong(vpos) - BetterBlockPos.ZfromLong(blockGoesAt);
BlockStateCachedData blockBeingPlaced = engineInput.graph.data(blockGoesAt);
for (BlockStatePlacementOption option : blockBeingPlaced.placeMe) {
long maybePlaceAgainst = option.against.offset(blockGoesAt);
if (!bounds.inRangePos(maybePlaceAgainst)) {
continue;
}
if (!worldState.blockExists(maybePlaceAgainst)) {
continue;
}
BlockStateCachedData placingAgainst = engineInput.graph.data(maybePlaceAgainst);
PlaceAgainstData againstData = placingAgainst.againstMe(option);
traces:
for (Raytracer.Raytrace trace : option.computeTraceOptions(againstData, relativeX, relativeY, relativeZ, PlayerVantage.LOOSE_CENTER, blockReachDistance())) { // TODO or only take the best one
for (long l : trace.passedThrough) {
if (worldState.blockExists(l)) {
continue traces;
}
}
return true;
}
}
return false;
}
private void expandNode(Node node) {
WorldState worldState = zobristCache.coalesceState(node);
long pos = node.pos();
Column within = scratchpadExpandNode1;
within.initFrom(pos, worldState, engineInput);
Column supportedBy;
boolean sneaking = node.sneaking();
if (sneaking) {
supportedBy = scratchpadExpandNode3;
long supportedFeetVoxel = SneakPosition.sneakDirectionFromPlayerToSupportingBlock(node.sneakingPosition()).offset(pos);
supportedBy.initFrom(supportedFeetVoxel, worldState, engineInput);
if (Main.DEBUG && !within.okToSneakIntoHereAtHeight(supportedBy.feetBlips)) {
throw new IllegalStateException();
}
} else {
supportedBy = within;
}
int playerFeet = supportedBy.feetBlips;
if (Main.DEBUG && !supportedBy.playerCanExistAtFootBlip(playerFeet)) {
throw new IllegalStateException();
}
// -------------------------------------------------------------------------------------------------------------
// place block beneath or within feet voxel
for (int dy = -1; dy <= 0; dy++) {
// this is the common case for sneak bridging with full blocks
long maybePlaceAt = BetterBlockPos.offsetBy(pos, 0, dy, 0);
BlockStateCachedData wouldBePlaced = engineInput.graph.data(maybePlaceAt);
int cost = blockPlaceCost();
int playerFeetWouldBeAt = playerFeet;
/*if (wouldBePlaced.collidesWithPlayer) {
int heightRelativeToCurrentVoxel = wouldBePlaced.collisionHeightBlips() + dy * Blip.FULL_BLOCK;
if (heightRelativeToCurrentVoxel > playerFeet) {
// we would need to jump in order to do this
cost += jumpCost();
playerFeetWouldBeAt = heightRelativeToCurrentVoxel; // because we'd have to jump, and could only place the block once we had cleared the collision box for it
if (!within.playerCanExistAtFootBlip(heightRelativeToCurrentVoxel) || !supportedBy.playerCanExistAtFootBlip(heightRelativeToCurrentVoxel)) {
continue;
}
}
}
if (wantToPlaceAt(maybePlaceAt, node, playerFeetWouldBeAt, worldState)) {
upsertEdge(node, worldState, pos, null, maybePlaceAt, cost);
}*/
}
// -------------------------------------------------------------------------------------------------------------
if (sneaking) {
// we can walk back to where we were
upsertEdge(node, worldState, supportedBy.pos, null, -1, flatCost());
// this will probably rarely be used. i can only imagine rare scenarios such as needing the extra perspective in order to place a block a bit more efficiently. like, this could avoid unnecessary ancillary scaffolding i suppose.
// ----
// also let's try just letting ourselves fall off the edge of the block
int descendBy = PlayerPhysics.playerFalls(pos, worldState, engineInput);
if (descendBy != -1) {
upsertEdge(node, worldState, BetterBlockPos.offsetBy(pos, 0, -descendBy, 0), null, -1, fallCost(descendBy));
}
return;
}
// not sneaking! sneaking returned ^^
// -------------------------------------------------------------------------------------------------------------
// walk sideways and either stay level, ascend, or descend
Column into = scratchpadExpandNode2;
for (Face travel : Face.HORIZONTALS) {
long newPos = travel.offset(pos);
into.initFrom(newPos, worldState, engineInput);
PlayerPhysics.Collision collision = PlayerPhysics.playerTravelCollides(within, into);
switch (collision) {
case BLOCKED: {
continue;
}
case FALL: {
upsertEdge(node, worldState, newPos, travel, -1, flatCost()); // sneak off edge of block
break;
}
default: {
long realNewPos = BetterBlockPos.offsetBy(newPos, 0, collision.voxelVerticalOffset(), 0);
upsertEdge(node, worldState, realNewPos, null, -1, collision.requiresJump() ? jumpCost() : flatCost());
break;
}
}
}
}
private int fallCost(int blocks) {
if (blocks < 1) {
throw new IllegalStateException();
}
throw new UnsupportedOperationException();
}
private int flatCost() {
throw new UnsupportedOperationException();
}
private int jumpCost() {
throw new UnsupportedOperationException();
}
private double blockReachDistance() {
throw new UnsupportedOperationException();
}
private void upsertEdge(Node node, WorldState worldState, long newPlayerPosition, Face sneakingTowards, long blockPlacement, int edgeCost) {
Node neighbor = getNode(newPlayerPosition, sneakingTowards, node, worldState, blockPlacement);
if (Main.SLOW_DEBUG && blockPlacement != -1 && !zobristCache.coalesceState(neighbor).blockExists(blockPlacement)) { // only in slow_debug because this force-allocates a WorldState for every neighbor of every node!
throw new IllegalStateException();
}
if (Main.DEBUG && node == neighbor) {
throw new IllegalStateException();
}
updateNeighbor(node, neighbor, edgeCost);
}
private void updateNeighbor(Node node, Node neighbor, int edgeCost) {
int currentCost = neighbor.cost;
int offeredCost = node.cost + edgeCost;
if (currentCost < offeredCost) {
return;
}
neighbor.previous = node;
neighbor.cost = offeredCost;
neighbor.combinedCost = offeredCost + neighbor.heuristic;
if (Main.DEBUG && neighbor.combinedCost < Integer.MIN_VALUE / 2) { // simple attempt to catch obvious overflow
throw new IllegalStateException();
}
if (neighbor.inHeap()) {
heap.update(neighbor);
} else {
heap.insert(neighbor);
}
}
private int calculateHeuristicModifier(WorldState previous, long blockPlacedAt) {
if (Main.DEBUG && previous.blockExists(blockPlacedAt)) {
throw new IllegalStateException();
}
if (true) {
throw new UnsupportedOperationException("tune the values first lol");
}
switch (engineInput.desiredToBePlaced(blockPlacedAt)) {
case PART_OF_CURRENT_GOAL:
case SCAFFOLDING_OF_CURRENT_GOAL:
return -100; // keep kitten on task
case PART_OF_FUTURE_GOAL:
return -10; // smaller kitten treat for working ahead
case SCAFFOLDING_OF_FUTURE_GOAL:
return -5; // smallest kitten treat for working ahead on scaffolding
case ANCILLARY:
return 0; // no kitten treat for placing a random extra block
default:
throw new IllegalStateException();
}
}
private int blockPlaceCost() {
// maybe like... ten?
throw new UnsupportedOperationException();
}
private Node getNode(long playerPosition, Face sneakingTowards, Node prev, WorldState prevWorld, long blockPlacement) {
if (Main.DEBUG && blockPlacement != -1 && prevWorld.blockExists(blockPlacement)) {
throw new IllegalStateException();
}
long worldStateZobristHash = prev.worldStateZobristHash;
if (blockPlacement != -1) {
worldStateZobristHash = WorldState.updateZobrist(worldStateZobristHash, blockPlacement);
}
long code = SneakPosition.encode(playerPosition, sneakingTowards) ^ worldStateZobristHash;
Node existing = nodes.get(code);
if (existing != null) {
return existing;
}
int newHeuristic = prev.heuristic;
if (blockPlacement != -1) {
newHeuristic += calculateHeuristicModifier(prevWorld, blockPlacement);
}
Node node = new Node(playerPosition, null, worldStateZobristHash, blockPlacement, newHeuristic);
if (Main.DEBUG && node.nodeMapKey() != code) {
throw new IllegalStateException();
}
nodes.put(code, node);
return node;
}
public BlockStateCachedData at(long pos, WorldState inWorldState) {
return engineInput.at(pos, inWorldState);
}
}

View File

@@ -0,0 +1,27 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public enum GreedySolverEngine implements ISolverEngine {
INSTANCE;
@Override
public SolverEngineOutput solve(SolverEngineInput in) {
return new GreedySolver(in).search();
}
}

View File

@@ -0,0 +1,22 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public enum Half {
TOP, BOTTOM, EITHER
}

View File

@@ -0,0 +1,44 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public interface IBlockStateDataProvider {
int numStates();
BlockStateCachedData getNullable(int i);
default BlockStateCachedData[] allNullable() {
BlockStateCachedData[] ret = new BlockStateCachedData[numStates()];
RuntimeException ex = null;
for (int i = 0; i < ret.length; i++) {
try {
ret[i] = getNullable(i);
} catch (RuntimeException e) {
if (ex != null) {
ex.printStackTrace(); // printstacktrace all but the one that we throw
}
ex = e;
}
}
if (ex != null) {
throw ex; // throw the last one
}
return ret;
}
}

View File

@@ -0,0 +1,21 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public interface INavigableSurface {
}

View File

@@ -0,0 +1,33 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import it.unimi.dsi.fastutil.longs.LongList;
public interface IReachabilityProvider {
LongList candidates(long playerEyeVoxel);
static IReachabilityProvider get(DependencyGraphScaffoldingOverlay overlay, PlayerReachSphere sphere) {
try {
return new ReachabilityCache(overlay, sphere);
} catch (ReachabilityCache.SchematicIsTooDenseForThisToMakeSenseException ex) {
return new ReachabilityLive(overlay, sphere);
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.LongList;
public interface IScaffolderStrategy {
// TODO passing in DGSO is not ideal because it's mutable
// TODO should it instead take in a list of (all) possible roots?
LongList scaffoldTo(DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph.CollapsedDependencyGraphComponent root, DependencyGraphScaffoldingOverlay overlayGraph);
}

View File

@@ -0,0 +1,23 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public interface ISolverEngine {
SolverEngineOutput solve(SolverEngineInput in);
}

View File

@@ -0,0 +1,346 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.pathing.movement.ActionCosts;
import baritone.api.utils.BetterBlockPos;
import baritone.builder.mc.DebugStates;
import baritone.builder.mc.VanillaBlockStateDataProvider;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Main {
public static final boolean DEBUG = true;
public static final boolean SLOW_DEBUG = false;
public static final boolean VERY_SLOW_DEBUG = false;
/**
* If true, many different parts of the builder switch to a more efficient mode where blocks can only be placed adjacent or upwards, never downwards
*/
public static final boolean STRICT_Y = true;
public static final BlockData DATA = new BlockData(new VanillaBlockStateDataProvider());
public static final Random RAND = new Random(5021);
public static void main() throws InterruptedException {
System.out.println("Those costs are " + (ActionCosts.FALL_N_BLOCKS_COST[2] / 2) + " and " + ActionCosts.JUMP_ONE_BLOCK_COST + " and " + ActionCosts.FALL_N_BLOCKS_COST[1]);
for (Face face : Face.VALUES) {
System.out.println(face);
System.out.println(face.x);
System.out.println(face.y);
System.out.println(face.z);
System.out.println(face.index);
System.out.println(face.offset);
System.out.println(face.oppositeIndex);
System.out.println("Horizontal " + face.horizontalIndex);
}
{
System.out.println("Without");
long start = BetterBlockPos.toLong(5021, 69, 420);
System.out.println(BetterBlockPos.fromLong(start));
start += Face.UP.offset;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.DOWN.offset;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.UP.offset;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.DOWN.offset;
System.out.println(BetterBlockPos.fromLong(start));
}
{
System.out.println("With");
long start = BetterBlockPos.toLong(5021, 69, 420);
System.out.println(BetterBlockPos.fromLong(start));
start += Face.UP.offset;
start &= BetterBlockPos.POST_ADDITION_MASK;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.DOWN.offset;
start &= BetterBlockPos.POST_ADDITION_MASK;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.UP.offset;
start &= BetterBlockPos.POST_ADDITION_MASK;
System.out.println(BetterBlockPos.fromLong(start));
start += Face.DOWN.offset;
start &= BetterBlockPos.POST_ADDITION_MASK;
System.out.println(BetterBlockPos.fromLong(start));
}
{
System.out.println(BetterBlockPos.fromLong(BetterBlockPos.toLong(150, 150, 150)));
}
for (int i = 0; i < 0; i++) {
Stream.of(new Object())
.flatMap(ignored -> IntStream.range(0, 100).boxed())
.parallel()
.forEach(x -> System.out.println(x + ""));
IntStream.range(100, 200).boxed()
.parallel()
.forEach(x -> System.out.println(x + ""));
Stream.of(new Object())
.flatMap(ignored -> IntStream.range(200, 300).boxed())
.collect(Collectors.toList()).parallelStream()
.forEach(x -> System.out.println(x + ""));
}
for (int aaaa = 0; aaaa < 0; aaaa++) {
/*Raytracer.raytraceMode++;
Raytracer.raytraceMode %= 3;*/
Random rand = new Random(5021);
DoubleArrayList A = new DoubleArrayList();
DoubleArrayList B = new DoubleArrayList();
DoubleArrayList C = new DoubleArrayList();
DoubleArrayList D = new DoubleArrayList();
DoubleArrayList E = new DoubleArrayList();
DoubleArrayList F = new DoubleArrayList();
LongArrayList G = new LongArrayList();
long a = System.currentTimeMillis();
for (int trial = 0; trial < 10_000_000; ) {
Vec3d playerEye = new Vec3d(rand.nextDouble() * 5 - 2.5, rand.nextDouble() * 5, rand.nextDouble() * 5 - 2.5);
long eyeBlock = playerEye.getRoundedToZeroPositionUnsafeDontUse();
if (eyeBlock == 0) {
// origin, unlucky
continue;
}
Face placeToAgainst = Face.VALUES[rand.nextInt(Face.NUM_FACES)];
Face againstToPlace = placeToAgainst.opposite();
long placeAgainst = placeToAgainst.offset(0);
if (eyeBlock == placeAgainst) {
continue;
}
double[] hitVec = new double[3];
for (int i = 0; i < 3; i++) {
switch (placeToAgainst.vec[i]) {
case -1: {
hitVec[i] = 0;
break;
}
case 0: {
hitVec[i] = rand.nextDouble();
break;
}
case 1: {
hitVec[i] = 1;
break;
}
}
}
Vec3d hit = new Vec3d(hitVec);
Raytracer.runTrace(playerEye, placeAgainst, againstToPlace, hit);
A.add(playerEye.x);
B.add(playerEye.y);
C.add(playerEye.z);
D.add(hit.x);
E.add(hit.y);
F.add(hit.z);
G.add(placeAgainst);
trial++;
}
long b = System.currentTimeMillis();
System.out.println("Nominal first run with overhead: " + (b - a) + "ms");
boolean checkAgainst = true;
for (int i = 0; i < 10_000_000; i++) {
if (i % 1000 == 0 && checkAgainst) {
System.out.println(i);
}
LongArrayList normal = Raytracer.rayTraceZoomy(A.getDouble(i), B.getDouble(i), C.getDouble(i), D.getDouble(i), E.getDouble(i), F.getDouble(i), G.getLong(i));
LongArrayList alternate = Raytracer.rayTraceZoomyAlternate(A.getDouble(i), B.getDouble(i), C.getDouble(i), D.getDouble(i), E.getDouble(i), F.getDouble(i), G.getLong(i));
if (!normal.equals(alternate)) {
throw new IllegalStateException();
}
if (checkAgainst) {
LongArrayList superSlow = Raytracer.rayTraceFast(A.getDouble(i), B.getDouble(i), C.getDouble(i), D.getDouble(i), E.getDouble(i), F.getDouble(i));
if (!normal.equals(superSlow)) {
Raytracer.print(normal);
Raytracer.print(superSlow);
checkAgainst = false;
}
}
}
for (int it = 0; it < 20; it++) {
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
Raytracer.rayTraceZoomy(A.getDouble(i), B.getDouble(i), C.getDouble(i), D.getDouble(i), E.getDouble(i), F.getDouble(i), G.getLong(i));
}
long end = System.currentTimeMillis();
System.out.println("Normal took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
Raytracer.rayTraceZoomyAlternate(A.getDouble(i), B.getDouble(i), C.getDouble(i), D.getDouble(i), E.getDouble(i), F.getDouble(i), G.getLong(i));
}
long end = System.currentTimeMillis();
System.out.println("Alternate took " + (end - start) + "ms");
}
}
}
{
DebugStates.debug();
}
for (int aaaa = 0; aaaa < 0; aaaa++) {
Random rand = new Random(5021);
int trials = 10_000_000;
int[] X = new int[trials];
int[] Y = new int[trials];
int[] Z = new int[trials];
int sz = 10;
CuboidBounds bounds = new CuboidBounds(sz, sz, sz);
for (int i = 0; i < trials; i++) {
for (int[] toAdd : new int[][]{X, Y, Z}) {
toAdd[i] = rand.nextBoolean() ? rand.nextInt(sz) : rand.nextBoolean() ? -1 : sz;
}
}
boolean[] a = new boolean[trials];
boolean[] b = new boolean[trials];
boolean[] c = new boolean[trials];
boolean[] d = new boolean[trials];
boolean[] e = new boolean[trials];
for (int it = 0; it < 20; it++) {
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
a[i] = bounds.inRangeBranchy(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchy took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
b[i] = bounds.inRangeBranchless(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
c[i] = bounds.inRangeBranchless2(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless2 took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
d[i] = bounds.inRangeBranchless3(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless3 took " + (end - start) + "ms");
}
{
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
long start = System.currentTimeMillis();
for (int i = 0; i < trials; i++) {
e[i] = bounds.inRangeBranchless4(X[i], Y[i], Z[i]);
}
long end = System.currentTimeMillis();
System.out.println("Branchless4 took " + (end - start) + "ms");
}
/*
Branchless2 took 55ms
Branchless3 took 53ms
Branchless4 took 47ms
Branchy took 137ms
Branchless took 35ms
Branchless2 took 36ms
Branchless3 took 35ms
Branchless4 took 41ms
Branchy took 118ms
Branchless took 33ms
Branchless2 took 39ms
Branchless3 took 36ms
Branchless4 took 42ms
Branchy took 125ms
Branchless took 41ms
Branchless2 took 45ms
Branchless3 took 41ms
Branchless4 took 45ms
Branchy took 123ms
Branchless took 38ms
Branchless2 took 43ms
Branchless3 took 35ms
Branchless4 took 43ms
Branchy took 117ms
Branchless took 37ms
Branchless2 took 42ms
Branchless3 took 41ms
Branchless4 took 45ms
Branchy took 123ms
Branchless took 35ms
Branchless2 took 42ms
Branchless3 took 38ms
Branchless4 took 46ms
Branchy took 126ms
Branchless took 34ms
Branchless2 took 47ms
Branchless3 took 40ms
Branchless4 took 47ms
Branchy took 124ms
*/
// 3 is better than 2 and 4 because of data dependency
// the L1 cache fetch for this.sizeX can happen at the same time as "x+1" (which is an increment of an argument)
// in other words: in options 2 and 4, the "+1" or "-1" has a data dependency on the RAM fetch for this.sizeX, but in option 3 alone, the +1 happens upon the argument x, which is likely in a register, meaning it can be pipelined in parallel with the L1 cache fetch for this.sizeX
}
}
/*{ // proguard test
PlayerPhysics.determinePlayerRealSupport(BlockStateCachedData.get(69), BlockStateCachedData.get(420));
PlayerPhysics.determinePlayerRealSupport(BlockStateCachedData.get(420), BlockStateCachedData.get(69));
}*/
{
for (int sneak = 0; sneak < 4; sneak++) {
System.out.println("meow");
System.out.println(sneak);
System.out.println(SneakPosition.encode(0, sneak));
System.out.println(SneakPosition.encode(BetterBlockPos.POST_ADDITION_MASK, sneak));
System.out.println(SneakPosition.decode(SneakPosition.encode(0, sneak)));
System.out.println(SneakPosition.decode(SneakPosition.encode(BetterBlockPos.POST_ADDITION_MASK, sneak)));
}
}
System.exit(0);
}
}

View File

@@ -0,0 +1,126 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import baritone.builder.utils.com.github.btrekkie.connectivity.Augmentation;
import baritone.builder.utils.com.github.btrekkie.connectivity.ConnGraph;
import java.util.Arrays;
import java.util.function.Function;
public class NavigableSurface {
private final CuboidBounds bounds;
private final BlockStateCachedData[] blocks; // TODO switch to xzy ordering so columnFrom is faster
private final ConnGraph connGraph;
private final Function<BetterBlockPos, Object> genVertexAugmentation;
private final Column col1 = new Column();
private final Column col2 = new Column();
public NavigableSurface(int x, int y, int z, Augmentation augmentation, Function<BetterBlockPos, Object> genVertexAugmentation) {
this.bounds = new CuboidBounds(x, y, z);
this.blocks = new BlockStateCachedData[bounds.volume()];
Arrays.fill(blocks, FakeStates.AIR);
this.genVertexAugmentation = genVertexAugmentation;
this.connGraph = new ConnGraph(augmentation);
if (!genVertexAugmentation.apply(new BetterBlockPos(0, 0, 0)).equals(genVertexAugmentation.apply(new BetterBlockPos(0, 0, 0)))) {
throw new IllegalStateException("RedBlackNode optimization requires correct impl of .equals on the attachment, to avoid percolating up spurious augmentation non-updates");
}
}
private void columnFrom(Column column, long pos) {
column.underneath = getBlockOrAir((pos + Column.DOWN_1) & BetterBlockPos.POST_ADDITION_MASK);
column.feet = getBlockOrAir(pos);
column.head = getBlockOrAir((pos + Column.UP_1) & BetterBlockPos.POST_ADDITION_MASK);
column.above = getBlockOrAir((pos + Column.UP_2) & BetterBlockPos.POST_ADDITION_MASK);
column.aboveAbove = getBlockOrAir((pos + Column.UP_3) & BetterBlockPos.POST_ADDITION_MASK);
column.init();
}
protected void setBlock(long pos, BlockStateCachedData data) {
blocks[bounds.toIndex(pos)] = data;
for (int dy = -2; dy <= 1; dy++) {
long couldHaveChanged = BetterBlockPos.offsetBy(pos, 0, dy, 0);
columnFrom(col1, couldHaveChanged);
boolean currentlyAllowed = col1.standing();
if (currentlyAllowed) {
// TODO skip the next line if it already has an augmentation?
connGraph.setVertexAugmentation(couldHaveChanged, genVertexAugmentation.apply(BetterBlockPos.fromLong(couldHaveChanged)));
for (Face dir : Face.HORIZONTALS) {
long adj = dir.offset(couldHaveChanged);
columnFrom(col2, adj);
Integer connDy = PlayerPhysics.bidirectionalPlayerTravel(col1, col2, getBlockOrAir((adj + Column.DOWN_2) & BetterBlockPos.POST_ADDITION_MASK), getBlockOrAir((adj + Column.DOWN_3) & BetterBlockPos.POST_ADDITION_MASK));
for (int fakeDy = -2; fakeDy <= 2; fakeDy++) {
long neighbor = BetterBlockPos.offsetBy(adj, 0, fakeDy, 0);
if (((Integer) fakeDy).equals(connDy)) {
connGraph.addEdge(couldHaveChanged, neighbor);
} else {
connGraph.removeEdge(couldHaveChanged, neighbor);
}
}
}
} else {
connGraph.removeVertexAugmentation(couldHaveChanged);
for (Face dir : Face.HORIZONTALS) {
long adj = dir.offset(couldHaveChanged);
for (int fakeDy = -2; fakeDy <= 2; fakeDy++) {
connGraph.removeEdge(couldHaveChanged, BetterBlockPos.offsetBy(adj, 0, fakeDy, 0));
}
}
}
}
}
protected void setBlock(int x, int y, int z, BlockStateCachedData data) {
setBlock(BetterBlockPos.toLong(x, y, z), data);
}
public CuboidBounds bounds() {
return bounds;
}
public BlockStateCachedData getBlock(long pos) {
return blocks[bounds.toIndex(pos)];
}
public BlockStateCachedData getBlockOrAir(long pos) {
if (!bounds.inRangePos(pos)) {
return FakeStates.AIR;
}
return getBlock(pos);
}
public boolean hasBlock(BetterBlockPos pos) {
return getBlockOrAir(pos.toLong()).collidesWithPlayer;
}
public boolean connected(BetterBlockPos a, BetterBlockPos b) {
return connGraph.connected(a.toLong(), b.toLong());
}
public Object getComponentAugmentation(BetterBlockPos pos) { // maybe should be protected? subclass defines it anyway
return connGraph.getComponentAugmentation(pos.toLong());
}
}

View File

@@ -0,0 +1,66 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
public class Node {
private final long posAndSneak;
public final long worldStateZobristHash;
public final int heuristic;
public int cost;
public int combinedCost;
public Node previous;
int heapPosition;
// boolean unrealizedZobristBlockChange; // no longer needed since presence in the overall GreedySolver zobristWorldStateCache indicates if this is a yet-unrealized branch of the zobrist space
long packedUnrealizedCoordinate;
// int unrealizedState; // no longer needed now that world state is binarized with scaffolding/build versus air
// long unrealizedZobristParentHash; // no longer needed since we can compute it backwards with XOR
public Node(long pos, Face sneakingTowards, long zobristState, long unrealizedBlockPlacement, int heuristic) {
this.posAndSneak = SneakPosition.encode(pos, sneakingTowards);
this.heapPosition = -1;
this.cost = Integer.MAX_VALUE;
this.heuristic = heuristic;
this.worldStateZobristHash = zobristState;
this.packedUnrealizedCoordinate = unrealizedBlockPlacement;
}
public boolean sneaking() {
return SneakPosition.hasSneak(posAndSneak);
}
public long pos() {
return posAndSneak & BetterBlockPos.POST_ADDITION_MASK;
}
public long sneakingPosition() {
return posAndSneak;
}
public long nodeMapKey() {
return posAndSneak ^ worldStateZobristHash;
}
public boolean inHeap() {
return heapPosition >= 0;
}
}

View File

@@ -0,0 +1,110 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.Arrays;
public class NodeBinaryHeap {
private static final int INITIAL_CAPACITY = 1024;
private Node[] array;
private int size;
public NodeBinaryHeap() {
this.size = INITIAL_CAPACITY;
this.array = new Node[this.size];
}
public int size() {
return size;
}
public void insert(Node value) {
if (size >= array.length - 1) {
array = Arrays.copyOf(array, array.length << 1);
}
size++;
value.heapPosition = size;
array[size] = value;
update(value);
}
public void update(Node val) {
int index = val.heapPosition;
int parentInd = index >>> 1;
int cost = val.combinedCost;
Node parentNode = array[parentInd];
while (index > 1 && parentNode.combinedCost > cost) {
array[index] = parentNode;
array[parentInd] = val;
val.heapPosition = parentInd;
parentNode.heapPosition = index;
index = parentInd;
parentInd = index >>> 1;
parentNode = array[parentInd];
}
}
public boolean isEmpty() {
return size == 0;
}
public Node removeLowest() {
if (size == 0) {
throw new IllegalStateException();
}
Node result = array[1];
Node val = array[size];
array[1] = val;
val.heapPosition = 1;
array[size] = null;
size--;
result.heapPosition = -1;
if (size < 2) {
return result;
}
int index = 1;
int smallerChild = 2;
int cost = val.combinedCost;
do {
Node smallerChildNode = array[smallerChild];
int smallerChildCost = smallerChildNode.combinedCost;
if (smallerChild < size) {
Node rightChildNode = array[smallerChild + 1];
int rightChildCost = rightChildNode.combinedCost;
if (smallerChildCost > rightChildCost) {
smallerChild++;
smallerChildCost = rightChildCost;
smallerChildNode = rightChildNode;
}
}
if (cost <= smallerChildCost) {
break;
}
array[index] = smallerChildNode;
array[smallerChild] = val;
val.heapPosition = smallerChild;
smallerChildNode.heapPosition = index;
index = smallerChild;
} while ((smallerChild <<= 1) <= size);
return result;
}
}

View File

@@ -0,0 +1,70 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.Arrays;
public class PackedBlockStateCuboid {
public final Bounds bounds;
private final BlockStateCachedData[] states;
private final BlockStateCachedData[] statesWithScaffolding;
private PackedBlockStateCuboid(int x, int y, int z) {
this.bounds = new CuboidBounds(x, y, z);
this.states = new BlockStateCachedData[bounds.volume()];
this.statesWithScaffolding = new BlockStateCachedData[bounds.volume()];
}
public PackedBlockStateCuboid(int[][][] blockStates, BlockData data) {
this(blockStates.length, blockStates[0].length, blockStates[0][0].length);
bounds.forEach((x, y, z) -> states[bounds.toIndex(x, y, z)] = data.get(blockStates[x][y][z]));
genScaffoldVariant();
}
public PackedBlockStateCuboid(BlockStateCachedData[][][] blockStates) {
this(blockStates.length, blockStates[0].length, blockStates[0][0].length);
bounds.forEach((x, y, z) -> states[bounds.toIndex(x, y, z)] = blockStates[x][y][z]);
genScaffoldVariant();
}
public static void fillWithAir(BlockStateCachedData[][][] states) {
for (BlockStateCachedData[][] layer : states) {
for (BlockStateCachedData[] slice : layer) {
Arrays.fill(slice, FakeStates.AIR);
}
}
}
private void genScaffoldVariant() {
for (int i = 0; i < states.length; i++) {
if (PlaceOrderDependencyGraph.treatedAsScaffolding(states[i])) {
throw new IllegalStateException("including FakeStates.SCAFFOLDING will confuse the place order dependency graph. use an alternate block like FakeStates.SOLID");
}
statesWithScaffolding[i] = states[i].isAir ? FakeStates.SCAFFOLDING : states[i];
}
}
public BlockStateCachedData get(int index) {
return states[index];
}
public BlockStateCachedData getScaffoldingVariant(int index) {
return statesWithScaffolding[index];
}
}

View File

@@ -0,0 +1,151 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
/**
* If you want to place against me, there's some things you gotta know
*/
public class PlaceAgainstData {
public final Face against;
public final boolean mustSneak; // like if its a crafting table
private final Vec3d[] hits;
private final boolean top;
private final boolean bottom;
public PlaceAgainstData(Face against, Vec3d[] hits, boolean mustSneak) {
this.mustSneak = mustSneak;
this.against = against;
boolean top = false;
boolean bottom = false;
for (Vec3d hit : hits) {
if (!validatePossibleHit(hit)) {
throw new IllegalArgumentException();
}
if (!against.vertical) {
if (BlockStatePlacementOption.hitOk(Half.BOTTOM, hit)) {
bottom = true;
}
if (BlockStatePlacementOption.hitOk(Half.TOP, hit)) {
top = true;
}
}
}
this.top = top;
this.bottom = bottom;
this.hits = hits;
if (!streamRelativeToMyself().allMatch(Vec3d::inOriginUnitVoxel)) {
throw new IllegalStateException();
}
if (!streamRelativeToPlace().allMatch(Vec3d::inOriginUnitVoxel)) {
throw new IllegalStateException();
}
if (hits.length == 0) {
throw new IllegalStateException();
}
}
public PlaceAgainstData(Face against, Half half, boolean mustSneak) {
this(against, project(against.opposite(), half), mustSneak);
if (against.vertical && half != Half.EITHER) {
throw new IllegalStateException();
}
}
public Stream<Vec3d> streamRelativeToPlace() {
return Stream.of(hits);
}
public Stream<Vec3d> streamRelativeToMyself() {
return streamRelativeToPlace().map(v -> v.plus(against.x, against.y, against.z));
}
private boolean validatePossibleHit(Vec3d hit) {
double[] h = {hit.x, hit.y, hit.z};
for (int i = 0; i < 3; i++) {
switch (against.vec[i]) {
case -1: {
if (h[i] != 1) {
return false;
}
break;
}
case 0: {
if (h[i] < 0.05 || h[i] > 0.95) {
return false;
}
break;
}
case 1: {
if (h[i] != 0) {
return false;
}
break;
}
}
}
return true;
}
// TODO for playerMustBeHorizontalFacing, do i need something like andThatOptionExtendsTheFullHorizontalSpaceOfTheVoxel()?
public boolean presentsAnOptionStrictlyInTheTopHalfOfTheStandardVoxelPlane() {
if (Main.DEBUG && against.vertical) {
throw new IllegalStateException();
}
return top;
}
public boolean presentsAnOptionStrictlyInTheBottomHalfOfTheStandardVoxelPlane() {
if (Main.DEBUG && against.vertical) {
throw new IllegalStateException();
}
return bottom;
}
private static final double[] LOCS = {0.1, 0.5, 0.9};
private static Vec3d[] project(Face ontoFace, Half filterHalf) {
return DoubleStream
.of(LOCS)
.boxed()
.flatMap(dx -> DoubleStream.of(LOCS).mapToObj(dz -> new double[]{dx, dz}))
.map(faceHit -> project(faceHit, ontoFace))
.map(Vec3d::new)
.filter(vec -> ontoFace.vertical || BlockStatePlacementOption.hitOk(filterHalf, vec))
.toArray(Vec3d[]::new);
}
private static double[] project(double[] faceHit, Face ontoFace) {
double[] ret = new double[3];
int j = 0;
for (int i = 0; i < 3; i++) {
if (ontoFace.vec[i] == 0) {
ret[i] = faceHit[j++];
} else {
if (ontoFace.vec[i] == 1) {
ret[i] = 1;
} // else leave it as zero
}
}
return ret;
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
public class PlaceOptions {
double blockReachDistance = 4;
DependencyGraphScaffoldingOverlay overlay;
IReachabilityProvider provider = IReachabilityProvider.get(overlay, new PlayerReachSphere(blockReachDistance));
public void whatCouldIDo(int playerX, int playerFeetBlips, int playerZ) {
int playerEyeBlips = playerFeetBlips + Blip.FEET_TO_EYE_APPROX;
// TODO ugh how tf to deal with sneaking UGH. maybe like if (playerEyeBlips % 16 < 2) { also run all candidates from one voxel lower down because if we snuck our eye would be in there}
int voxelY = playerEyeBlips / Blip.FULL_BLOCK;
long pos = BetterBlockPos.toLong(playerX, voxelY, playerZ);
for (long blockPos : provider.candidates(pos)) {
BlockStateCachedData placingAgainst = overlay.data(blockPos);
outer:
for (Face againstToPlace : Face.VALUES) {
Face placeToAgainst = againstToPlace.opposite();
if (overlay.outgoingEdge(blockPos, againstToPlace)) {
long placingBlockAt = againstToPlace.offset(blockPos);
BlockStateCachedData blockBeingPlaced = overlay.data(placingBlockAt);
for (BlockStatePlacementOption option : blockBeingPlaced.placeMe) {
if (option.against == placeToAgainst) {
PlaceAgainstData againstData = placingAgainst.againstMe(option);
int relativeX = playerX - BetterBlockPos.XfromLong(placingBlockAt);
int relativeY = playerFeetBlips - Blip.FULL_BLOCK * BetterBlockPos.YfromLong(placingBlockAt);
int relativeZ = playerZ - BetterBlockPos.ZfromLong(placingBlockAt);
for (Raytracer.Raytrace trace : option.computeTraceOptions(againstData, relativeX, relativeY, relativeZ, PlayerVantage.LOOSE_CENTER, blockReachDistance)) {
// yay, gold star
}
continue outer;
}
}
throw new IllegalStateException();
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
/**
* An immutable graph representing block placement dependency order
* <p>
* Air blocks are treated as scaffolding!
* (the idea is that treating air blocks as air would be boring - nothing can place against them and they can't be placed against anything)
* <p>
* Edge A --> B means that B can be placed against A
*/
public class PlaceOrderDependencyGraph {
private final PackedBlockStateCuboid states;
private final byte[] edges;
public PlaceOrderDependencyGraph(PackedBlockStateCuboid states) {
this.states = states;
this.edges = new byte[bounds().volume()];
bounds().forEach(this::compute);
}
private void compute(long pos) {
byte val = 0;
for (BlockStatePlacementOption option : data(pos).placeMe) {
if (Main.STRICT_Y && option.against == Face.UP) {
throw new IllegalStateException();
}
long againstPos = option.against.offset(pos);
BlockStateCachedData against;
if (inRange(againstPos)) {
against = data(againstPos);
} else {
against = FakeStates.SCAFFOLDING;
}
if (against.possibleAgainstMe(option)) {
val |= 1 << option.against.index;
}
}
edges[states.bounds.toIndex(pos)] = val;
}
public BlockStateCachedData data(long pos) {
return states.getScaffoldingVariant(bounds().toIndex(pos));
}
// example: dirt at 0,0,0 torch at 0,1,0. outgoingEdge(0,0,0,UP) returns true, incomingEdge(0,1,0,DOWN) returns true
public boolean outgoingEdge(long pos, Face face) {
if (!inRange(pos)) {
return false;
}
return incomingEdge(face.offset(pos), face.opposite());
}
public boolean incomingEdge(long pos, Face face) {
if (!inRange(face.offset(pos))) {
return false;
}
return incomingEdgePermitExterior(pos, face);
}
public boolean incomingEdgePermitExterior(long pos, Face face) {
if (!inRange(pos)) {
return false;
}
return (edges[bounds().toIndex(pos)] & 1 << face.index) != 0;
}
public boolean airTreatedAsScaffolding(long pos) {
return treatedAsScaffolding(data(pos));
}
private boolean inRange(long pos) {
return bounds().inRangePos(pos);
}
public Bounds bounds() {
return states.bounds;
}
public static boolean treatedAsScaffolding(BlockStateCachedData data) {
return data == FakeStates.SCAFFOLDING;
}
}

View File

@@ -0,0 +1,391 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
public class PlayerPhysics {
public static int determinePlayerRealSupportLevel(BlockStateCachedData underneath, BlockStateCachedData within, VoxelResidency residency) {
switch (residency) {
case STANDARD_WITHIN_SUPPORT:
return within.collisionHeightBlips();
case UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK:
return underneath.collisionHeightBlips() - Blip.FULL_BLOCK;
default:
return -1;
}
}
public static int determinePlayerRealSupportLevel(BlockStateCachedData underneath, BlockStateCachedData within) {
return determinePlayerRealSupportLevel(underneath, within, canPlayerStand(underneath, within));
}
/**
* the player Y is within within. i.e. the player Y is greater than or equal within and less than within+1
* <p>
* underneath is the block underneath that, which we annoyingly also have to check due to fences and other blocks that are taller than a block
*/
public static VoxelResidency canPlayerStand(BlockStateCachedData underneath, BlockStateCachedData within) {
if (within.collidesWithPlayer) {
if (underneath.collidesWithPlayer && underneath.collisionHeightBlips() - Blip.FULL_BLOCK > within.collisionHeightBlips()) { // > because imagine something like slab on top of fence, we can walk on the slab even though the fence is equivalent height
if (!underneath.fullyWalkableTop) {
return VoxelResidency.PREVENTED_BY_UNDERNEATH;
}
return VoxelResidency.UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK; // this could happen if "underneath" is a fence and "within" is a carpet
}
if (!within.fullyWalkableTop) {
return VoxelResidency.PREVENTED_BY_WITHIN;
}
if (within.collisionHeightBlips() >= Blip.FULL_BLOCK) {
return VoxelResidency.IMPOSSIBLE_WITHOUT_SUFFOCATING;
}
return VoxelResidency.STANDARD_WITHIN_SUPPORT;
} else {
if (!underneath.collidesWithPlayer) {
return VoxelResidency.FLOATING;
}
if (!underneath.fullyWalkableTop) {
return VoxelResidency.PREVENTED_BY_UNDERNEATH;
}
if (underneath.collisionHeightBlips() < Blip.FULL_BLOCK) { // short circuit only calls collisionHeightBlips when fullyWalkableTop is true, so this is safe
return VoxelResidency.FLOATING;
}
return VoxelResidency.UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK;
}
}
public enum VoxelResidency {
UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK, // i fucking hate notch for adding fences to the game. anyway this means that the height is underneath.collisionHeightBlips minus a full block.
STANDARD_WITHIN_SUPPORT, // :innocent: emoji, the height is simply the collisionHeightBlips of the within
IMPOSSIBLE_WITHOUT_SUFFOCATING, // aka: um we are literally underground
FLOATING, // aka: um we are literally floating in midair
PREVENTED_BY_UNDERNEATH, // fences :woozy_face:
PREVENTED_BY_WITHIN, // what are you even thinking?
}
public static boolean valid(VoxelResidency res) {
// FWIW this is equivalent to "return determinePlayerRealSupportLevel > 0"
switch (res) {
case UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK:
case STANDARD_WITHIN_SUPPORT:
return true;
default:
return false;
}
}
public static Collision playerTravelCollides(Column within, Column into) {
if (Main.DEBUG && within.head.collidesWithPlayer) {
throw new IllegalStateException();
}
return playerTravelCollides(within.feetBlips,
within.above,
into.above,
into.head,
into.feet,
into.underneath,
within.underneath,
within.feet,
within.aboveAbove,
into.aboveAbove);
}
/**
* Is there a movement (ascend, level, descend) that the player can do AND UNDO?
* This is basically the same as playerTravelCollides, but, it's okay to fall, but only as long as you don't fall further than what can be jumped back up
*
* @return the integer change in voxel Y, or null if there is no such option
*/
public static Integer bidirectionalPlayerTravel(
Column within,
Column into,
BlockStateCachedData underUnderneathInto,
BlockStateCachedData underUnderUnderneathInto
) {
switch (playerTravelCollides(within, into)) {
case BLOCKED:
return null;
case JUMP_TO_VOXEL_TWO_UP:
return 2;
case VOXEL_UP:
case JUMP_TO_VOXEL_UP:
return 1;
case VOXEL_LEVEL:
case JUMP_TO_VOXEL_LEVEL:
return 0;
case FALL:
if (Main.DEBUG && (canPlayerStand(into.underneath, into.feet) != VoxelResidency.FLOATING || !into.playerCanExistAtFootBlip(within.feetBlips))) {
throw new IllegalStateException();
}
VoxelResidency downOne = canPlayerStand(underUnderneathInto, into.underneath);
switch (downOne) {
case PREVENTED_BY_UNDERNEATH:
case PREVENTED_BY_WITHIN:
return null; // a block we aren't allowed to stand on
case UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK:
case STANDARD_WITHIN_SUPPORT:
int fallBy = within.feetBlips + Blip.FULL_BLOCK - determinePlayerRealSupportLevel(underUnderneathInto, into.underneath, downOne);
if (fallBy < 1) {
throw new IllegalStateException();
}
if (fallBy <= Blip.JUMP) {
return -1; // we could jump back! yay!
}
return null; // one-way trip
default:
throw new IllegalStateException();
case FLOATING:
break;
}
// ^ as shown, we are floating in downOne, so let's try downTwo
// but first let's make sure it's possible
int highestBlipsWithinUnderUnderUnder = Blip.FULL_BLOCK - 1; // 15
int highestJumpFromTwoUnder = highestBlipsWithinUnderUnderUnder + Blip.JUMP; // 35
int highestJumpIntoThisVoxel = highestJumpFromTwoUnder - Blip.TWO_BLOCKS; // 3
if (within.feetBlips > highestJumpIntoThisVoxel) {
return null;
}
VoxelResidency downTwo = canPlayerStand(underUnderUnderneathInto, underUnderneathInto);
switch (downTwo) {
case UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK: // tallest block is 24 blips, which is less than 29, so this case is impossible
case PREVENTED_BY_UNDERNEATH:
case PREVENTED_BY_WITHIN:
case FLOATING:
return null;
case STANDARD_WITHIN_SUPPORT:
int fallBy = within.feetBlips + Blip.TWO_BLOCKS - determinePlayerRealSupportLevel(underUnderUnderneathInto, underUnderneathInto, downTwo);
if (fallBy < Blip.FULL_BLOCK + 1) {
throw new IllegalStateException();
}
if (fallBy <= Blip.JUMP) {
return -2; // we could jump back! yay!
}
return null; // one-way trip
default:
throw new IllegalStateException();
}
default:
throw new IllegalStateException();
}
}
/**
* "Can the player walk forwards without needing to break anything?"
* <p>
* Takes into account things like the automatic +0.5 from walking into a slab.
* <p>
* Player is standing at X (feet) Y (head) on top of S, intends to walk forwards into this ABCD column
* EF
* UA
* YB
* XC
* SD
*/
public static Collision playerTravelCollides(int feetBlips,
BlockStateCachedData U,
BlockStateCachedData A,
BlockStateCachedData B,
BlockStateCachedData C,
BlockStateCachedData D,
BlockStateCachedData S,
BlockStateCachedData X,
BlockStateCachedData E,
BlockStateCachedData F) {
if (Main.DEBUG && (feetBlips < 0 || feetBlips >= Blip.FULL_BLOCK)) {
throw new IllegalStateException();
}
if (Main.DEBUG && (feetBlips != determinePlayerRealSupportLevel(S, X))) {
throw new IllegalStateException();
}
boolean alreadyWithinU = protrudesIntoThirdBlock(feetBlips);
if (Main.DEBUG && (alreadyWithinU && U.collidesWithPlayer)) {
throw new IllegalStateException();
}
int couldJumpUpTo = feetBlips + Blip.JUMP;
int couldStepUpTo = feetBlips + Blip.HALF_BLOCK;
if (couldJumpUpTo >= Blip.TWO_BLOCKS && !E.collidesWithPlayer && !F.collidesWithPlayer) {
// probably blocked, but maybe could we stand on A?
// imagine X is soul sand, A is carpet. that jump is possible
int jumpUpTwo = determinePlayerRealSupportLevel(B, A);
if (jumpUpTwo >= 0 && jumpUpTwo <= couldJumpUpTo - Blip.TWO_BLOCKS) {
if (Main.DEBUG && (!alreadyWithinU || protrudesIntoThirdBlock(jumpUpTwo))) {
throw new IllegalStateException(); // numeric impossibilities
}
return Collision.JUMP_TO_VOXEL_TWO_UP;
}
}
if (alreadyWithinU && A.collidesWithPlayer) { // now that voxel two up, the result within A, is eliminated, we can't proceed if A would block at head level
return Collision.BLOCKED; // we are too tall. bonk!
}
// D cannot prevent us from doing anything because it cant be higher than 1.5. therefore, makes sense to check CB before DC.
int voxelUp = determinePlayerRealSupportLevel(C, B);
if (voxelUp >= 0) {
// fundamentally a step upwards, from X to B instead of X to C
// too high?
if (A.collidesWithPlayer || U.collidesWithPlayer) {
return Collision.BLOCKED;
}
if (protrudesIntoThirdBlock(voxelUp) && (E.collidesWithPlayer || F.collidesWithPlayer)) {
return Collision.BLOCKED;
}
int heightRelativeToStartVoxel = voxelUp + Blip.FULL_BLOCK;
if (heightRelativeToStartVoxel > couldJumpUpTo) {
return Collision.BLOCKED;
}
if (heightRelativeToStartVoxel > couldStepUpTo) {
return Collision.JUMP_TO_VOXEL_UP;
} // else this is possible!
if (Main.DEBUG && (U.collidesWithPlayer || A.collidesWithPlayer || !alreadyWithinU)) {
// must already be colliding with U because in order for this non-jump voxel-up step to be even possible, feet must be at least HALF_BLOCK
throw new IllegalStateException();
}
// B can collide with player here, such as if X is soul sand, C is full block, and B is carpet
return Collision.VOXEL_UP; // A is already checked, so that's it!
}
// voxelUp is impossible. pessimistically, this means B is colliding. optimistically, this means B and C are air.
if (B.collidesWithPlayer) {
// voxel up and voxel two up are both eliminated, so...
return Collision.BLOCKED; // we have ruled out stepping on top of B, so now if B is still colliding there is no way forward
}
int stayLevel = determinePlayerRealSupportLevel(D, C);
if (stayLevel >= 0) {
// fundamentally staying within the same vertical voxel, X -> C
if (protrudesIntoThirdBlock(stayLevel) && !alreadyWithinU) { // step up, combined with our height, protrudes into U and A, AND we didn't already
if (U.collidesWithPlayer) { // stayLevel could even be LESS than feet
return Collision.BLOCKED;
}
if (A.collidesWithPlayer) { // already checked (alreadyWithinU && A.collidesWithPlayer) earlier
return Collision.BLOCKED;
}
}
if (stayLevel > couldStepUpTo) { // staying within the same voxel means that a jump will always succeed
// aka. the jump might headbonk but not in a way that prevents the action. also TODO this is an example of where Collision.INADVISABLE could come in such as if E/F/U/A were like lava or something
return Collision.JUMP_TO_VOXEL_LEVEL;
}
return Collision.VOXEL_LEVEL; // btw it's possible that stayLevel < feetBlips, since a small fall is sorta ignored and treated as a VOXEL_LEVEL
} // voxel_level (C within, D underneath) is eliminated, only remaining possibilities are blocked or fall
if (C.collidesWithPlayer) {
return Collision.BLOCKED;
}
if (!D.collidesWithPlayer) {
return Collision.FALL;
}
if (Main.DEBUG && D.collisionHeightBlips() >= Blip.FULL_BLOCK && D.fullyWalkableTop) {
throw new IllegalStateException();
}
if (D.collisionHeightBlips() < Blip.FULL_BLOCK + feetBlips) {
return Collision.FALL;
} else {
return Collision.BLOCKED;
}
}
public static boolean protrudesIntoThirdBlock(int feet) { // only false for feet between 0 and 3. true for 4 and up
return feet > Blip.TWO_BLOCKS - Blip.PLAYER_HEIGHT_SLIGHT_OVERESTIMATE; // > and not >= because the player height is a slight overestimate
}
public enum Collision {
// TODO maybe we need another option that is like "you could do it, but you shouldn't". like, "if you hit W, you would walk forward, but you wouldn't like the outcome" such as cactus or lava or something
BLOCKED, // if you hit W, you would not travel (example: walking into wall)
JUMP_TO_VOXEL_LEVEL, // blocked, BUT, if you jumped, you would end up at voxel level. this one is rare, it could only happen if you jump onto a block that is between 0.5 and 1.0 blocks high, such as 7-high snow layers
JUMP_TO_VOXEL_UP, // blocked, BUT, if you jumped, you would end up at one voxel higher. this is the common case for jumping.
JUMP_TO_VOXEL_TWO_UP, // blocked, BUT, if you jumped, you would end up two voxels higher. this can only happen for weird blocks like jumping out of soul sand and up one
VOXEL_UP, // if you hit W, you will end up at a position that's a bit higher, such that you'd determineRealPlayerSupport up by one (example: walking from a partial block to a full block or higher, e.g. half slab to full block, or soul sand to full block, or soul sand to full block+carpet on top)
VOXEL_LEVEL, // if you hit W, you will end up at a similar position, such that you'd determineRealPlayerSupport at the same integer grid location (example: walking forward on level ground)
FALL; // if you hit W, you will not immediately collide with anything, at all, to the front or to the bottom (example: walking off a cliff)
public int voxelVerticalOffset() {
switch (this) {
case VOXEL_LEVEL:
case JUMP_TO_VOXEL_LEVEL:
return 0;
case VOXEL_UP:
case JUMP_TO_VOXEL_UP:
return 1;
case JUMP_TO_VOXEL_TWO_UP:
return 2;
default:
throw new IllegalStateException();
}
}
public boolean requiresJump() {
switch (this) {
case VOXEL_LEVEL:
case VOXEL_UP:
return false;
case JUMP_TO_VOXEL_LEVEL:
case JUMP_TO_VOXEL_UP:
case JUMP_TO_VOXEL_TWO_UP:
return true;
default:
throw new IllegalStateException();
}
}
}
public static int playerFalls(long newPos, WorldState worldState, SolverEngineInput engineInput) {
// this means that there is nothing preventing us from walking forward and falling
// iterate downwards to see what we would hit
for (int descent = 0; ; descent++) {
// NOTE: you cannot do (descent*Face.DOWN.offset)&BetterBlockPos.POST_ADDITION_MASK because Y is serialized into the center of the long. but I suppose you could do it with X. hm maybe Y should be moved to the most significant bits purely to allow this :^)
long support = BetterBlockPos.offsetBy(newPos, 0, -descent, 0);
long under = Face.DOWN.offset(support);
if (Main.DEBUG && !engineInput.bounds.inRangePos(under)) {
throw new IllegalStateException(); // should be caught by PREVENTED_BY_UNDERNEATH
}
VoxelResidency res = canPlayerStand(engineInput.at(under, worldState), engineInput.at(support, worldState));
if (Main.DEBUG && descent == 0 && res != VoxelResidency.FLOATING) {
throw new IllegalStateException(); // CD shouldn't collide, it should be D and the one beneath... (playerTravelCollides would have returned BLOCKED or VOXEL_LEVEL)
}
switch (res) {
case FLOATING:
continue; // as expected
case PREVENTED_BY_UNDERNEATH:
case PREVENTED_BY_WITHIN:
return -1; // no safe landing
case UNDERNEATH_PROTRUDES_AT_OR_ABOVE_FULL_BLOCK:
case STANDARD_WITHIN_SUPPORT:
// found our landing spot
return descent;
default:
throw new IllegalStateException();
}
}
}
/**
* If the player feet is greater than the return from this function, the player can sneak out at that altitude without colliding with this column
* <p>
* If there is no collision possible this will return a negative number (which should fit in fine with the above ^ use case)
*/
public static int highestCollision(BlockStateCachedData underneath, BlockStateCachedData within) {
return Math.max(
underneath.collidesWithPlayer ? underneath.collisionHeightBlips() - Blip.FULL_BLOCK : -1,
within.collidesWithPlayer ? within.collisionHeightBlips() : -1
);
}
}

View File

@@ -0,0 +1,77 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.LongArrayList;
public class PlayerReachSphere {
public final long[] positions;
public PlayerReachSphere(double playerReachDistance) {
int ceiledPlusOne = 1 + (int) Math.ceil(playerReachDistance);
double realSq = playerReachDistance * playerReachDistance;
int ceiledSq = (int) Math.ceil(realSq);
LongArrayList sphere = new LongArrayList();
for (int x = -ceiledPlusOne; x <= ceiledPlusOne; x++) {
for (int y = -ceiledPlusOne; y <= ceiledPlusOne; y++) {
for (int z = -ceiledPlusOne; z <= ceiledPlusOne; z++) {
int d = closestPossibleDist(x, y, z);
if (d <= ceiledSq && d < realSq) { // int comparison short circuits before floating point
sphere.add(BetterBlockPos.toLong(x, y, z));
}
}
}
}
sphere.trim();
positions = sphere.elements();
}
public static int closestPossibleDist(int dx, int dy, int dz) { // player eye is within the origin voxel (0,0,0)
dx = lower(dx);
dy = lower(dy);
dz = lower(dz);
return dx * dx + dy * dy + dz * dz;
}
/**
* Bring closer to 0 by one if not already zero
*/
private static int lowerBranchy(int v) {
if (v > 0) {
return v - 1;
} else if (v < 0) {
return v + 1;
} else {
return v;
}
}
private static int lower(int v) {
return v + (v >>> 31) - ((-v) >>> 31);
}
static {
for (int i = -10; i <= 10; i++) {
if (lowerBranchy(i) != lower(i)) {
throw new IllegalStateException();
}
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public enum PlayerVantage {
LOOSE_CENTER, // allow plus or minus 0.15 in either direction
STRICT_CENTER, // player stands at (0.5, 0.5) on the block with no variance allowed
SNEAK_BACKPLACE
}

View File

@@ -0,0 +1,91 @@
This is where I am working on a rewrite from-scratch of the Baritone schematic builder.
I will write more documentation eventually, I have some planning documents that I may eventually paste into here and such, I just don't want to commit to writing about something when it's still up for change :)
The current Baritone BuilderProcess has a number of issues, no one disagrees there. The question is whether to try and fix those issues, or to start from scratch. I believe that starting from scratch is a good idea in this case. The reason is that I want to change the fundamental approach to building schematics. The current approach is basically "do whatever places / breaks I can from wherever I happen to be at the moment" and also "and if I can't work on anything from here, pathfind to a coordinate where I probably will be able to". This suffers from an inherently unfixable problem, which is that **there is no plan**. There are an untold number of infinite loops it can get stuck in that I have just hack patched over. Did you know that whenever the builder places a block, it increments a counter so that for the next five ticks (0.25 seconds) it won't consider breaking any blocks? This was my fix for it just placing and breaking the same block over and over. Did you know that the builder only places blocks at or below foot level purely because otherwise it would stupidly box itself in, because it isn't thinking ahead to "if I cover myself in blocks, I won't be able to walk to the next thing I need to do". And this doesn't even get into the larger scale infinite loops. I patched over the short ones, where it gets itself stuck within seconds, but the long term ones are a different beast. For example, the pathfinder that takes it to the next task can place / break blocks to get there. It has strict penalties for undoing work in the schematic, of course, but sometimes that's the only way forward. And in that scenario, it can cause infinite loops where each cycle lasts 30+ seconds ("nothing to do here, let's pathfind to the other side of the schematic" -> break a block in order to get there -> arrive at the other end -> "whoa i need to go place that block" -> walk back to where it started and place that block -> repeat the cycle).
So the idea, as you might have guessed from that foreshadowing, is that the new builder will generate a **complete** plan that includes interspersed actions of placing blocks and moving the player. This plan will be coherent and will not suffer from the issues of the current pathfinder in which costs are unable to take into account the previous actions that have changed the world. I'll say more on that later. This does mean that the schematic builder will not use the same path planner as the rest of Baritone.
Current restrictions that the builder will have, at least initially:
* No breaking blocks. If you want to `cleararea` you can just use the old one, it works fine for that specific thing, at least.
* For that reason, the schematic area should be clear. Think of this like a 3d printer going from bottom to top. It takes in a schematic, generates a plan of how to build it, and executes it.
* Because it generates a full plan, if you think about it, this means that your schematic needs to be plannable. Therefore, if your schematic has something unplaceable in it, it won't even begin to do anything. I'll probably add an option to just skip impossible blocks with a warning. But plan for certain things (e.g. lava) to not be possible.
* Ancillary only placed old-baritone-style to assist in movement
This is less dependent on Minecraft code. I'm too annoyed by things like `IBlockState` -> `BlockState` or `player.motionY()` -> `player.getMotion().y` or `== Blocks.AIR` -> `instanceof BlockAir`. For now, I've put everything that imports from Minecraft code into a subpackage called `mc`. I will probably stick with that. Basically I hate having to change all the stupid mapping names and deal with merge conflicts on those. For that reason I plan to use `int` instead of `IBlockState` everywhere, and basically do like `cachedData[state].isAir` instead of `state.getBlock() instanceof BlockAir` (so like `cachedData` would be a `BlockStateCachedData[]` with one entry per valid `IBlockState`). Then, for different versions of the game, all that would need to change is the code to populate array. This should help with checks like can-walk-on and can-place-against.
If all the blocks that existed in Minecraft were solid, can-walk-on, can-place-against, this would be trivially easy. (I know - even for schematics of blocks like that, the current builder has problems.) Even for blocks such as glazed terracotta that have orientation, it would be okay.
The problem arises with all these blocks that people love to use in builds that are really frustrating to work around. I'm referring to slabs, stairs, fences, walls, etc.
The current pathfinder makes a naive assumption that every movement's starting point is sane. In other words, it assumes that the previous movement will have gotten the player to the starting point stably. So even if the starting point is currently underground, the previous movement will have broken the two blocks that the player is now assumed to be standing in. If the starting point is floating in midair, the previous movement must have gotten a block placed that the player is currently standing on. This allows for incredibly fast long distance pathing. However, that isn't the goal here. In normal Baritone pathfinding, the goal is to get from point A to point B. Placing and breaking blocks along the way is only done as necessary to accomplish the first goal. Whereas in structure building, it's to place and break blocks, to realize a schematic. The player moving around is only done as necessary to accomplish the first goal. So, clearly, the builder pathfinder needs to be smarter than this. In order to generate these plans, it does need to be answer hypotheticals like "by this point in the schematic, if I stood here, could I reach this face of this block to complete this placement?". It also needs to be able to answer questions like "I'm standing at XYZ. But what am I actually standing on? At this point in the construction, is that a scaffolding throwaway block, or is it the half slab that we eventually intend to be placed here?". Therefore, a standard pathfinder, where a node is uniquely identified by its coordinate, will not be sufficient, as that doesn't convey any information about actions taken to the world up to that point. ~~If we want to use a traditional graph search pathfinder,~~ a node must actually be identified by the XYZ of the player and the set of blocks that have been modified so far.
~~I don't want to go too much further into how a traditional graph search algorithm could work here, as I'm not even certain that's what would work best. I'm considering STAN (an offshoot of graphplan) as well. It might be a hybrid solution, who knows. (e.g. try naive DFS, if that can't find a plan after X milliseconds, then fall back to graphplan).~~
I'm not sure how to think about performance for this. Obviously there are copes like "pregenerate a plan for a schematic, reuse it". There are also copes like "just throw it in another thread, who cares how long it takes". Because if I dump in a 128x128x128 schematic of all stone (two million blocks), it takes a few seconds to separate the strongly connected components. Is that okay? I think it is. Lol.
One particularly annoying thing is needing to reimplement portions of Minecraft. For example, there is a bit of code that I can easily call that says "If I looked in this direction right now, and right clicked, what block precisely would it place". Because, well, Minecraft needs to decide what block appears if and when I right click. This works great for current Baritone and I can just call it without needing to worry. For this builder though, I need to do hypotheticals. "If I were standing here on top of this block (that doesn't exist yet), looking at this face of this block (which doesn't exist yet), and right clicked while holding this item (which I don't have), what block would appear?". Minecraft code isn't built like that. There are so many conditions based on Y hit, face hit, player orientation, etc. Just look at the trapdoor place logic to instantly die. So I need to reimplement all the code for "I desire this block state (northward facing upside down acacia stairs). How can I make this coordinate contain that block? (well, you need to be facing north and right click the top face of an adjacent block with such a stair)". That code needs to be written for every block that has custom placement like that. The question of "If block X is at coordinate Y, could I right click against that to place block Z on face D" is incredibly frustratingly not easily answerable. It all depends on external variables like the horizontal orientation of the player, and even the Y coordinate of the player (for some blocks like pistons and dispensers).
I'm trying my best to minimize floating point calculations as much as possible. The fast case for the graph search should never use floating point. I'm too annoyed by even simple things like `> 1` being randomly wrong (with `0.99999` or `1.000001`). Also, no need to have path costs be floating point. Both path costs and player Y will be fixed point integers. Costs have a to-be-determined denominator, for player Y it's sixteen.
I'm going to list stupid things about Minecraft that have pissed me off during this:
* Every `IBlockState` in the game has an entry in `Block.BLOCK_STATE_IDS`. Except for some variants of tripwire. Nothing else. Just tripwire is the exception. And it isn't even for a good or elegant reason. It's just hardcoded in the thing that constructs the block state registry to skip some of the tripwire states. No idea why.
* `Block.BLOCK_STATE_IDS` has double entries. There are many states that map to the same integer ID, when you go back to states it sets certain properties to default. Such as `BlockStairs.SHAPE`.
Things about old Baritone builder that are stupid and that I will do better:
* The thing I mentioned above about left and right clicking
* Complete nonunderstanding of orientable blocks. It has no idea that in order to place a stair / a torch it has to go walk around and look at the block from the other side.
* Instead of just placing blocks normally, it sneaks before EVERY right click. This makes it look unnatural, stupid, and jittery, for no reason.
The area shall be combined with the schematic in the following way:
* The entire buildable area is considered to be "part of the schematic"
* The working space will be that area. The pathfinder's universe ends at the border of that area.
* The current status is therefore defineable as the schematic, the set of block positions that are "intended scaffolding", and the set of block positions that have been placed (air is interpreted as scaffolding whether intended or not)
* Needed because the scaffolder is not magic, it's possible that the solver needs to place additional scaffolding in order to reach something
* There are no further possible options beyond those (want air have air, want scaffolding have air, want air have scaffolding, want block have air, want block have block)
* Blocks that are desired to be not-air that are currently a different block are disallowed (for the time being) (because that would require breaking blocks)
* Not yet decided if the schematic itself is allowed to have throwaways. Probably should be allowed, it would be indicative of bad design if it weren't allowed.
# How will it work
I'm going to boldly write down the current plan. The above part of the readme ^ was all written in 2021 when I was just getting started and didn't really have a well-formed solution to how this mystical schematic building plan would be generated in the first place.
## The preprocessing
We load the schematic from disk and probably do some basic checks that it doesn't have any impossible blocks in it.
First, we create the [place order dependency graph](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/PlaceOrderDependencyGraph.java). This is a simple 3d voxel grid that stores six bits per blockpos in the schematic, representing what blocks have to be placed before this one. For a normal block, in theory all six could be on. (in practice, baritone 2 will probably never place top-to-bottom, so in practice it'll only ever use five of those six). For something like a torch, only one will be on, representing how the torch depends on the block it'll be placed against. Sand will have only the DOWN bit on, because if you tried to place the sand before that block that supports it, it would just fall. In short, this graph represents the dependencies of placing the blocks of the schematic. Each bit that's on means that this block *could* be placed against that face, therefore we assume that once any of those faces are placed, this block can be placed next. This calculation is pretty complicated and involves checking both the block and the one it would hypothetically be placed against. For example, bottom slabs can be placed against upside-down stairs... but only if the stair is facing away from the side you want to place the slab against (yes it does take that into account, see [here](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/mc/BlockStatePropertiesExtractor.java#L48-L69)).
A [few basic checks](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/DependencyGraphAnalyzer.java) are run against the place order dependency graph, namely that every block can be placed from at least one side, then we check that there is some hypothetical placeable route from the boundary of the schematic to everything in the interior. This catches cases such as shulker boxes placed against each other, where you need one already there to place the other, but that other one also needs the first one (baritone 2 won't support this since it makes the code much easier). Also you might be wondering, "what do you mean placeable route from the boundary? isn't the schematic air? air isn't placeable". The PlaceOrderDependencyGraph treats air as scaffolding, because, it could be. The alternative is just to have no data in the air parts, which is boring. In practice, this is useful for the next step because the scaffolder might need to add scaffolding to the build.
## Scaffolding
In Minecraft, blocks have to be placed against other blocks. You can't make one appear way up in the air, you can only make one appear next to an existing block. Therefore, we are going to add a reasonable and hopefully nearly minimal quanity of scaffolding blocks to the schematic so that the entire thing is placeable from a single starting point (which will be at the very bottom, due to how (as mentioned earlier) the dependency graph never points downward, only upward or sideways (this is due to the fact that builds are completed in survival mode and Steve can't fly so he has to walk on top of what he's building)).
To accomplish this, first the the place order dependency graph is analyzed using [Tarjan's algorithm](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/TarjansAlgorithm.java) to find the strongly connected components, which are collapsed into single nodes within a collapsed dependency graph (which is now a DAG since all the cycles are collapsed), maintained within a [dependency graph scaffolding overlay](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/DependencyGraphScaffoldingOverlay.java). This means the sections of the schematic in which every block can be placed starting from any of them. That's a bit hard to understand but don't worry about it because in practice, most builds will be mostly comprised of normal blocks that can be placed from any direction, so in practice these collapsed components will be just horizontal slices of the build. But, they won't contain air gaps. (example: if your schematic is a simple tower, each y level gets its own slice. if your schematic is two disconnected towers, there will be two slices at each y level, one for each tower). When I say "slice" or "collapsed component" or "strongly connected component" or just "component", that all means the same thing. Basically like Baritone's existing buildInLayers, except the layers are also split sideways at each air gap.
The point of doing this is that we can very simply and easily tell where scaffolding should go by looking at the collapsed dependency graph. The algorithm is now actually very simple: look at every component that has no incoming edges (called a "root component"). (there will always be at least one since this graph is acyclic) Those are the component(s) that we'd need to "start with". Because we can't (yet) place them starting anywhere else. So, we add scaffolding until there is only one such root component. The actual algorithm for this is not yet fully nailed down. But probably something simple like "pick the root component with the highest Y level then Dijkstra outwards and downwards until you find another component, then add scaffolding along that path" will work fine. Or it might be more complicated (simultaneous Dijkstra from every starting source). Note that this is guaranteed to succeed because of the preprocessing which checked that the build is placeable from the exterior.
As the scaffolder decides where to place scaffolding, it updates the dependency graph scaffolding overlay. But, rerunning Tarjan's algorithm each time would be very slow (it's `O(blocks)`). Instead, it detects the new edges added to the graph and updates accordingly. If there are any cycles created (aka strongly connected components), they are collapsed in real time as the scaffolding is placed. This is actually pretty fast because under the "no downward placement" assumption, cycles can only really be created on the same y level, and complicated collapsed graphs on the same y level are probably not going to come up much. You'd have to have some borderline malicious schematic full of directional blocks like hoppers or shulkers or logs or something for this to actually be an issue.
When the scaffolder is done and all the necessary scaffolding is added to the overlay, we now have a beautiful DAG representing how we're going to build the schematic. There is one root node at the bottom of the build with no incoming edges, and outward edges go upwards connecting the entire build. (note: even though there are no cycles, there can still be diamond shapes (A->B A->C B->D C->D), which ends up being [super annoying](https://github.com/cabaletta/baritone/blob/a9d6aaebf3f72c7af8e8fe550a1c8cdd36b96fef/src/main/java/baritone/builder/DependencyGraphScaffoldingOverlay.java#L242-L288)). Each node represents a component aka slice of the build, and every one of those components is a connected set of blocks all at the same y level. The idea is that once we've got any one of the blocks in a component placed, we can get the rest of them placed (this is how they were computed in the first place, from the place order dependency graph).
## Planning
The crux of the matter is that the Minecraft player can't fly or teleport in survival mode. If you were in creative mode, this would be no trouble at all, just do it like a 3d printer bottom to top. But alas, we have to walk on top of the build while we're building it.
Therefore, it's critical to keep track of the area where the player can walk around. We need to be able to frequently and efficiently answer the question "Is it okay to place this block? Or would that get me stuck and unable to walk onwards?"
The idea here is a data structure that enables [dynamic connectivity](https://en.wikipedia.org/wiki/Dynamic_connectivity), specifically the level structure on top of Euler Tour Forests. This is a data structure that accepts information such as "vertex v and u are now linked by an edge" and "that edge has now been cut" in any order, taking amortized `O((log n)^2)` time. It is able to answer queries of whether v and u are connected by any path also in amortized `O((log n)^2)` time. This is *mind boggling*. Even if you have some incredibly complicated grid with millions of nodes, it can tell you whether a path exists in polylog time. And, even more incredibly, if you remove an edge, the structure updates *also in polylog time* and future queries will reflect that reality (those components are now disconnected if that was the only link, or if there was another link that one is being used now). Here are some links: [wiki](https://en.wikipedia.org/wiki/Dynamic_connectivity#The_Level_structure), [wiki2](https://en.wikipedia.org/wiki/Euler_tour_technique#Euler_tour_trees), [the paper that invented this particular structure (with these deterministic runtimes), see sections 2, 2.1, 3, 3.1, 3.2](https://u.cs.biu.ac.il/~rodittl/p723-holm.pdf).
The builder is able to treat this mostly as a black box, an API that exposes `Link(u,v)` `Cut(u,v)` and `Is-Connected?(u,v)`. You don't really need to understand how the euler tour forest works. But the basic idea is to maintain a minimum spanning tree of every graph in the forest, maintain an euler tour on each such graph, and store each euler tour in a balanced binary search tree. e.g. if you had nodes A,B,C with connections AB and BC, the euler tour could look like `(a,b) (b,c) (c,b) (b,a)`. It's very important to note that the tour can go in any order. For example you can "cut the deck" and move the first element of the tour to the back, or vice versa, and it's still perfectly valid. The rule is just that the euler tour will go along each edge twice (once in each direction). The way to tell if two nodes have any path between them is by finding which euler tour each is in - if they're both in the same tour, there is a path between them. When linking two nodes U and V that previously had no path between them, the idea is that you grab the euler tour for both, "cut the deck" so that one begins and ends at U, and the other begins and ends at V, then connect them with the new edge. So, like `"previous tour rotated to start and end at U" + [(U,V)] + "the other previous tour rotated to start and end at V" + [(V,U)]`. (these tours were separate, now they're combined). When cutting an edge, it's a bit more complicated because you have to search for whether there's some other edge that can keep these two components connected by another route, but anyway if you don't find one, you just do the opposite (find the `(V,U)` and the `(U,V)` and snip the tour into half at those places, rotating so that one is at the beginning so you get two pieces not three). You could do this with the tour being stored in a simple `ArrayList<Edge>` for sure, but it would have very bad performance, we can do better. Instead, we use a balanced binary tree (specifically a [red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)). It's sortof interesting - we aren't actually storing any data in the tree, the nodes don't actually have any kind of "order" that is being maintained. All the tree is being used for is maintaining the structure (the pointers and the red/black colors). With this structure, operations such as "cut off all elements before this one, and move them to the back" can be done in `O(log n)` time (as well as the simple operations like insert and delete too) (whereas an ArrayList would take `O(n)` for split / concat). There is an additional `O(log n)` term, as we do some trickery with levels - we actually store `log n` copies of this entire data structure, with the maximum size cut in half each time. We need to do that so `Cut` isn't `O(n)`, because `Cut` needs to find an alternate edge if it exists whenever you try and disconnect two components. I'm not going to explain it but take a look at the previous links or [this lecture which covers it](https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf).
The one thing you DO need to know is that we can attach arbitrary data to these trees. As a simple example, each node could have attached to it a count of adjacent blocks that we still intend to place but haven't done so yet. Going up the red-black tree, we could code each node to sum the values of its child nodes. Now, we can look at where the player is standing, traverse the red black tree to the root and look at its count, and that'll let us check if there's still work to do in the area we're standing in. This becomes interesting when used as a hypothetical. For example, when we place a block that would cut off the area the player can walk into two pieces with no path between them anymore, we can notice "Hey, one of these has a remaining block count of 0. It's actually fine to cut off that area, because we're done there! We just need to make sure we're standing on the OTHER side when we place that block". This might not sound exciting but it really is because this is done in `O(log n)` time!!
This solves the "don't paint yourself into the corner" problem.
Baritone will be able to realize what block placements are "safe" (aka: don't affect the navigable surface (the area that it can walk around in)), and which ones require special conditions (aka: the ones that cut off the surface into two, three, or even four pieces). And the ones that require special conditions will be much easier to reason about (as described in the last paragraph).
We'll create a plan by having a list of slices that we're currently working on, starting with the root. When a slice is completed, there will be some logic to potentially unlock new slices (its children and maybe also grandchildren (under some conditions) in the collapsed dependency DAG). As blocks are placed, the navigable surface is updated in real time. Perhaps the rules can be relaxed and we can allow slight "working ahead" into slices that are up next, since we can tell when that would block off our future path or not! The navigable surface will be able to answer quickly "can I get next to this block so I can place it?". There will probably be complicated rules about when it is and isn't okay to cut off the walkable area. For example building any kind of pillar absolutely requires cutting it off. For example maybe it needs to look ahead, see what the build will look like once this offshoot tower is completed, and see if it can fall back down to where it was before? Maybe when it tries to place a block but decides no this one is for later it does exponential backoff and moves it further and further back in some priority queue? No idea :D

View File

@@ -0,0 +1,396 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Traces rays
*/
public class Raytracer {
public static Optional<Raytrace> runTrace(Vec3d playerEye, long againstPos, Face againstFace, Vec3d hit) {
// TODO this totally could be cached tho...
if (againstFace.offset(againstPos) != 0) {
throw new IllegalStateException("sanity check - for now we assume that all placed blocks end up at 0,0,0");
}
if (toLong(playerEye) == 0) {
throw new IllegalStateException("player eye is within the block we want to place, this is maybe possible? idk i suppose you could do this with, like, a torch? still seems weird. idk if this should be allowed");
}
if (toLong(playerEye) == againstPos) {
throw new IllegalStateException("player eye is within the block we want to place against, this is DEFINITELY impossible");
}
//System.out.println(BetterBlockPos.fromLong(toLong(playerEye)) + " to " + BetterBlockPos.fromLong(toLong(hit)) + " aka " + playerEye + " to " + hit);
if (Main.STRICT_Y && floor(playerEye.y) < 0) {
throw new IllegalStateException("im lazy and dont want to fix occupancyCountByY");
}
long hitPos = toLong(hit);
if (hitPos != againstPos && hitPos != 0) {
throw new IllegalStateException("ambiguous or incorrect hitvec?");
}
LongArrayList trace = rayTrace(playerEye.x, playerEye.y, playerEye.z, hit.x, hit.y, hit.z, againstPos);
if (trace.size() < 2) {
throw new IllegalStateException();
}
if (trace.getLong(trace.size() - 1) == 0 && trace.getLong(trace.size() - 2) == againstPos) {
// placing through the block
// for example, we might be standing to the south of the againstPos but we want to place against the north side of it
// while we can raytrace to that face, the problem is that our raytrace is hitting some other side of againstPos (maybe south or up) first and then hitting the face we want second
return Optional.empty();
}
return Optional.of(new Raytrace(playerEye, againstPos, againstFace, hit, trace));
}
private static long toLong(Vec3d hit) {
return BetterBlockPos.toLong(floor(hit.x), floor(hit.y), floor(hit.z));
}
public static class Raytrace implements Comparable<Raytrace> {
public final long againstPos;
public final Face againstFace;
public final long[] passedThrough;
public final Vec3d playerEye;
public final Vec3d hit;
public final int[] occupancyCounts;
private Raytrace(Vec3d playerEye, long againstPos, Face againstFace, Vec3d hit, LongArrayList trace) {
this.againstFace = againstFace;
this.againstPos = againstPos;
this.playerEye = playerEye;
this.hit = hit;
if (trace.getLong(trace.size() - 1) != againstPos) {
print(trace);
throw new IllegalStateException();
}
if (trace.getLong(trace.size() - 2) != 0) {
throw new IllegalStateException();
}
for (int i = 0; i < trace.size() - 1; i++) {
if (!adjacent(trace.getLong(i), trace.getLong(i + 1))) {
throw new IllegalStateException(BetterBlockPos.fromLong(trace.getLong(i)) + " to " + BetterBlockPos.fromLong(trace.getLong(i + 1)));
}
}
trace.removeLong(trace.size() - 1); // againstPos doesn't ACTUALLY need to be air, so remove it. it was only there for sanity checking and confirming which face we collided with first
trace.trim();
if (trace.getLong(0) != toLong(playerEye)) {
throw new IllegalStateException();
}
this.passedThrough = trace.elements();
this.occupancyCounts = computeOccupancyCount();
}
private int[] computeOccupancyCount() {
long freebieTop = passedThrough[0]; // player eye
long freebieBottom = Face.DOWN.offset(freebieTop); // player feet
if (Main.STRICT_Y) {
IntArrayList list = new IntArrayList();
for (int i = passedThrough.length - 1; i >= 0; i--) {
long pos = passedThrough[i];
int y = BetterBlockPos.YfromLong(pos);
if (list.size() == y) { // works because we removed the last trace element (against), which could have negative y
list.add(0);
}
if (list.size() != y + 1) { // this works because we go in reverse order
throw new IllegalStateException("nonconsecutive");
}
if (pos == freebieTop) {
// only here for correctness in spirit, technically not needed for comparison since it will exist in all of them
continue;
}
if (pos == freebieBottom) {
if (i != 1) {
throw new IllegalStateException();
}
continue;
}
list.elements()[y]++; // troll face
}
list.trim();
return list.elements();
} else {
int cnt = 0;
for (long pos : passedThrough) {
if (pos != freebieBottom && pos != freebieTop) {
cnt++;
}
}
return new int[]{cnt};
}
}
@Override
public int compareTo(Raytrace o) { // lower is better
{ // first, sort by occupancy counts. shortest ray wins. or rather, ray that is shortest on the bottom.
if (occupancyCounts.length != o.occupancyCounts.length) {
// any two traces with the same src and dst MAY very well have different passedThrough.length
// HOWEVER they are guaranteed to have the same occupancyCounts.length, since they start at the same floor(y) and end at the same floor(y)
throw new IllegalStateException("comparing raytraces with unequal src/dst");
}
for (int i = 0; i < occupancyCounts.length; i++) {
int cmp = Integer.compare(occupancyCounts[i], o.occupancyCounts[i]);
if (cmp != 0) {
return cmp;
}
}
}
{ // if occupancy counts match, tiebreak with strict center winning over loose center
int cmp = Double.compare(centerDistApprox(), o.centerDistApprox());
if (cmp != 0) {
return cmp;
}
}
{ // if center status matches, finally tiebreak with simple ray length
return Double.compare(distSq(), o.distSq());
}
}
public double centerDistApprox() {
// calculate distance to center of block but intentionally round it off
// the intent is for LOOSE_CENTER to always tie with itself, even though floating point inaccuracy would make it unequal if we did a direct Double.compare
double dx = playerEye.x - (floor(playerEye.x) + 0.5d);
double dz = playerEye.z - (floor(playerEye.z) + 0.5d);
double dist = dx * dx + dz * dz;
dist = Math.round(dist * 1000000);
return dist;
}
public double distSq() {
return hit.distSq(playerEye);
}
}
/**
* If I add 10 to all the numbers I raytrace, then subtract them afterwards, then I can just use (int) instead of the nasty BRANCHING floor.
* <p>
* No difference raytracing from -2 to -1 as it is to raytrace from 8 to 9. Just add ten!
*/
private static final int POSITIVITY_OFFSET = 10;
private static final int NUM_STEPS = 10_000;
public static int raytraceMode = 2;
private static LongArrayList rayTrace(double rawStartX, double rawStartY, double rawStartZ, double endX, double endY, double endZ, long againstPos) {
LongArrayList slow = raytraceMode == 0 || Main.SLOW_DEBUG ? rayTraceSlow(rawStartX, rawStartY, rawStartZ, endX, endY, endZ) : null;
LongArrayList fast = raytraceMode == 1 || Main.SLOW_DEBUG ? rayTraceFast(rawStartX, rawStartY, rawStartZ, endX, endY, endZ) : null;
LongArrayList faster = raytraceMode == 2 || Main.SLOW_DEBUG ? rayTraceZoomy(rawStartX, rawStartY, rawStartZ, endX, endY, endZ, againstPos) : null;
if (Main.SLOW_DEBUG) {
if (fast.equals(slow) && fast.equals(faster)) {
} else {
System.out.println(rawStartX + " " + rawStartY + " " + rawStartZ + " " + endX + " " + endY + " " + endZ + " " + againstPos);
print(slow);
print(fast);
print(faster);
throw new IllegalStateException();
}
}
return fast == null ? slow == null ? faster : slow : fast;
}
public static void print(LongArrayList trace) {
System.out.println(trace.stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()));
}
public static LongArrayList rayTraceFast(double rawStartX, double rawStartY, double rawStartZ, double endX, double endY, double endZ) {
if (willFlipSign(rawStartX) || willFlipSign(rawStartY) || willFlipSign(rawStartZ) || willFlipSign(endX) || willFlipSign(endY) || willFlipSign(endZ)) {
throw new IllegalStateException("I suppose this could happen if you set the block reach distance absurdly high? Don't do that.");
}
double diffX = endX - rawStartX;
double diffY = endY - rawStartY;
double diffZ = endZ - rawStartZ;
if (Math.abs(diffX) < 0.1 && Math.abs(diffY) < 0.1 && Math.abs(diffZ) < 0.1) {
// need more checks than before because now the tightest inner do-while does NOT check step against any upper limit at all
// therefore, if diff was zero, it would truly get stuck indefinitely, unlike previously where it would bail out at 10010
throw new IllegalArgumentException("throwing exception instead of entering infinite inner loop");
}
double startX = rawStartX + POSITIVITY_OFFSET;
double startY = rawStartY + POSITIVITY_OFFSET;
double startZ = rawStartZ + POSITIVITY_OFFSET;
int x = Integer.MIN_VALUE;
int y = Integer.MIN_VALUE;
int z = Integer.MIN_VALUE;
LongArrayList voxelsIntersected = new LongArrayList();
int step = 0;
double mult = 1.0d / NUM_STEPS;
double frac;
while (step < NUM_STEPS) {
do frac = ++step * mult;
while (((x ^ (x = (int) (startX + diffX * frac))) | (y ^ (y = (int) (startY + diffY * frac))) | (z ^ (z = (int) (startZ + diffZ * frac)))) == 0);
voxelsIntersected.add(BetterBlockPos.toLong(x - POSITIVITY_OFFSET, y - POSITIVITY_OFFSET, z - POSITIVITY_OFFSET));
}
if (step > NUM_STEPS + 1) {
throw new IllegalStateException("No floating point inaccuracies allowed. Or, at least, no more than 2 parts in 10,000 of wiggle room lol. " + step);
}
return voxelsIntersected;
}
/**
* Here's an alternate implementation of the above that is functionally the same, just simpler (and slower)
* <p>
* The inner loop branches seven times instead of one, for example.
*/
private static LongArrayList rayTraceSlow(double startX, double startY, double startZ, double endX, double endY, double endZ) {
// i'd love to use strictfp here and on floor, but it could unironically prevent a much needed JIT to native, so I won't, sadly
double diffX = endX - startX;
double diffY = endY - startY;
double diffZ = endZ - startZ;
int prevX = Integer.MIN_VALUE;
int prevY = Integer.MIN_VALUE;
int prevZ = Integer.MIN_VALUE;
LongArrayList ret = new LongArrayList();
int ourLimit = NUM_STEPS + 1;
for (int step = 0; step <= ourLimit; step++) { // 1 branch (step <= ourLimit)
double frac = step / (double) NUM_STEPS; // go THROUGH the face by a little bit, poke through into the block
int x = floor(startX + diffX * frac); // 1 branch in floor
int y = floor(startY + diffY * frac); // 1 branch in floor
int z = floor(startZ + diffZ * frac); // 1 branch in floor
if (x == prevX && y == prevY && z == prevZ) { // 3 branches (due to && short circuiting)
continue;
}
prevX = x;
prevY = y;
prevZ = z;
ret.add(BetterBlockPos.toLong(x, y, z));
}
return ret;
}
private static boolean willFlipSign(double pos) {
return flippedSign(pos + POSITIVITY_OFFSET);
}
private static boolean flippedSign(double pos) {
return pos < 1d;
}
private static boolean adjacent(long a, long b) {
if (a == b) {
throw new IllegalStateException();
}
for (Face face : Face.VALUES) {
if (face.offset(a) == b) {
return true;
}
}
return false;
}
private static int floor(double value) {
int rawCast = (int) value;
if (value < (double) rawCast) {
// casting rounded up (probably because value < 0)
return rawCast - 1;
} else {
// casting rounded down
return rawCast;
}
}
public static LongArrayList rayTraceZoomy(double startX, double startY, double startZ, double endX, double endY, double endZ, long againstPos) {
if (endX < 0 || endX > 1 || endY < 0 || endY > 1 || endZ < 0 || endZ > 1) {
throw new IllegalStateException("won't work");
}
if (flippedSign(startX += POSITIVITY_OFFSET) | flippedSign(startY += POSITIVITY_OFFSET) | flippedSign(startZ += POSITIVITY_OFFSET) | flippedSign(endX += POSITIVITY_OFFSET) | flippedSign(endY += POSITIVITY_OFFSET) | flippedSign(endZ += POSITIVITY_OFFSET)) {
throw new IllegalStateException("I suppose this could happen if you set the block reach distance absurdly high? Don't do that.");
}
int voxelEndX = BetterBlockPos.XfromLong(againstPos) + POSITIVITY_OFFSET;
int voxelEndY = BetterBlockPos.YfromLong(againstPos) + POSITIVITY_OFFSET;
int voxelEndZ = BetterBlockPos.ZfromLong(againstPos) + POSITIVITY_OFFSET;
int voxelInX = (int) startX;
int voxelInY = (int) startY;
int voxelInZ = (int) startZ;
if (startX == (double) voxelInX || startY == (double) voxelInY || startZ == (double) voxelInZ) {
throw new IllegalStateException("Integral starting coordinates not supported ever since I removed the -0.0d check");
}
LongArrayList voxelsIntersected = new LongArrayList();
int steps = 64; // default is 200
while (steps-- >= 0) {
long posAsLong = BetterBlockPos.toLong(voxelInX - POSITIVITY_OFFSET, voxelInY - POSITIVITY_OFFSET, voxelInZ - POSITIVITY_OFFSET);
voxelsIntersected.add(posAsLong);
if (posAsLong == againstPos) {
if (voxelsIntersected.size() == 1 || voxelsIntersected.getLong(voxelsIntersected.size() - 2) != 0) {
voxelsIntersected.add(0);
}
return voxelsIntersected;
}
double nextIntegerX, nextIntegerY, nextIntegerZ;
// potentially more based branchless impl?
nextIntegerX = voxelInX + ((voxelInX - voxelEndX) >>> 31); // if voxelEnd > voxelIn, then voxelIn-voxelEnd will be negative, meaning the sign bit is 1
nextIntegerY = voxelInY + ((voxelInY - voxelEndY) >>> 31); // if we do an unsigned right shift by 31, that sign bit becomes the LSB
nextIntegerZ = voxelInZ + ((voxelInZ - voxelEndZ) >>> 31); // therefore, this increments nextInteger iff EndX>inX, otherwise it leaves it alone
// remember: don't have to worry about the case when voxelEnd == voxelIn, because nextInteger value wont be used
double fracIfSkipX = 2; // just has to be strictly greater than 1, might as well just go up to the next int
double fracIfSkipY = 2;
double fracIfSkipZ = 2;
double distanceFromStartToEndX = endX - startX;
double distanceFromStartToEndY = endY - startY;
double distanceFromStartToEndZ = endZ - startZ;
if (voxelEndX != voxelInX) { // reminder to future self: don't "branchlessify" this, it's MUCH slower (pretty obviously, floating point div is much worse than a branch mispredict, but integer increment (like the other two removed branches) are cheap enough to be worth doing either way)
fracIfSkipX = (nextIntegerX - startX) / distanceFromStartToEndX;
}
if (voxelEndY != voxelInY) {
fracIfSkipY = (nextIntegerY - startY) / distanceFromStartToEndY;
}
if (voxelEndZ != voxelInZ) {
fracIfSkipZ = (nextIntegerZ - startZ) / distanceFromStartToEndZ;
}
if (fracIfSkipX < fracIfSkipY && fracIfSkipX < fracIfSkipZ) {
// note: voxelEndX == voxelInX is impossible because allowSkip would be set to false in that case, meaning that the elapsed distance would stay at default
startX = nextIntegerX;
startY += distanceFromStartToEndY * fracIfSkipX;
startZ += distanceFromStartToEndZ * fracIfSkipX;
voxelInX = ((int) startX) - ((voxelEndX - voxelInX) >>> 31); // tested: faster to paste this 3 times with only one of the subtractions in each
voxelInY = ((int) startY);
voxelInZ = ((int) startZ);
} else if (fracIfSkipY < fracIfSkipZ) {
startX += distanceFromStartToEndX * fracIfSkipY;
startY = nextIntegerY;
startZ += distanceFromStartToEndZ * fracIfSkipY;
voxelInX = ((int) startX);
voxelInY = ((int) startY) - ((voxelEndY - voxelInY) >>> 31);
voxelInZ = ((int) startZ);
} else {
startX += distanceFromStartToEndX * fracIfSkipZ;
startY += distanceFromStartToEndY * fracIfSkipZ;
startZ = nextIntegerZ;
voxelInX = ((int) startX);
voxelInY = ((int) startY);
voxelInZ = ((int) startZ) - ((voxelEndZ - voxelInZ) >>> 31);
}
}
print(voxelsIntersected);
throw new IllegalStateException();
}
public static LongArrayList rayTraceZoomyAlternate(double startX, double startY, double startZ, double endX, double endY, double endZ, long againstPos) {
return rayTraceZoomy(startX, startY, startZ, endX, endY, endZ, againstPos);
}
}

View File

@@ -0,0 +1,59 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
public class ReachabilityCache implements IReachabilityProvider {
private final Long2ObjectOpenHashMap<LongArrayList> playerPositionToBlock;
public ReachabilityCache(DependencyGraphScaffoldingOverlay overlay, PlayerReachSphere sphere) throws SchematicIsTooDenseForThisToMakeSenseException {
playerPositionToBlock = new Long2ObjectOpenHashMap<>();
int maxReasonableCacheSize = overlay.bounds().volume();
int[] cnt = {0};
overlay.forEachReal(blockPos -> { // by only iterating through real blocks, this will be a much faster and better option for sparse schematics (e.g. staircased map art)
for (long offset : sphere.positions) {
long playerEyeVoxel = (blockPos + offset) & BetterBlockPos.POST_ADDITION_MASK;
if (overlay.bounds().inRangePos(playerEyeVoxel)) {
LongArrayList blocks = playerPositionToBlock.get(playerEyeVoxel);
if (blocks == null) {
blocks = new LongArrayList();
playerPositionToBlock.put(playerEyeVoxel, blocks);
}
blocks.add(blockPos);
if (cnt[0]++ > maxReasonableCacheSize) {
throw new SchematicIsTooDenseForThisToMakeSenseException(); // although, of course, it's perfectly possible for this to NOT be a good idea too
}
}
}
});
}
@Override
public LongList candidates(long playerEyeVoxel) {
return playerPositionToBlock.get(playerEyeVoxel);
}
public static class SchematicIsTooDenseForThisToMakeSenseException extends RuntimeException {
}
}

View File

@@ -0,0 +1,45 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
public class ReachabilityLive implements IReachabilityProvider {
private final DependencyGraphScaffoldingOverlay overlay;
private final PlayerReachSphere sphere;
public ReachabilityLive(DependencyGraphScaffoldingOverlay overlay, PlayerReachSphere sphere) {
this.overlay = overlay;
this.sphere = sphere;
}
@Override
public LongList candidates(long playerEyeVoxel) {
LongArrayList ret = new LongArrayList();
for (long offset : sphere.positions) {
long block = (playerEyeVoxel + offset) & BetterBlockPos.POST_ADDITION_MASK;
if (overlay.bounds().inRangePos(block)) {
ret.add(block);
}
}
return ret;
}
}

View File

@@ -0,0 +1,211 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import baritone.builder.DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph;
import baritone.builder.DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph.CollapsedDependencyGraphComponent;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.longs.LongSets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
/**
* Given a DependencyGraphScaffoldingOverlay, put in scaffolding blocks until the entire graph is navigable from the root.
* <p>
* In other words, add scaffolding blocks to the schematic until the entire thing can theoretically be built from one
* starting point, just by placing blocks against blocks. So like, anything floating in the air will get a connector down to the
* ground (or over to something that's eventually connected to the ground). After this is done, nothing will be left floating in
* midair with no connection to the rest of the build.
*/
public class Scaffolder {
private final IScaffolderStrategy strategy;
private final DependencyGraphScaffoldingOverlay overlayGraph;
// NOTE: these next three fields are updated in-place as the overlayGraph is updated :)
private final CollapsedDependencyGraph collapsedGraph;
private final Int2ObjectMap<CollapsedDependencyGraphComponent> components;
private final Long2ObjectMap<CollapsedDependencyGraphComponent> componentLocations;
private final List<CollapsedDependencyGraphComponent> rootComponents;
private Scaffolder(PlaceOrderDependencyGraph graph, IScaffolderStrategy strategy) {
this.strategy = strategy;
this.overlayGraph = new DependencyGraphScaffoldingOverlay(graph);
this.collapsedGraph = overlayGraph.getCollapsedGraph();
this.components = collapsedGraph.getComponents();
this.componentLocations = collapsedGraph.getComponentLocations();
this.rootComponents = calcRoots();
}
public static Output run(PlaceOrderDependencyGraph graph, IScaffolderStrategy strategy) {
Scaffolder scaffolder = new Scaffolder(graph, strategy);
while (scaffolder.rootComponents.size() > 1) {
scaffolder.loop();
}
return scaffolder.new Output();
}
private List<CollapsedDependencyGraphComponent> calcRoots() {
// since the components form a DAG (because all strongly connected components, and therefore all cycles, have been collapsed)
// we can locate all root components by simply finding the ones with no incoming edges
return components.values().stream().filter(component -> component.getIncoming().isEmpty()).collect(Collectors.toCollection(ArrayList::new)); // ensure arraylist since we will be mutating the list
}
private void loop() {
if (rootComponents.size() <= 1) {
throw new IllegalStateException();
}
for (CollapsedDependencyGraphComponent root : rootComponents) {
// don't remove from rootComponents yet since we aren't sure which way it'll merge (in theory, in practice it'll stop being a root when STRICT_Y is true, since it'll become a descendant, but in theory with STRICT_Y false it could merge on equal footing with another component)
if (!root.getIncoming().isEmpty()) {
throw new IllegalStateException();
}
LongList path = strategy.scaffoldTo(root, overlayGraph);
if (path == null) {
continue;
}
if (!root.getPositions().contains(path.get(path.size() - 1))) {
throw new IllegalStateException();
}
if (root.getPositions().contains(path.get(0))) {
throw new IllegalStateException();
}
internalEnable(path);
return;
}
throw new IllegalStateException("unconnectable");
}
private void internalEnable(LongList path) {
int cid = collapsedGraph.lastComponentID().getAsInt();
applyScaffoldingConnection(overlayGraph, path);
int newCID = collapsedGraph.lastComponentID().getAsInt();
for (int i = cid + 1; i <= newCID; i++) {
if (components.get(i) != null && components.get(i).getIncoming().isEmpty()) {
rootComponents.add(components.get(i));
System.out.println("Adding");
}
}
// why is this valid?
// for this to be valid, we need to be confident that no component from cid 0 to old lastcid could have had incomings become empty
// consider the case root -> descendant
// what if scaffolding created descendant -> root, then they were merged together, but descendant won?
// then, descendant would have cid less than last cid, and it wouldn't be added to rootComponents by the previous line perhaps?
// but, dijkstra strategy skips merging roots with their descendants intentionally since it's useless to do so
rootComponents.removeIf(root -> {
if (root.deleted()) {
if (!rootComponents.contains(root.deletedIntoRecursive())) {
throw new IllegalStateException(); // sanity check the above - if this throws, i suspect it would mean that a root component was merged into one of its descendants by useless scaffolding
// if this ends up being unavoidable, then iterating over all deletedIntoRecursive of rootComponents should find all new rootComponents
// this is because all new scaffoldings have their own component, so the only way for an old component to have no incomings is if it was merged "the wrong way" with the root, which is easily locatable by deletedIntoRecursive
}
return true;
}
if (!root.getIncoming().isEmpty()) { // handle the actual root itself that we just connected (hopefully)
return true;
}
return false;
});
/*rootComponents.clear();
rootComponents.addAll(calcRoots());*/
if (Main.DEBUG) {
if (!new HashSet<>(rootComponents).equals(new HashSet<>(calcRoots()))) { // equal ignoring order
// TODO rootComponents should be a Set instead of a List anyway
System.out.println(rootComponents);
System.out.println(calcRoots());
throw new IllegalStateException();
}
}
}
public static void applyScaffoldingConnection(DependencyGraphScaffoldingOverlay overlayGraph, LongList path) {
CollapsedDependencyGraph collapsedGraph = overlayGraph.getCollapsedGraph();
Long2ObjectMap<CollapsedDependencyGraphComponent> componentLocations = collapsedGraph.getComponentLocations();
if (!componentLocations.containsKey(path.getLong(0))) {
throw new IllegalStateException();
}
if (!componentLocations.containsKey(path.getLong(path.size() - 1))) {
throw new IllegalStateException();
}
if (componentLocations.get(path.getLong(0)) == componentLocations.get(path.getLong(path.size() - 1))) {
throw new IllegalStateException();
}
if (!componentLocations.get(path.getLong(path.size() - 1)).getIncoming().isEmpty()) {
throw new IllegalStateException();
}
// componentLocations.get(path.getLong(path.size() - 1)).getIncoming() can be either empty or nonempty
for (int i = 1; i < path.size(); i++) {
if (!overlayGraph.hypotheticalScaffoldingIncomingEdge(path.getLong(i), Face.between(path.getLong(i), path.getLong(i - 1)))) {
throw new IllegalStateException();
}
}
LongList positions = path.subList(1, path.size() - 1);
positions.forEach(pos -> {
if (componentLocations.containsKey(pos)) {
throw new IllegalStateException();
}
});
System.out.println("Enabling " + positions.stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()));
positions.forEach(overlayGraph::enable); // TODO more performant to enable in reverse order maybe?
}
public class Output {
public void enableAncillaryScaffoldingAndRecomputeRoot(LongList positions) {
throw new UnsupportedOperationException("mutable components after scaffolding is not worth it");
}
public CollapsedDependencyGraphComponent getRoot() { // TODO this should probably return a new class that is not mutable in-place
if (rootComponents.size() != 1) {
throw new IllegalStateException(); // this is okay because this can only possibly be called after Scaffolder.run is completed
}
CollapsedDependencyGraphComponent root = rootComponents.get(0);
if (!root.getIncoming().isEmpty() || root.deleted()) {
throw new IllegalStateException();
}
return root;
}
public boolean real(long pos) {
return overlayGraph.real(pos);
}
public void forEachReal(Bounds.BoundsLongConsumer consumer) {
overlayGraph.forEachReal(consumer);
}
public LongSets.UnmodifiableSet scaffolding() {
return overlayGraph.scaffolding();
}
public boolean air(long pos) {
return overlayGraph.air(pos);
}
DependencyGraphScaffoldingOverlay secretInternalForTesting() {
return overlayGraph;
}
}
}

View File

@@ -0,0 +1,184 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import baritone.builder.DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph.CollapsedDependencyGraphComponent;
import it.unimi.dsi.fastutil.HashCommon;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
public class SimultaneousDijkstraScaffolder implements IScaffolderStrategy {
@Override
public LongList scaffoldTo(CollapsedDependencyGraphComponent $ignored$, DependencyGraphScaffoldingOverlay overlayGraph) {
// the use-case that i'm keeping mind from a performance pov is staircased mapart
// O(n^2) across all 128x128=16384 blocks would be cringe
// so let's have a combined priority queue from all cdg provenances without a reset on every scaffolding placement
List<CollapsedDependencyGraphComponent> roots = overlayGraph.getCollapsedGraph().getComponents().values().stream().filter(component -> component.getIncoming().isEmpty()).collect(Collectors.toList());
ObjectOpenHashSet<CollapsedDependencyGraphComponent> remainingRoots = new ObjectOpenHashSet<>();
remainingRoots.addAll(roots);
Object2IntOpenHashMap<CollapsedDependencyGraphComponent> componentToId = new Object2IntOpenHashMap<>();
for (int i = 0; i < roots.size(); i++) {
componentToId.put(roots.get(i), i);
}
PriorityQueue<ScaffoldingSearchNode> openSet = new PriorityQueue<>(Comparator.<ScaffoldingSearchNode>comparingInt(node -> node.costSoFar).thenComparingInt(node -> node.key.cdgid));
Object2ObjectOpenHashMap<ScaffoldingSearchKey, ScaffoldingSearchNode> nodeMap = new Object2ObjectOpenHashMap<>();
for (CollapsedDependencyGraphComponent component : roots) {
int cdgid = componentToId.getInt(component);
LongIterator it = component.getPositions().iterator();
while (it.hasNext()) {
long l = it.nextLong();
ScaffoldingSearchKey key = new ScaffoldingSearchKey(l, cdgid);
nodeMap.put(key, new ScaffoldingSearchNode(key));
}
}
openSet.addAll(nodeMap.values());
while (!openSet.isEmpty()) {
ScaffoldingSearchNode node = openSet.poll();
CollapsedDependencyGraphComponent provenance = roots.get(node.key.cdgid).deletedIntoRecursive();
// is the deletedIntoRecursive valid? i think so because the costSoFar is a shared key in the priority queue. sure you could get a suboptimal path from an old subsection of a new cdg, but, it would only be considered after the optimal path. so, no issue? i think?
if (!provenance.getIncoming().isEmpty()) {
continue; // node originated from a cdg that has been scaffolded making it no longer a root
}
CollapsedDependencyGraphComponent tentativeComponent = overlayGraph.getCollapsedGraph().getComponentLocations().get(node.key.pos);
if (tentativeComponent != provenance) {
// TODO eventually figure out the situation with exclusiveDescendents like in the sequential dijkstra scaffolder
LongList toActivate = reconstructPathTo(node);
// have to do this bs because the scaffolding route can touch a third component even if only one scaffolding block is added
long[] allNearby = toActivate.stream().flatMapToLong(pos -> LongStream.of(OFFS_INCL_ZERO).map(off -> (off + pos) & BetterBlockPos.POST_ADDITION_MASK)).toArray();
// have to check before applying scaffolding because getComponentLocations will return the new component and not the deleted root if we did it after
Set<CollapsedDependencyGraphComponent> toCheck = LongStream.of(allNearby).mapToObj(overlayGraph.getCollapsedGraph().getComponentLocations()::get).collect(Collectors.toCollection(ObjectOpenHashSet::new));
Scaffolder.applyScaffoldingConnection(overlayGraph, toActivate);
// have to check this again because new scaffolding can make its own collapsed node
// for example if there are two individual blocks separated by a knight's move in strict_y mode, meaning there are two new scaffolding blocks added at a certain y, connecting to one block at that y and another at a higher y, then the two new scaffolding can form a larger collapsed node, causing the previous block to be merged into it
// in short, it's possible for a new root to be created
toCheck.addAll(LongStream.of(allNearby).mapToObj(overlayGraph.getCollapsedGraph().getComponentLocations()::get).collect(Collectors.toSet()));
int sz = remainingRoots.size();
for (CollapsedDependencyGraphComponent component : toCheck) {
if (component.deleted()) {
remainingRoots.remove(component);
}
}
if (remainingRoots.size() >= sz) {
throw new IllegalStateException();
}
for (long pos : toActivate) { // im being lazy, this boxes to Long i think
CollapsedDependencyGraphComponent comp = overlayGraph.getCollapsedGraph().getComponentLocations().get(pos);
if (comp.getIncoming().isEmpty()) {
if (remainingRoots.add(comp)) {
int cdgid = roots.size();
roots.add(comp);
componentToId.put(comp, cdgid);
}
ScaffoldingSearchNode newNode = new ScaffoldingSearchNode(new ScaffoldingSearchKey(pos, componentToId.getInt(comp)));
nodeMap.put(newNode.key, newNode);
openSet.add(newNode);
}
}
if (remainingRoots.size() == 1) {
return null;
}
if (remainingRoots.isEmpty()) {
throw new IllegalStateException();
}
}
for (Face face : Face.VALUES) {
int newCost = node.costSoFar + DijkstraScaffolder.edgeCost(face);
if (overlayGraph.hypotheticalScaffoldingIncomingEdge(node.key.pos, face)) {
long neighborPos = face.offset(node.key.pos);
ScaffoldingSearchKey neighborKey = new ScaffoldingSearchKey(neighborPos, node.key.cdgid);
ScaffoldingSearchNode existingNode = nodeMap.get(neighborKey);
if (existingNode != null) {
if (existingNode.costSoFar > newCost) {
throw new IllegalStateException();
}
continue;
}
ScaffoldingSearchNode newNode = new ScaffoldingSearchNode(neighborKey);
newNode.costSoFar = newCost;
newNode.prev = node;
nodeMap.put(newNode.key, newNode);
openSet.add(newNode);
}
}
}
throw new UnsupportedOperationException();
}
private static class ScaffoldingSearchKey {
long pos;
int cdgid;
public ScaffoldingSearchKey(long pos, int cdgid) {
this.pos = pos;
this.cdgid = cdgid;
}
@Override
public boolean equals(Object o) {
//if (this == o) return true;
//if (!(o instanceof ScaffoldingSearchKey)) return false;
ScaffoldingSearchKey that = (ScaffoldingSearchKey) o;
return pos == that.pos && cdgid == that.cdgid;
}
@Override
public int hashCode() {
return HashCommon.murmurHash3(cdgid) + (int) HashCommon.murmurHash3(pos);
}
}
private static class ScaffoldingSearchNode {
private final ScaffoldingSearchKey key;
private int costSoFar;
private ScaffoldingSearchNode prev;
private ScaffoldingSearchNode(ScaffoldingSearchKey key) {
this.key = key;
}
}
private static LongList reconstructPathTo(ScaffoldingSearchNode end) {
LongList path = new LongArrayList();
while (end != null) {
path.add(end.key.pos);
end = end.prev;
}
return path;
}
private static final long[] OFFS_INCL_ZERO = new long[Face.NUM_FACES + 1];
static {
for (int i = 0; i < Face.NUM_FACES; i++) {
OFFS_INCL_ZERO[i] = Face.VALUES[i].offset;
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
public class SneakPosition {
public static long encode(long pos, Face sneakingTowards) {
if (Main.DEBUG && sneakingTowards != null && sneakingTowards.vertical) {
throw new IllegalStateException();
}
return sneakingTowards == null ? pos : encode(pos, sneakingTowards.horizontalIndex);
}
public static long encode(long pos, int sneak) {
if (Main.DEBUG && (sneak < 0 || sneak > 3)) {
throw new IllegalStateException();
}
if (Main.DEBUG && ((pos & BetterBlockPos.POST_ADDITION_MASK) != pos)) {
throw new IllegalStateException();
}
long ret = pos
| (sneak & 0x1L) << 26 // snugly and cozily fit into the two bits left between Y and Z
| (sneak & 0x2L) << 35 // and between X and Y
| 1L << 63; // and turn on the top bit as a signal
if (Main.DEBUG && ((ret & BetterBlockPos.POST_ADDITION_MASK) != pos)) { // ensure that POST_ADDITION_MASK undoes this trickery (see the comments around POST_ADDITION_MASK definition for why/how)
throw new IllegalStateException();
}
return ret;
}
public static int decode(long posAndSneak) {
if (Main.DEBUG && !hasSneak(posAndSneak)) {
throw new IllegalStateException();
}
return (int) (posAndSneak >> 26 & 0x1L | posAndSneak >> 35 & 0x2L);
}
public static boolean hasSneak(long posAndSneak) {
return posAndSneak < 0; // checks the MSB like a boss (epically)
}
public static Face sneakDirectionFromPlayerToSupportingBlock(long posAndSneak) {
if (hasSneak(posAndSneak)) {
return Face.HORIZONTALS[Face.oppositeHorizontal(decode(posAndSneak))];
} else {
return null;
}
}
public static Face sneakDirectionFromSupportingBlockToPlayer(long posAndSneak) {
if (hasSneak(posAndSneak)) {
return Face.HORIZONTALS[decode(posAndSneak)];
} else {
return null;
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import java.util.OptionalLong;
public class SolvedActionStep {
private final long placePosition;
private final long playerEndPosition;
private final Face towardsSneakSupport;
public SolvedActionStep(long posAndSneak) {
this(posAndSneak, -1);
}
public SolvedActionStep(long posAndSneak, long blockPlacedAt) {
this.playerEndPosition = posAndSneak & BetterBlockPos.POST_ADDITION_MASK;
this.placePosition = blockPlacedAt;
this.towardsSneakSupport = SneakPosition.sneakDirectionFromPlayerToSupportingBlock(posAndSneak);
if (Main.DEBUG && blockPlacedAt < -1) {
throw new IllegalStateException();
}
}
public OptionalLong placeAt() {
return placePosition == -1 ? OptionalLong.empty() : OptionalLong.of(placePosition);
}
public long playerMovesTo() {
return playerEndPosition;
}
public Face towardsSupport() {
return towardsSneakSupport;
}
public boolean sneaking() {
return towardsSneakSupport != null;
}
}

View File

@@ -0,0 +1,139 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.builder.DependencyGraphScaffoldingOverlay.CollapsedDependencyGraph.CollapsedDependencyGraphComponent;
import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
import java.util.Set;
import java.util.stream.Collectors;
public class SolverEngineHarness {
private final ISolverEngine engine;
private final PackedBlockStateCuboid blocks;
private final PlaceOrderDependencyGraph graph;
private final Scaffolder.Output scaffolder;
public SolverEngineHarness(ISolverEngine engine, PackedBlockStateCuboid blocks, IScaffolderStrategy scaffolderStrategy) {
this.engine = engine;
this.blocks = blocks;
this.graph = new PlaceOrderDependencyGraph(blocks);
this.scaffolder = Scaffolder.run(graph, scaffolderStrategy);
}
public List<SolvedActionStep> solve(long playerStartPos) {
LongOpenHashSet alreadyPlacedSoFar = new LongOpenHashSet();
List<SolvedActionStep> steps = new ArrayList<>();
while (true) {
Set<CollapsedDependencyGraphComponent> frontier = calculateCurrentSolverFrontier(alreadyPlacedSoFar);
if (frontier.isEmpty()) {
// nothing on the table!
break;
}
List<LongOpenHashSet> goals = expandAndSubtract(frontier, alreadyPlacedSoFar);
long playerPos = steps.isEmpty() ? playerStartPos : steps.get(steps.size() - 1).playerMovesTo();
SolverEngineInput inp = new SolverEngineInput(graph, scaffolder.scaffolding(), alreadyPlacedSoFar, goals, playerPos);
SolverEngineOutput out = engine.solve(inp);
if (Main.DEBUG) {
out.sanityCheck(inp);
}
steps.addAll(out.getSteps());
LongList ancillaryScaffolding = new LongArrayList();
for (SolvedActionStep step : out.getSteps()) {
OptionalLong blockPlace = step.placeAt();
if (blockPlace.isPresent()) {
long pos = blockPlace.getAsLong();
if (!alreadyPlacedSoFar.add(pos)) {
throw new IllegalStateException();
}
if (scaffolder.air(pos)) { // not part of the schematic, nor intended scaffolding
ancillaryScaffolding.add(pos); // therefore it must be ancillary scaffolding, some throwaway block we needed to place in order to achieve something else, maybe to get a needed vantage point on some particularly tricky placement
}
}
}
scaffolder.enableAncillaryScaffoldingAndRecomputeRoot(ancillaryScaffolding);
}
if (Main.DEBUG) {
scaffolder.forEachReal(pos -> {
if (!alreadyPlacedSoFar.contains(pos)) {
throw new IllegalStateException();
}
});
alreadyPlacedSoFar.forEach(pos -> {
if (!scaffolder.real(pos)) {
throw new IllegalStateException();
}
});
}
return steps;
}
private List<LongOpenHashSet> expandAndSubtract(Set<CollapsedDependencyGraphComponent> frontier, LongSet already) {
return frontier.stream()
.map(component -> {
LongOpenHashSet remainingPositionsInComponent = new LongOpenHashSet(component.getPositions().size());
LongIterator it = component.getPositions().iterator();
while (it.hasNext()) {
long pos = it.nextLong();
if (!already.contains(pos)) {
remainingPositionsInComponent.add(pos);
}
}
return remainingPositionsInComponent;
}).collect(Collectors.toList());
}
private Set<CollapsedDependencyGraphComponent> calculateCurrentSolverFrontier(LongSet alreadyPlacedSoFar) {
Set<CollapsedDependencyGraphComponent> currentFrontier = new ObjectOpenHashSet<>();
Set<CollapsedDependencyGraphComponent> confirmedFullyCompleted = new ObjectOpenHashSet<>();
ObjectArrayFIFOQueue<CollapsedDependencyGraphComponent> toExplore = new ObjectArrayFIFOQueue<>();
toExplore.enqueue(scaffolder.getRoot());
outer:
while (!toExplore.isEmpty()) {
CollapsedDependencyGraphComponent component = toExplore.dequeue();
for (CollapsedDependencyGraphComponent parent : component.getIncoming()) {
if (!confirmedFullyCompleted.contains(parent)) {
// to be here, one parent must have been fully completed, but it's possible a different parent is not yet fully completed
// this is because while the collapsed block placement dependency graph is a directed acyclic graph, it can still have a diamond shape
// imagine the dependency is A->B, A->C, C->D, B->E, D->E (so, A is root, it splits in two, then converges at E)
// it might explore it in order A, B, C, E (because B, yet canceled as D is not completed yet), D (because C), E (now succeeds since B and D are confirmed)
continue outer;
}
}
LongIterator it = component.getPositions().iterator();
while (it.hasNext()) {
if (!alreadyPlacedSoFar.contains(it.nextLong())) {
currentFrontier.add(component);
continue outer;
}
}
if (confirmedFullyCompleted.add(component)) {
for (CollapsedDependencyGraphComponent child : component.getOutgoing()) {
toExplore.enqueue(child); // always reenqueue children because we added ourselves to confirmedFullyCompleted, meaning this time they may well have all parents completed
}
}
}
return currentFrontier;
}
}

View File

@@ -0,0 +1,141 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.longs.LongSets;
import java.util.Collection;
import java.util.List;
public class SolverEngineInput {
public final PlaceOrderDependencyGraph graph;
public final LongSet intendedScaffolding;
public final LongSet alreadyPlaced;
public final LongSet allToPlaceNow;
private final List<LongOpenHashSet> toPlaceNow;
public final Bounds bounds;
public final long player;
/**
* @param graph The place dependency graph of the end goal (both schematic and terrain). Where the terrain ends and the schematic begins is immaterial.
* @param intendedScaffolding These locations are overridden from air to scaffolding in the dependency graph. Also, bias towards placing scaffolding blocks in these locations (since the scaffolder tells us that'll be helpful to future sections)
* @param alreadyPlaced Locations that are currently not-air. Can include entries in intendedScaffolding (indicating placed scaffolding), entries that are non-air in the graph (indicating completed parts of the build), and even entries that are air in the graph and not in intendedScaffolding (indicating incidental scaffolding from previous sections that the scaffolder did not anticipate needing to place)
* @param toPlaceNow Locations that are currently top of mind and must be placed. For example, to place the rest of the graph, this would be intendedScaffolding|(graph.allNonAir&~alreadyPlaced)
* @param player Last but not least, where is the player standing?
*/
public SolverEngineInput(PlaceOrderDependencyGraph graph, LongSets.UnmodifiableSet intendedScaffolding, LongOpenHashSet alreadyPlaced, List<LongOpenHashSet> toPlaceNow, long player) {
this.graph = graph;
this.intendedScaffolding = intendedScaffolding;
this.alreadyPlaced = LongSets.unmodifiable(alreadyPlaced);
this.toPlaceNow = toPlaceNow;
this.player = player;
this.allToPlaceNow = combine(toPlaceNow);
this.bounds = graph.bounds();
if (Main.DEBUG) {
sanityCheck();
}
}
private void sanityCheck() {
if (!bounds.inRangePos(player)) {
throw new IllegalStateException();
}
for (LongSet toVerify : new LongSet[]{intendedScaffolding, alreadyPlaced}) {
for (long pos : toVerify) {
if (!bounds.inRangePos(pos)) {
throw new IllegalStateException();
}
}
}
for (LongSet toPlace : toPlaceNow) {
for (long pos : toPlace) {
if (alreadyPlaced.contains(pos)) {
throw new IllegalStateException();
}
if (!bounds.inRangePos(pos)) {
throw new IllegalStateException();
}
if (intendedScaffolding.contains(pos) ^ graph.airTreatedAsScaffolding(pos)) {
throw new IllegalStateException();
}
}
}
}
public PlacementDesire desiredToBePlaced(long pos) {
if (Main.DEBUG && !graph.bounds().inRangePos(pos)) {
throw new IllegalStateException();
}
if (allToPlaceNow.contains(pos)) {
if (Main.DEBUG && (intendedScaffolding.contains(pos) != graph.airTreatedAsScaffolding(pos))) {
throw new IllegalStateException("adding this sanity check 2+ years later, hope it's correct");
}
if (graph.airTreatedAsScaffolding(pos)) {
return PlacementDesire.SCAFFOLDING_OF_CURRENT_GOAL;
} else {
return PlacementDesire.PART_OF_CURRENT_GOAL;
}
} else {
// for positions NOT in allToPlaceNow, intendedScaffolding is not guaranteed to be equivalent to airTreatedAsScaffolding
if (graph.airTreatedAsScaffolding(pos)) {
if (intendedScaffolding.contains(pos)) {
return PlacementDesire.SCAFFOLDING_OF_FUTURE_GOAL;
} else {
return PlacementDesire.ANCILLARY;
}
} else {
return PlacementDesire.PART_OF_FUTURE_GOAL;
}
}
}
public enum PlacementDesire {
PART_OF_CURRENT_GOAL,
PART_OF_FUTURE_GOAL,
SCAFFOLDING_OF_CURRENT_GOAL,
SCAFFOLDING_OF_FUTURE_GOAL,
ANCILLARY
}
private static LongOpenHashSet combine(List<LongOpenHashSet> entries) {
LongOpenHashSet ret = new LongOpenHashSet(entries.stream().mapToInt(Collection::size).sum());
for (LongOpenHashSet set : entries) {
LongIterator it = set.iterator();
while (it.hasNext()) {
ret.add(it.nextLong());
}
}
return ret;
}
public BlockStateCachedData at(long pos, WorldState inWorldState) {
if (bounds.inRangePos(pos)) {
if (inWorldState.blockExists(pos)) {
return graph.data(pos);
} else {
return FakeStates.AIR;
}
} else {
return FakeStates.OUT_OF_BOUNDS;
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import java.util.Collections;
import java.util.List;
public class SolverEngineOutput {
private final List<SolvedActionStep> steps;
public SolverEngineOutput(List<SolvedActionStep> steps) {
this.steps = steps;
}
public void sanityCheck(SolverEngineInput in) {
for (SolvedActionStep step : steps) {
step.placeAt().ifPresent(pos -> {
if (!in.graph.bounds().inRangePos(pos)) {
throw new IllegalStateException();
}
});
if (!in.graph.bounds().inRangePos(step.playerMovesTo())) {
throw new IllegalStateException();
}
}
long prev = in.player;
for (SolvedActionStep step : steps) {
long curr = step.playerMovesTo();
sanityCheckMovement(prev, curr);
prev = curr;
}
if (steps.get(steps.size() - 1).sneaking()) {
throw new IllegalStateException();
}
}
public List<SolvedActionStep> getSteps() {
return Collections.unmodifiableList(steps);
}
private static void sanityCheckMovement(long from, long to) {
int dx = BetterBlockPos.XfromLong(from) - BetterBlockPos.XfromLong(to);
int dz = BetterBlockPos.ZfromLong(from) - BetterBlockPos.ZfromLong(to);
if (Math.abs(dx) + Math.abs(dz) > 1) {
throw new IllegalStateException();
}
}
}

View File

@@ -0,0 +1,233 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.*;
import java.util.ArrayDeque;
import java.util.Collections;
/**
* Tarjans algorithm destructured into a coroutine-like layout with an explicit "call stack" on the heap
* <p>
* This is needed so as to not stack overflow on huge schematics sadly. My test schematic (128x128x128) creates a
* call stack of depth 2 million, which the JVM cannot handle on its own on the stack, but it has no trouble with an
* ArrayDeque of 2 million entries on the heap.
*/
public class TarjansAlgorithm {
private final DependencyGraphScaffoldingOverlay graph;
private final Long2ObjectOpenHashMap<TarjanVertexInfo> infoMap;
private ArrayDeque<TarjanVertexInfo> vposStack;
private ArrayDeque<TarjanVertexInfo> tarjanCallStack;
private int index;
private TarjansResult result;
private TarjansAlgorithm(DependencyGraphScaffoldingOverlay overlayedGraph) {
this.graph = overlayedGraph;
this.infoMap = new Long2ObjectOpenHashMap<>();
this.result = new TarjansResult();
this.vposStack = new ArrayDeque<>();
this.tarjanCallStack = new ArrayDeque<>();
}
public static TarjansResult run(DependencyGraphScaffoldingOverlay overlayedGraph) {
TarjansAlgorithm algo = new TarjansAlgorithm(overlayedGraph);
algo.run();
if (Main.VERY_SLOW_DEBUG) {
sanityCheckResult(overlayedGraph, algo.result);
}
return algo.result;
}
private void run() {
if (Main.DEBUG) {
//System.out.println("Tarjan start");
}
long a = System.currentTimeMillis();
graph.forEachReal(pos -> {
strongConnect(pos);
while (!tarjanCallStack.isEmpty()) {
strongConnectPart2(tarjanCallStack.pop());
}
});
if (Main.DEBUG) {
//System.out.println("Tarjan end " + (System.currentTimeMillis() - a) + "ms");
}
}
private void strongConnect(long vpos) {
if (Main.DEBUG && !graph.real(vpos)) {
throw new IllegalStateException();
}
TarjanVertexInfo info = infoMap.get(vpos);
if (info == null) {
info = createInfo(vpos);
} else {
if (info.doneWithMainLoop) {
return;
}
}
strongConnectPart2(info);
}
private TarjanVertexInfo createInfo(long vpos) {
TarjanVertexInfo info = new TarjanVertexInfo();
info.pos = vpos;
info.index = index++;
info.lowlink = info.index;
vposStack.push(info);
info.onStack = true;
infoMap.put(vpos, info);
return info;
}
private void strongConnectPart2(TarjanVertexInfo info) {
if (info.doneWithMainLoop) {
throw new IllegalStateException();
}
long vpos = info.pos;
for (int fi = info.facesCompleted; fi < Face.NUM_FACES; fi++) {
Face face = Face.VALUES[fi];
if (graph.outgoingEdge(vpos, face)) {
long wpos = face.offset(vpos);
TarjanVertexInfo winfo = infoMap.get(wpos);
if (winfo == null) {
if (info.recursingInto != -1) {
throw new IllegalStateException();
}
info.recursingInto = wpos;
winfo = createInfo(wpos);
tarjanCallStack.push(info);
tarjanCallStack.push(winfo);
return;
}
if (info.recursingInto == wpos) {
info.lowlink = Math.min(info.lowlink, winfo.lowlink);
info.recursingInto = -1;
} else if (winfo.onStack) {
info.lowlink = Math.min(info.lowlink, winfo.index);
}
}
info.facesCompleted = fi + 1;
}
if (info.recursingInto != -1) {
throw new IllegalStateException();
}
info.doneWithMainLoop = true;
if (info.lowlink == info.index) {
result.startComponent();
TarjanVertexInfo winfo;
do {
winfo = vposStack.pop();
winfo.onStack = false;
result.addNode(winfo.pos);
} while (winfo.pos != vpos);
}
}
public static class TarjansResult {
private final Long2IntOpenHashMap posToComponent = new Long2IntOpenHashMap();
private int componentID = -1;
private TarjansResult() {
posToComponent.defaultReturnValue(-1);
}
private void startComponent() {
componentID++;
}
private void addNode(long nodepos) {
if (posToComponent.put(nodepos, componentID) != -1) {
throw new IllegalStateException();
}
}
public int getComponent(long nodepos) {
int result = posToComponent.get(nodepos);
if (result == -1) {
throw new IllegalStateException();
}
return result;
}
public int numComponents() {
return componentID + 1;
}
}
private static class TarjanVertexInfo {
private long pos;
private int index;
private int lowlink;
private boolean onStack;
private boolean doneWithMainLoop;
private long recursingInto = -1;
private int facesCompleted;
}
public static void sanityCheckResult(DependencyGraphScaffoldingOverlay graph, TarjansResult result) {
// this is a much slower (O(n^2) at least instead of O(n)) implementation of finding strongly connected components
Int2ObjectOpenHashMap<LongSet> checkedCids = new Int2ObjectOpenHashMap<>();
LongSet claimedAlready = new LongOpenHashSet();
graph.forEachReal(pos -> {
int cid = result.getComponent(pos);
LongSet componentShouldBe = checkedCids.get(cid);
if (componentShouldBe == null) {
componentShouldBe = sanityCheckResultFrom(graph, pos);
checkedCids.put(cid, componentShouldBe);
LongIterator it = componentShouldBe.iterator();
while (it.hasNext()) {
if (!claimedAlready.add(it.nextLong())) {
throw new IllegalStateException();
}
}
}
if (!componentShouldBe.contains(pos)) {
throw new IllegalStateException();
}
});
}
private static LongSet sanityCheckResultFrom(DependencyGraphScaffoldingOverlay graph, long start) {
if (graph.air(start)) {
throw new IllegalStateException();
}
LongList startList = new LongArrayList(Collections.singletonList(start));
LongSet reachableForward = DependencyGraphAnalyzer.searchGraph(startList, graph::outgoingEdge);
LongSet reachableBackward = DependencyGraphAnalyzer.searchGraph(startList, graph::incomingEdge);
// correct iff the intersection of reachableForward and reachableBackward is exactly the component containing start
LongSet ret = new LongOpenHashSet();
boolean selection = reachableForward.size() < reachableBackward.size();
LongSet toIterate = selection ? reachableForward : reachableBackward;
LongSet toCheck = selection ? reachableBackward : reachableForward;
LongIterator it = toIterate.iterator();
while (it.hasNext()) {
long pos = it.nextLong();
if (toCheck.contains(pos)) {
ret.add(pos);
}
}
return ret;
}
}

View File

@@ -0,0 +1,225 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import baritone.utils.BlockStateInterface;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import java.util.Iterator;
/**
* Just some testing stuff
*/
public class Testing {
private static final int MAX_LEAF_LEVEL = 16;
public static class AbstractNodeCostSearch {
long[] KEYS_CACHE = new long[MAX_LEAF_LEVEL];
int[] VALS_CACHE = new int[MAX_LEAF_LEVEL];
}
public static abstract class BlockStateInterfaceAbstractWrapper {
protected long zobrist;
public abstract IBlockState get(long coord);
public IBlockState get1(int x, int y, int z) {
return get(BetterBlockPos.toLong(x, y, z));
}
public BlockStateInterfaceAbstractWrapper with(long packedCoordinate, int state, AbstractNodeCostSearch ref) {
return new BlockStateInterfaceLeafDiff(this, packedCoordinate, Block.BLOCK_STATE_IDS.getByValue(state));
}
}
public static class BlockStateInterfaceLeafDiff extends BlockStateInterfaceAbstractWrapper {
private BlockStateInterfaceAbstractWrapper delegate;
private final long coord;
private final IBlockState state;
public BlockStateInterfaceLeafDiff(BlockStateInterfaceAbstractWrapper delegate, long coord, IBlockState state) {
this.delegate = delegate;
this.state = state;
this.coord = coord;
zobrist = updateZobristHash(delegate.zobrist, coord, state, delegate.get(coord));
}
public static long updateZobristHash(long oldZobristHash, long coord, IBlockState emplaced, IBlockState old) {
if (old != null) {
return updateZobristHash(updateZobristHash(oldZobristHash, coord, emplaced, null), coord, old, null);
}
return oldZobristHash ^ BetterBlockPos.murmur64(BetterBlockPos.longHash(coord) ^ Block.BLOCK_STATE_IDS.get(emplaced));
}
@Override
public IBlockState get(long coord) {
if (this.coord == coord) {
return state;
}
return delegate.get(coord);
}
public int leafLevels() { // cant cache leaflevel because of how materialize changes it
return 1 + (delegate instanceof BlockStateInterfaceLeafDiff ? ((BlockStateInterfaceLeafDiff) delegate).leafLevels() : 0);
}
@Override
public BlockStateInterfaceAbstractWrapper with(long packedCoordinate, int state, AbstractNodeCostSearch ref) {
int level = leafLevels();
if (level < MAX_LEAF_LEVEL) {
return super.with(packedCoordinate, state, ref);
}
if (level > MAX_LEAF_LEVEL) {
throw new IllegalStateException();
}
BlockStateInterfaceLeafDiff parent = this;
for (int i = 0; i < MAX_LEAF_LEVEL / 2; i++) {
parent = (BlockStateInterfaceLeafDiff) parent.delegate;
}
parent.delegate = ((BlockStateInterfaceLeafDiff) parent.delegate).materialize(ref);
return super.with(packedCoordinate, state, ref);
}
public BlockStateInterfaceMappedDiff materialize(AbstractNodeCostSearch ref) {
BlockStateInterfaceLeafDiff parent = this;
int startIdx = MAX_LEAF_LEVEL;
BlockStateInterfaceMappedDiff ancestor;
long[] keys = ref.KEYS_CACHE;
int[] vals = ref.VALS_CACHE;
while (true) {
startIdx--;
keys[startIdx] = parent.coord;
vals[startIdx] = Block.BLOCK_STATE_IDS.get(parent.state);
if (parent.delegate instanceof BlockStateInterfaceLeafDiff) {
parent = (BlockStateInterfaceLeafDiff) parent.delegate;
} else {
if (parent.delegate instanceof BlockStateInterfaceMappedDiff) {
ancestor = (BlockStateInterfaceMappedDiff) parent.delegate;
} else {
ancestor = new BlockStateInterfaceMappedDiff((BlockStateInterfaceWrappedSubstrate) parent.delegate);
}
break;
}
}
BlockStateInterfaceMappedDiff coalesced = new BlockStateInterfaceMappedDiff(ancestor, keys, vals, startIdx, zobrist);
//ref.zobristMap.put(zobrist, coalesced);
return coalesced;
}
}
public static class BlockStateInterfaceMappedDiff extends BlockStateInterfaceAbstractWrapper {
private final BlockStateInterfaceWrappedSubstrate delegate;
private final Long2ObjectOpenHashMap<Long2IntOpenHashMap> sections;
private static final long MASK = BetterBlockPos.toLong(15, 15, 15);
public BlockStateInterfaceMappedDiff(BlockStateInterfaceWrappedSubstrate substrate) {
this.delegate = substrate;
this.sections = new Long2ObjectOpenHashMap<>();
}
public BlockStateInterfaceMappedDiff(BlockStateInterfaceMappedDiff delegate, long[] diffKeys, int[] diffVals, int startIdx, long zobrist) {
this.sections = new Long2ObjectOpenHashMap<>();
this.zobrist = zobrist;
this.delegate = delegate.delegate;
for (int i = startIdx; i < diffKeys.length; i++) {
long bucket = diffKeys[i] & ~MASK;
Long2IntOpenHashMap val = sections.get(bucket);
if (val == null) {
Long2IntOpenHashMap parent = delegate.sections.get(bucket);
if (parent == null) {
val = new Long2IntOpenHashMap();
val.defaultReturnValue(-1);
} else {
val = parent.clone();
}
sections.put(bucket, val);
}
val.put(diffKeys[i] & MASK, diffVals[i]);
}
Iterator<Long2ObjectMap.Entry<Long2IntOpenHashMap>> it = delegate.sections.long2ObjectEntrySet().fastIterator();
while (it.hasNext()) {
Long2ObjectMap.Entry<Long2IntOpenHashMap> entry = it.next();
if (sections.containsKey(entry.getLongKey())) {
continue;
}
sections.put(entry.getLongKey(), entry.getValue());
}
}
@Override
public IBlockState get(long coord) {
Long2IntOpenHashMap candidateSection = sections.get(coord & ~MASK);
if (candidateSection != null) {
int val = candidateSection.get(coord & MASK);
if (val != -1) {
return Block.BLOCK_STATE_IDS.getByValue(val);
}
}
return delegate.get(coord);
}
}
public static class BlockStateInterfaceWrappedSubstrate extends BlockStateInterfaceAbstractWrapper {
private BlockStateInterface delegate;
@Override
public IBlockState get(long coord) {
return delegate.get0(BetterBlockPos.XfromLong(coord), BetterBlockPos.YfromLong(coord), BetterBlockPos.ZfromLong(coord));
}
/*@Override
public IBlockState getOverridden(long coord) {
return null;
}*/
}
public static class BlockStateLookupHelper {
// this falls back from System.identityHashCode to == so it's safe even in the case of a 32-bit collision
// but that would never have happened anyway: https://www.wolframalpha.com/input/?i=%28%282%5E32-1%29%2F%282%5E32%29%29%5E2000
private static Reference2IntOpenHashMap<IBlockState> states = new Reference2IntOpenHashMap<>();
static {
states.defaultReturnValue(-1); // normal default is 0
}
public static int lookupBlockState(IBlockState state) {
int stateMaybe = states.getInt(state);
if (stateMaybe >= 0) {
return stateMaybe;
}
int realState = Block.BLOCK_STATE_IDS.get(state); // uses slow REAL hashcode that walks through the Map of properties, gross
states.put(state, realState);
return realState;
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
public class Vec2d {
public final double x;
public final double z;
public Vec2d(double x, double z) {
this.x = x;
this.z = z;
}
public Vec2d plus(double dx, double dz) {
return new Vec2d(this.x + dx, this.z + dz);
}
public double[] toArr() {
return new double[]{x, z};
}
public static final Vec2d HALVED_CENTER = new Vec2d(0.5d, 0.5d);
}

View File

@@ -0,0 +1,89 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
public class Vec3d {
public final double x;
public final double y;
public final double z;
public Vec3d(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public Vec3d(double[] vec) {
this(vec[0], vec[1], vec[2]);
if (vec.length != 3) {
throw new IllegalArgumentException();
}
}
public boolean inOriginUnitVoxel() {
return x >= 0 && x <= 1 && y >= 0 && y <= 1 && z >= 0 && z <= 1;
}
public Vec3d plus(double x, double y, double z) {
return new Vec3d(this.x + x, this.y + y, this.z + z);
}
public long getRoundedToZeroPositionUnsafeDontUse() {
return BetterBlockPos.toLong((int) x, (int) y, (int) z);
}
public double distSq(Vec3d other) {
double dx = x - other.x;
double dy = y - other.y;
double dz = z - other.z;
return dx * dx + dy * dy + dz * dz;
}
public Face flatDirectionTo(Vec3d dst) {
return new Vec3d(dst.x - x, dst.y - y, dst.z - z).flatDirection();
}
private static final double AMBIGUITY_TOLERANCE = 0.01;
public Face flatDirection() {
if (Math.abs(Math.abs(x) - Math.abs(z)) < AMBIGUITY_TOLERANCE) {
throw new IllegalStateException("ambiguous");
}
if (Math.abs(x) > Math.abs(z)) {
if (x > 0) {
return Face.EAST;
} else {
return Face.WEST;
}
} else {
if (z > 0) {
return Face.SOUTH;
} else {
return Face.NORTH;
}
}
}
@Override
public String toString() {
return "Vec3d{" + x + "," + y + "," + z + "}";
}
}

View File

@@ -0,0 +1,123 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import it.unimi.dsi.fastutil.HashCommon;
import it.unimi.dsi.fastutil.longs.LongCollection;
import it.unimi.dsi.fastutil.longs.LongIterator;
import java.util.BitSet;
public abstract class WorldState {
/**
* https://en.wikipedia.org/wiki/Zobrist_hashing
*/
public final long zobristHash;
protected WorldState(long zobristHash) {
this.zobristHash = zobristHash;
}
public abstract boolean blockExists(long pos);
public WorldState withChild(long pos) {
return new WorldStateLeafDiff(this, pos);
}
public static long updateZobrist(long worldStateZobristHash, long changedPosition) {
return zobrist(changedPosition) ^ worldStateZobristHash;
}
public static long predetermineGoalZobrist(LongCollection goal) {
LongIterator it = goal.iterator();
long ret = 0;
while (it.hasNext()) {
ret ^= zobrist(it.nextLong());
}
return ret;
}
public static long zobrist(long packed) {
return HashCommon.mix(BetterBlockPos.ZOBRIST_MURMUR_MASK ^ packed);
}
public static long unzobrist(long zobrist) {
return BetterBlockPos.ZOBRIST_MURMUR_MASK ^ HashCommon.invMix(zobrist);
}
public static class WorldStateWrappedSubstrate extends WorldState {
private final Bounds bounds;
private final BitSet placed; // won't be copied, since alreadyPlaced is going to be **far** larger than toPlaceNow
private final int offset;
public WorldStateWrappedSubstrate(SolverEngineInput inp) {
super(0L);
this.bounds = inp.graph.bounds();
int min;
int max;
{
LongIterator positions = inp.alreadyPlaced.iterator();
if (!positions.hasNext()) {
throw new IllegalStateException();
}
min = bounds.toIndex(positions.nextLong());
max = min;
while (positions.hasNext()) {
int val = bounds.toIndex(positions.nextLong());
min = Math.min(min, val);
max = Math.max(max, val);
}
}
this.offset = min;
this.placed = new BitSet(max - min + 1);
LongIterator it = inp.alreadyPlaced.iterator();
while (it.hasNext()) {
placed.set(bounds.toIndex(it.nextLong()) - offset);
}
}
@Override
public boolean blockExists(long pos) {
int storedAt = bounds.toIndex(pos) - offset;
if (storedAt < 0) {
return false;
}
return placed.get(storedAt);
}
}
public static class WorldStateLeafDiff extends WorldState {
private WorldState delegate;
private final long pos;
private WorldStateLeafDiff(WorldState delegate, long pos) {
super(updateZobrist(delegate.zobristHash, pos));
this.delegate = delegate;
this.pos = pos;
}
@Override
public boolean blockExists(long pos) {
return this.pos == pos || delegate.blockExists(pos);
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
public class ZobristWorldStateCache {
private final Long2ObjectOpenHashMap<WorldState> zobristWorldStateCache;
public ZobristWorldStateCache(WorldState zeroEntry) {
this.zobristWorldStateCache = new Long2ObjectOpenHashMap<>();
this.zobristWorldStateCache.put(0L, zeroEntry);
}
public WorldState coalesceState(Node node) {
WorldState alr = zobristWorldStateCache.get(node.worldStateZobristHash);
if (alr != null) {
if (Main.DEBUG && alr.zobristHash != node.worldStateZobristHash) {
throw new IllegalStateException();
}
// don't check packedUnrealizedCoordinate here because it could exist (not -1) if a different route was taken to a zobrist-equivalent node (such as at a different player position) which was then expanded and coalesced
return alr;
}
if (Main.DEBUG && node.packedUnrealizedCoordinate == -1) {
throw new IllegalStateException();
}
long parent = WorldState.updateZobrist(node.worldStateZobristHash, node.packedUnrealizedCoordinate); // updateZobrist is symmetric because XOR, so the same operation can do child->parent as parent->child
WorldState myState = zobristWorldStateCache.get(parent).withChild(node.packedUnrealizedCoordinate);
if (Main.DEBUG && myState.zobristHash != node.worldStateZobristHash) {
throw new IllegalStateException();
}
zobristWorldStateCache.put(node.worldStateZobristHash, myState);
node.packedUnrealizedCoordinate = -1;
return myState;
}
}

View File

@@ -0,0 +1,307 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder.mc;
import baritone.builder.*;
import net.minecraft.block.*;
import net.minecraft.block.state.IBlockState;
import net.minecraft.util.EnumFacing;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* I expect this class to get extremely complicated.
* <p>
* Thankfully, all of it will be confined to this class, so when there's changes to new versions of Minecraft, e.g. new blocks, there will be only one place to look.
*/
public class BlockStatePropertiesExtractor {
public static BlockStateCachedDataBuilder getData(IBlockState state) {
Block block = state.getBlock();
BlockStateCachedDataBuilder builder = new BlockStateCachedDataBuilder();
// returns null only if we don't know how to walk on / walk around / place against this block
// if we don't know how to place the block, get everything else but add .placementLogicNotImplementedYe
// special cases
{
if (block instanceof BlockAir || block instanceof BlockStructureVoid) {
return builder.setAir();
}
if (block instanceof BlockStairs) {
boolean rightsideUp = state.getValue(BlockStairs.HALF) == BlockStairs.EnumHalf.BOTTOM; // true if normal stair, false if upside down stair
Face facing = fromMC(state.getValue(BlockStairs.FACING));
BlockStateCachedDataBuilder stairBuilder = new BlockStateCachedDataBuilder() {
@Override
protected PlaceAgainstData placeAgainstFace(Face face) {
if (face == facing) {
// this is "the back" of the stair, which is a full face that you can place against just fine
return new PlaceAgainstData(face, Half.EITHER, isMustSneakWhenPlacingAgainstMe());
}
return super.placeAgainstFace(face);
}
};
if (!rightsideUp) {
stairBuilder.fullyWalkableTop();
}
return stairBuilder.mustBePlacedAgainst(rightsideUp ? Half.BOTTOM : Half.TOP)
.collidesWithPlayer(true)
.collisionHeight(1)
.canPlaceAgainstMe()
.playerMustBeHorizontalFacingInOrderToPlaceMe(facing);
}
if (block instanceof BlockSlab) {
if (((BlockSlab) block).isDouble()) {
builder.placementLogicNotImplementedYet().collisionHeight(1);
} else if (state.getValue(BlockSlab.HALF) == BlockSlab.EnumBlockHalf.BOTTOM) {
builder.mustBePlacedAgainst(Half.BOTTOM).collisionHeight(0.5);
} else {
builder.mustBePlacedAgainst(Half.TOP).collisionHeight(1);
}
return builder
.fullyWalkableTop()
.canPlaceAgainstMe()
.collidesWithPlayer(true);
}
if (block instanceof BlockTrapDoor) {
boolean bottom = state.getValue(BlockTrapDoor.HALF) == BlockTrapDoor.DoorHalf.BOTTOM;
Face facing = fromMC(state.getValue(BlockTrapDoor.FACING));
return new BlockStateCachedDataBuilder() {
@Override
public List<BlockStatePlacementOption> howCanIBePlaced() {
List<BlockStatePlacementOption> ret = new ArrayList<>();
if (!(Main.STRICT_Y && !bottom)) {
ret.add(BlockStatePlacementOption.get(bottom ? Face.DOWN : Face.UP, Half.EITHER, Optional.ofNullable(facing.opposite()), Optional.empty()));
}
ret.add(BlockStatePlacementOption.get(facing.opposite(), bottom ? Half.BOTTOM : Half.TOP, Optional.empty(), Optional.empty()));
return ret;
}
}
.collisionHeight(1) // sometimes it can be 1, and for collision height we err on the side of max
.collidesWithPlayer(true); // dont allow walking on top of closed top-half trapdoor because redstone activation is scary and im not gonna predict it
}
if (block instanceof BlockLog) {
BlockLog.EnumAxis axis = state.getValue(BlockLog.LOG_AXIS);
BlockStateCachedDataBuilder logBuilder = new BlockStateCachedDataBuilder() {
@Override
public List<BlockStatePlacementOption> howCanIBePlaced() {
List<BlockStatePlacementOption> ret = super.howCanIBePlaced();
ret.removeIf(place -> BlockLog.EnumAxis.fromFacingAxis(place.against.toMC().getAxis()) != axis);
return ret;
}
};
if (axis == BlockLog.EnumAxis.NONE) {
logBuilder.placementLogicNotImplementedYet(); // ugh
}
return logBuilder
.fullyWalkableTop()
.collisionHeight(1)
.canPlaceAgainstMe()
.collidesWithPlayer(true);
}
if (block instanceof BlockRotatedPillar) { // hay block, bone block
// even though blocklog inherits from blockrotatedpillar it uses its own stupid enum, ugh
// this is annoying because this is pretty much identical
EnumFacing.Axis axis = state.getValue(BlockRotatedPillar.AXIS);
BlockStateCachedDataBuilder rotatedPillarBuilder = new BlockStateCachedDataBuilder() {
@Override
public List<BlockStatePlacementOption> howCanIBePlaced() {
List<BlockStatePlacementOption> ret = super.howCanIBePlaced();
ret.removeIf(place -> place.against.toMC().getAxis() != axis);
return ret;
}
};
return rotatedPillarBuilder
.fullyWalkableTop()
.collisionHeight(1)
.canPlaceAgainstMe()
.collidesWithPlayer(true);
}
if (block instanceof BlockStructure) {
return null; // truly an error to encounter this
}
}
{
// blocks that have to be placed with the player facing a certain way
// rotated clockwise about the Y axis
if (block instanceof BlockAnvil) {
builder.playerMustBeHorizontalFacingInOrderToPlaceMe(fromMC(state.getValue(BlockAnvil.FACING).rotateYCCW()));
}
// unchanged
if (block instanceof BlockChest
// it is not right || block instanceof BlockSkull // TODO is this right?? skull can be any facing?
) { // TODO fence gate and lever
builder.playerMustBeHorizontalFacingInOrderToPlaceMe(fromMC(state.getValue(BlockHorizontal.FACING)));
}
// opposite
if (block instanceof BlockCocoa
|| block instanceof BlockEnderChest
|| block instanceof BlockEndPortalFrame
|| block instanceof BlockFurnace
|| block instanceof BlockGlazedTerracotta
|| block instanceof BlockPumpkin
|| block instanceof BlockRedstoneDiode // both repeater and comparator
) {
builder.playerMustBeHorizontalFacingInOrderToPlaceMe(fromMC(state.getValue(BlockHorizontal.FACING)).opposite());
}
}
// ladder
{
if (block instanceof BlockContainer || block instanceof BlockWorkbench) {
// TODO way more blocks have a right click action, e.g. redstone repeater, daylight sensor
builder.mustSneakWhenPlacingAgainstMe();
}
}
if (block instanceof BlockCommandBlock
|| block instanceof BlockDispenser // dropper extends from dispenser
|| block instanceof BlockPistonBase) {
builder.playerMustBeEntityFacingInOrderToPlaceMe(fromMC(state.getValue(BlockDirectional.FACING)));
}
if (block instanceof BlockObserver) {
builder.playerMustBeEntityFacingInOrderToPlaceMe(fromMC(state.getValue(BlockDirectional.FACING)).opposite());
}
// fully passable blocks
{
// some ways of determining this list that don't work:
// calling isPassable: even though blocks such as carpet and redstone repeaters are marked as passable, they are not really, they do have a height
// checking material: Material.PLANTS is not enough because it includes cocoa and chorus, Material.CIRCUITS is not enough because it includes redstone repeaters
// these are the blocks that you can fully walk through with no collision at all, such as torches and sugar cane
if (block instanceof BlockBush // includes crops
|| block instanceof BlockReed
|| block instanceof BlockTorch
//|| block instanceof BlockSign
|| block instanceof BlockRedstoneWire
|| block instanceof BlockRailBase
|| (block instanceof BlockFenceGate && state.getValue(BlockFenceGate.OPEN))
|| block instanceof BlockLever
|| block instanceof BlockButton
|| block instanceof BlockBanner
// TODO include pressure plate, tripwire, tripwire hook?
) {
builder.collidesWithPlayer(false);
} else {
builder.collidesWithPlayer(true);
}
}
if (block instanceof BlockFalling) {
builder.mustBePlacedBottomToTop();
}
// TODO multiblocks like door and bed and double plant
boolean fullyUnderstood = false; // set this flag to true for any state for which we have fully and completely described it
// getStateForPlacement.against is the against face. placing a torch will have it as UP. placing a bottom slab will have it as UP. placing a top slab will have it as DOWN.
if (block instanceof BlockFence || (block instanceof BlockFenceGate && !state.getValue(BlockFenceGate.OPEN)) || block instanceof BlockWall) {
builder.collisionHeight(1.5);
fullyUnderstood = true;
}
if (block instanceof BlockTorch) { // includes redstone torch
builder.canOnlyPlaceAgainst(fromMC(state.getValue(BlockTorch.FACING)).opposite());
fullyUnderstood = true;
}
if (block instanceof BlockShulkerBox) {
builder.canOnlyPlaceAgainst(fromMC(state.getValue(BlockShulkerBox.FACING)).opposite());
builder.collisionHeight(1); // TODO should this be 1.5 because sometimes the shulker is open?
fullyUnderstood = true;
if (state.getValue(BlockShulkerBox.FACING) == EnumFacing.DOWN && Main.STRICT_Y) {
// TODO fix downward facing shulker box in STRICT_Y mode
fullyUnderstood = false;
}
}
if (block instanceof BlockFenceGate // because if we place it we need to think about hypothetically walking through it
|| block instanceof BlockLever
|| block instanceof BlockButton
|| block instanceof BlockBanner) {
builder.placementLogicNotImplementedYet();
fullyUnderstood = true;
}
if (block instanceof BlockSapling
|| block instanceof BlockRedstoneWire
|| block instanceof BlockRailBase
|| block instanceof BlockFlower
|| block instanceof BlockDeadBush
|| block instanceof BlockMushroom
) {
builder.mustBePlacedBottomToTop();
fullyUnderstood = true;
}
//isBlockNormalCube=true implies isFullCube=true
if (state.isBlockNormalCube() || state.isFullBlock() || block instanceof BlockGlass || block instanceof BlockStainedGlass) {
builder.canPlaceAgainstMe();
fullyUnderstood = true;
}
if (state.isBlockNormalCube() || block instanceof BlockGlass || block instanceof BlockStainedGlass) {
builder.collisionHeight(1);
if (!(block instanceof BlockMagma || block instanceof BlockSlime)) {
builder.fullyWalkableTop();
}
fullyUnderstood = true;
}
if (block instanceof BlockSnow) {
fullyUnderstood = true;
builder.fullyWalkableTop().collisionHeight(0.125 * (state.getValue(BlockSnow.LAYERS) - 1));
// funny - if you have snow layers packed 8 high, it only supports the player to a height of 0.875, but it still counts as "isTopSolid" for placing stuff like torches on it
}
if (block instanceof BlockSoulSand) {
builder.collisionHeight(0.875).fakeLessThanFullHeight();
fullyUnderstood = true;
}
if (block instanceof BlockGrassPath || block instanceof BlockFarmland) {
builder.collisionHeight(0.9375);
fullyUnderstood = true;
}
// TODO fully walkable top and height
if (fullyUnderstood) {
return builder;
} else {
return null;
}
}
public static Face fromMC(EnumFacing facing) {
return Face.VALUES[facing.getIndex()];
}
}

View File

@@ -0,0 +1,112 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder.mc;
import baritone.builder.BlockData;
import baritone.builder.BlockStateCachedData;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.init.Blocks;
import java.util.*;
import java.util.stream.Stream;
public class DebugStates {
public static void debug() {
Map<Block, List<IBlockState>> bts = new HashMap<>();
Map<String, List<IBlockState>> byString = new HashMap<>();
BlockData data = new BlockData(new VanillaBlockStateDataProvider());
for (IBlockState state : Block.BLOCK_STATE_IDS) {
//System.out.println(state);
bts.computeIfAbsent(state.getBlock(), $ -> new ArrayList<>()).add(state);
String str = toString(data.get(Block.BLOCK_STATE_IDS.get(state)));
byString.computeIfAbsent(str, $ -> new ArrayList<>()).add(state);
}
for (String key : (Iterable<String>) byString.keySet().stream().sorted()::iterator) {
System.out.println("\n");
System.out.println(key);
Set<Block> skip = new HashSet<>();
List<IBlockState> matches = byString.get(key);
for (IBlockState state : matches) {
if (skip.contains(state.getBlock())) {
continue;
}
if (matches.containsAll(bts.get(state.getBlock()))) {
System.out.println("All " + bts.get(state.getBlock()).size() + " variants of " + state.getBlock());
skip.add(state.getBlock());
} else {
System.out.println(state);
}
}
}
/*System.out.println(Blocks.OAK_STAIRS.getDefaultState());
System.out.println(Block.BLOCK_STATE_IDS.get(Blocks.OAK_STAIRS.getDefaultState()));
System.out.println(Block.BLOCK_STATE_IDS.getByValue(Block.BLOCK_STATE_IDS.get(Blocks.OAK_STAIRS.getDefaultState())));*/
Set<Block> normal = new HashSet<>();
Block.REGISTRY.iterator().forEachRemaining(normal::add);
Set<Block> alternate = new HashSet<>();
for (IBlockState state : Block.BLOCK_STATE_IDS) {
alternate.add(state.getBlock());
}
if (!alternate.equals(normal)) {
throw new IllegalStateException();
}
outer:
for (Block block : normal) {
for (IBlockState state : block.getBlockState().getValidStates()) {
if (Block.BLOCK_STATE_IDS.get(state) == -1) {
System.out.println(state + " doesn't exist?!");
continue;
}
if (block == Blocks.TRIPWIRE) {
System.out.println(state + " does exist");
}
if (!Block.BLOCK_STATE_IDS.getByValue(Block.BLOCK_STATE_IDS.get(state)).equals(state)) {
System.out.println(block + " is weird");
continue outer;
}
}
}
}
public static String toString(BlockStateCachedData data) {
if (data == null) {
return "UNKNOWN";
}
Map<String, String> props = new LinkedHashMap<>();
props(data, props);
return props.toString();
}
private static void props(BlockStateCachedData data, Map<String, String> props) {
if (data.isAir) {
props.put("air", "true");
return;
}
props.put("collides", "" + data.collidesWithPlayer);
props.put("walkabletop", "" + data.fullyWalkableTop);
props.put("placeme", "" + data.placeMe.size());
props.put("sneak", "" + data.mustSneakWhenPlacingAgainstMe);
props.put("againstme", "" + Stream.of(data.placeAgainstMe).filter(Objects::nonNull).count());
if (data.collidesWithPlayer) {
props.put("y", "" + data.collisionHeightBlips());
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder.mc;
import baritone.builder.BlockStateCachedData;
import baritone.builder.BlockStateCachedDataBuilder;
import baritone.builder.IBlockStateDataProvider;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
public class VanillaBlockStateDataProvider implements IBlockStateDataProvider {
@Override
public int numStates() {
return Block.BLOCK_STATE_IDS.size();
}
@Override
public BlockStateCachedData getNullable(int i) {
IBlockState state = Block.BLOCK_STATE_IDS.getByValue(i);
if (state == null) {
return null;
}
try {
BlockStateCachedDataBuilder builder = BlockStatePropertiesExtractor.getData(state);
if (builder == null) {
return null;
} else {
return new BlockStateCachedData(builder);
}
} catch (Throwable th) {
throw new RuntimeException("Exception while extracting " + state + " ID " + i, th);
}
}
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 btrekkie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,15 @@
Original readme --> https://github.com/btrekkie/dynamic-connectivity/
I am experimenting with using dynamic connectivity to help with Baritone builder 2
https://en.wikipedia.org/wiki/Dynamic_connectivity
This code in this folder was written under the MIT license (see the LICENSE file). This is compatible with LGPL
per https://directory.fsf.org/wiki/License:Expat https://www.gnu.org/licenses/license-list.en.html#Expat
The new license for this modified version (the combined work including my modifications) is still the Baritone license,
which is LGPL.
The git histories of both repos https://github.com/btrekkie/dynamic-connectivity/ and its
dependency https://github.com/btrekkie/RedBlackNode/ have been included by using git's "merge unrelated histories"
feature, so the git blame is accurate for giving credit, I think.

View File

@@ -0,0 +1,44 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
/**
* A combining function for taking the augmentations associated with a set of ConnVertices and reducing them to a single
* result. The function is a binary operation for combining two values into one. For example, given vertices with
* augmentations A1, A2, A3, and A4, the combined result may be obtained by computing
* combine(combine(combine(A1, A2), A3), A4). In order for an augmentation result to be meaningful, the combining
* function must be commutative, meaning combine(x, y) is equivalent to combine(y, x), and associative, meaning
* combine(x, combine(y, z)) is equivalent to combine(combine(x, y), z).
* <p>
* If a ConnGraph represents a game map, then one example of an augmentation would be the amount of gold accessible from
* a certain location. Each vertex would be augmented with the amount of gold in that location, and the combining
* function would add the two amounts of gold passed in as arguments. Another example would be the strongest monster
* that can reach a particular location. Each vertex with at least one monster would be augmented with a pointer to the
* strongest monster at that location, and the combining function would return the stronger of the two monsters passed
* in as arguments. A third example would be the number of locations accessible from a given vertex. Each vertex would
* be augmented with the number 1, and the combining function would add the two numbers of vertices passed in as
* arguments.
* <p>
* ConnGraph treats two augmentation values X and Y as interchangeable if they are equal, as in
* X != null ? X.equals(Y) : Y == null. The same holds for two combined augmentation values, and for one combined
* augmentation value and one augmentation value.
* <p>
* See the comments for ConnGraph.
*/
public interface Augmentation {
/**
* Returns the result of combining the specified values into one. Each argument is either the augmentation
* information associated with a vertex, or the result of a previous call to "combine".
* <p>
* Note that a value of null is never passed in to indicate the absence of augmentation information. The fact that
* ConnGraph.getVertexAugmentation, for example, may return null when there is no associated augmentation might lead
* you to believe that a null argument indicates the absence of augmentation information, but again, it does not. A
* null argument can only mean that a vertex is explicitly associated with null augmentation information, due to a
* prior call to ConnGraph.setVertexAugmentation(vertex, null), or that the "combine" method previously returned
* null.
*/
public Object combine(Object value1, Object value2);
}

View File

@@ -0,0 +1,70 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
/**
* Represents an edge in a ConnGraph, at the level of the edge (i.e. at the lowest level i for which G_i contains the
* edge). Every graph edge has exactly one corresponding ConnEdge object, regardless of the number of levels it appears
* in. See the comments for the implementation of ConnGraph.
* <p>
* ConnEdges are stored in the linked lists suggested by EulerTourVertex.graphListHead and
* EulerTourVertex.forestListHead. Each ConnEdge is in two linked lists, so care must be taken when traversing the
* linked lists. prev1 and next1 are the links for the list starting at vertex1.graphListHead or vertex1.forestListHead,
* while prev2 and next2 are the links for vertex2. But the vertex1 and vertex2 fields of a given edge are different
* from the vertex1 and vertex2 fields of the linked edges. For example, the edge after next1 is not necessarily
* next1.next1. It depends on whether next1.vertex1 is the same as vertex1. If next1.vertex1 == vertex1, then the edge
* after next1 is next1.next1, but otherwise, it is next1.next2.
*/
class ConnEdge {
/**
* The edge's first endpoint (at the same level as the edge).
*/
public EulerTourVertex vertex1;
/**
* The edge's second endpoint (at the same level as the edge).
*/
public EulerTourVertex vertex2;
/**
* The EulerTourEdge object describing the edge's presence in an Euler tour tree, at the same level as the edge, or
* null if the edge is not in the Euler tour forest F_i.
*/
public EulerTourEdge eulerTourEdge;
/**
* The edge preceding this in a linked list of same-level edges adjacent to vertex1, if any. The edge is either part
* of a list of non-forest edges starting with vertex1.graphListHead, or part of a list of forest edges starting
* with vertex1.forestListHead. Note that this list excludes any edges that also appear in lower levels.
*/
public ConnEdge prev1;
/**
* The edge succeeding this in a linked list of same-level edges adjacent to vertex1, if any. The edge is either
* part of a list of non-forest edges starting with vertex1.graphListHead, or part of a list of forest edges
* starting with vertex1.forestListHead. Note that this list excludes any edges that also appear in lower levels.
*/
public ConnEdge next1;
/**
* The edge preceding this in a linked list of same-level edges adjacent to vertex2, if any. The edge is either part
* of a list of non-forest edges starting with vertex2.graphListHead, or part of a list of forest edges starting
* with vertex2.forestListHead. Note that this list excludes any edges that also appear in lower levels.
*/
public ConnEdge prev2;
/**
* The edge succeeding this in a linked list of same-level edges adjacent to vertex2, if any. The edge is either
* part of a list of non-forest edges starting with vertex2.graphListHead, or part of a list of forest edges
* starting with vertex2.forestListHead. Note that this list excludes any edges that also appear in lower levels.
*/
public ConnEdge next2;
public ConnEdge(EulerTourVertex vertex1, EulerTourVertex vertex2) {
this.vertex1 = vertex1;
this.vertex2 = vertex2;
}
}

View File

@@ -0,0 +1,52 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
import java.util.Random;
/**
* A vertex in a ConnGraph. See the comments for ConnGraph.
*/
public class ConnVertex {
/**
* The thread-local random number generator we use by default to set the "hash" field.
*/
private static final ThreadLocal<Random> random = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random();
}
};
/**
* A randomly generated integer to use as the return value of hashCode(). ConnGraph relies on random hash codes for
* its performance guarantees.
*/
private final long hash;
public ConnVertex() {
hash = random.get().nextLong();
}
/**
* Constructs a new ConnVertex.
*
* @param random The random number generator to use to produce a random hash code. ConnGraph relies on random hash
* codes for its performance guarantees.
*/
public ConnVertex(Random random) {
hash = random.nextLong();
}
@Override
public int hashCode() {
throw new UnsupportedOperationException();
}
public long getIdentity() {
return hash;
}
}

View File

@@ -0,0 +1,35 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
/**
* The representation of a forest edge in some Euler tour forest F_i at some particular level i. Each forest edge has
* one EulerTourEdge object for each level it appears in. See the comments for the implementation of ConnGraph.
*/
class EulerTourEdge {
/**
* One of the two visits preceding the edge in the Euler tour, in addition to visit2. (The node is at the same level
* as the EulerTourEdge.)
*/
public final EulerTourNode visit1;
/**
* One of the two visits preceding the edge in the Euler tour, in addition to visit1. (The node is at the same level
* as the EulerTourEdge.)
*/
public final EulerTourNode visit2;
/**
* The representation of this edge in the next-higher level. higherEdge is null if this edge is in the highest
* level.
*/
public EulerTourEdge higherEdge;
public EulerTourEdge(EulerTourNode visit1, EulerTourNode visit2) {
this.visit1 = visit1;
this.visit2 = visit2;
}
}

View File

@@ -0,0 +1,125 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
import baritone.builder.utils.com.github.btrekkie.red_black_node.RedBlackNode;
/**
* A node in an Euler tour tree for ConnGraph (at some particular level i). See the comments for the implementation of
* ConnGraph.
*/
class EulerTourNode extends RedBlackNode<EulerTourNode> {
/**
* The dummy leaf node.
*/
public static final EulerTourNode LEAF = new EulerTourNode(null, null);
/**
* The vertex this node visits.
*/
public final EulerTourVertex vertex;
/**
* The number of nodes in the subtree rooted at this node.
*/
public int size;
/**
* Whether the subtree rooted at this node contains a node "node" for which
* node.vertex.arbitraryNode == node && node.vertex.graphListHead != null.
*/
public boolean hasGraphEdge;
/**
* Whether the subtree rooted at this node contains a node "node" for which
* node.vertex.arbitraryNode == node && node.vertex.forestListHead != null.
*/
public boolean hasForestEdge;
/**
* The combining function for combining user-provided augmentations. augmentationFunc is null if this node is not in
* the highest level.
*/
public final Augmentation augmentationFunc;
/**
* The combined augmentation for the subtree rooted at this node. This is the result of combining the augmentation
* values node.vertex.augmentation for all nodes "node" in the subtree rooted at this node for which
* node.vertex.arbitraryVisit == node, using augmentationFunc. This is null if hasAugmentation is false.
*/
public Object augmentation;
/**
* Whether the subtree rooted at this node contains at least one augmentation value. This indicates whether there is
* some node "node" in the subtree rooted at this node for which node.vertex.hasAugmentation is true and
* node.vertex.arbitraryVisit == node.
*/
public boolean hasAugmentation;
public EulerTourNode(EulerTourVertex vertex, Augmentation augmentationFunc) {
this.vertex = vertex;
this.augmentationFunc = augmentationFunc;
}
/**
* Like augment(), but only updates the augmentation fields hasGraphEdge and hasForestEdge.
*/
public boolean augmentFlags() {
boolean newHasGraphEdge =
left.hasGraphEdge || right.hasGraphEdge || (vertex.arbitraryVisit == this && vertex.graphListHead != null);
boolean newHasForestEdge =
left.hasForestEdge || right.hasForestEdge ||
(vertex.arbitraryVisit == this && vertex.forestListHead != null);
if (newHasGraphEdge == hasGraphEdge && newHasForestEdge == hasForestEdge) {
return false;
} else {
hasGraphEdge = newHasGraphEdge;
hasForestEdge = newHasForestEdge;
return true;
}
}
@Override
public boolean augment() {
int newSize = left.size + right.size + 1;
boolean augmentedFlags = augmentFlags();
Object newAugmentation = null;
boolean newHasAugmentation = false;
if (augmentationFunc != null) {
if (left.hasAugmentation) {
newAugmentation = left.augmentation;
newHasAugmentation = true;
}
if (vertex.hasAugmentation && vertex.arbitraryVisit == this) {
if (newHasAugmentation) {
newAugmentation = augmentationFunc.combine(newAugmentation, vertex.augmentation);
} else {
newAugmentation = vertex.augmentation;
newHasAugmentation = true;
}
}
if (right.hasAugmentation) {
if (newHasAugmentation) {
newAugmentation = augmentationFunc.combine(newAugmentation, right.augmentation);
} else {
newAugmentation = right.augmentation;
newHasAugmentation = true;
}
}
}
if (newSize == size && !augmentedFlags && hasAugmentation == newHasAugmentation &&
(newAugmentation != null ? newAugmentation.equals(augmentation) : augmentation == null)) {
return false;
} else {
size = newSize;
augmentation = newAugmentation;
hasAugmentation = newHasAugmentation;
return true;
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
/**
* The representation of a ConnVertex at some particular level i. Each vertex has one EulerTourVertex object for each
* level it appears in. Note that different vertices may appear in different numbers of levels, as EulerTourVertex
* objects are only created for lower levels as needed. See the comments for the implementation of ConnGraph.
*/
class EulerTourVertex {
/**
* The representation of this edge in the next-lower level. lowerVertex is null if this is the lowest-level
* representation of this vertex.
*/
public EulerTourVertex lowerVertex;
/**
* The representation of this edge in the next-higher level. This is null if this vertex is in the highest level.
*/
public EulerTourVertex higherVertex;
/**
* An arbitrarily selected visit to the vertex in the Euler tour tree that contains it (at the same level as this).
*/
public EulerTourNode arbitraryVisit;
/**
* The first edge in the linked list of level-i edges that are adjacent to the vertex in G_i, but are not in the
* Euler tour forest F_i, where i is the level of the vertex. Note that this list excludes any edges that also
* appear in lower levels.
*/
public ConnEdge graphListHead;
/**
* The first edge in the linked list of level-i edges adjacent to the vertex that are in F_i, where i is the level
* of the vertex. Note that this list excludes any edges that also appear in lower levels.
*/
public ConnEdge forestListHead;
/**
* The augmentation associated with this vertex, if any. This is null instead if higherVertex != null.
*/
public Object augmentation;
/**
* Whether there is any augmentation associated with this vertex. This is false instead if higherVertex != null.
*/
public boolean hasAugmentation;
}

View File

@@ -0,0 +1,30 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.utils.com.github.btrekkie.connectivity;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
/**
* Describes a ConnVertex, with respect to a particular ConnGraph. There is exactly one VertexInfo object per vertex in
* a given graph, regardless of how many levels the vertex is in. See the comments for the implementation of ConnGraph.
*/
class VertexInfo {
/**
* The representation of the vertex in the highest level.
*/
public EulerTourVertex vertex;
/**
* A map from each ConnVertex adjacent to this vertex to the ConnEdge object for the edge connecting it to this
* vertex. Lookups take O(1) expected time and O(log N / log log N) time with high probability, because "edges" is a
* HashMap, and ConnVertex.hashCode() returns a random integer.
*/
public Long2ObjectOpenHashMap<ConnEdge> edges = new Long2ObjectOpenHashMap<>(); // TODO in my use case, each vertex will have at most 4 edges (at most one each to north, south, east, west). there's probably a MASSIVE performance improvement to be had by simply switching this to such an array. it might require some bitwise twiddling because the key is a long so we'd have to subtract xyz, but, it would work pretty easily just by copying the Face.horizontalIndex approach (x & 1 | (x | z) & 2)
public VertexInfo(EulerTourVertex vertex) {
this.vertex = vertex;
}
}

View File

@@ -0,0 +1,124 @@
Thoughts concerning optimization:
- I should not optimize until I have access to real-world input samples. Any
optimizations are bound to result in increased code complexity, which is not
worth it unless I can demonstrate a non-trivial reduction in running time in a
real-world setting.
- I should profile the program and see if there are any quick wins.
- Most obviously, I could implement the optimization of using a B-tree in the
top level, and see what effect this has on performance. It would definitely
improve the query time, and hopefully it wouldn't affect the update time too
much.
- If I do implement the B-tree optimization, I should also store a binary search
tree representation of the same forest in the top level, for the user-defined
augmentation. That way, updates will require O(log N) calls to
Augmentation.combine, rather than O(log^2 N / log log N) calls. We have no
control over how long Augmentation.combine takes, so we should minimize calls
to that method. It will take a bit of care to ensure that fetching the
augmentation for a connected component takes O(log N / log log N) time,
however.
- Alternatively, in each B-tree node, we could store a binary search tree that
combines the augmentation information for the items in that node. This might
even benefit ConnGraphs that do not have user-supplied augmentation.
- Actually, there is a way of speeding up queries by an O(log log N) factor that
should be cleaner and easier to implement than a B-tree. We could change each
Euler tour node in the top level to store its kth ancestor (e.g. if k = 3,
then each node stores a pointer to its great grandparent). Each time we change
a node's parent, we have to change the pointers for all of the descendants
that are less than k levels below the node - up to 2^k - 1 nodes. Since there
are O(log N) parent pointer changes per update, and updates already take
O(log^2 N) amortized time, we can afford a k value of up to lg lg N + O(1)
(but interestingly, not O(log log N)). However, I think this would be
significantly slower than a B-tree in practice.
- If I don't implement the B-tree optimization, there are a couple of small
optimizations I could try. First, I could move the field
EulerTourNode.augmentationFunc to EulerTourVertex, in order to save a little
bit of space. Second, I could create EulerTourNode and EulerTourVertex
subclasses specifically for the top level, and offload the fields related to
user augmentation to those subclasses. These changes seem inelegant, but it
might be worth checking the effect they have on performance.
- I looked at the heuristics recommended in
http://people.csail.mit.edu/karger/Papers/impconn.pdf (Iyer, et al. (2001): An
Experimental Study of Poly-Logarithmic Fully-Dynamic Connectivity Algorithms).
The first heuristic is: before pushing down all of the same-level forest
edges, which is an expensive operation, sample O(log N) same-level non-forest
edges to see if we can get lucky and find a replacement edge without pushing
anything. The second heuristic is to refrain from pushing edges in
sufficiently small components.
The first heuristic seems reasonable. However, I don't get the second
heuristic. The concept seems to be basically the same as the first - don't
push down any edges if we can cheaply find a replacement edge. However, the
execution is cruder. Rather than limiting the number of edges to search, which
is closely related to the cost of the search, the second heuristic is based on
the number of vertices, which is not as closely related.
- I have a few ideas for implementing this first heuristic, which could be
attempted and their effects on performance measured. The first is that to
sample the graph edges in a semi-random order, I could augment each Euler tour
node with the sum across all canonical visits to vertices of the number of
adjacent same-level graph edges. Then, to obtain a sample, we select each
vertex with a probability proportional to this adjacency number. This is
fairly straightforward: at each node, we decide to go left, go right, or use
the current vertex in proportion to the augmentation.
This is not exactly random, because after we select a vertex, we choose an
arbitrary adjacent edge. However, it seems close enough, and it's not easy to
do better.
- After sampling an edge, we should remove it from the adjacency list, to avoid
repeatedly sampling the same edge. We should then store it in an array of
edges we sampled, so that later, we can either re-add the edges to the
adjacency lists or push all of them down as necessary.
- If the sampling budget (i.e. the number of edges we intend to sample) is at
least the number of same-level graph edges, then we should forgo pushing down
any edges, regardless of whether we find a replacement edge.
- We don't need to sample edges from the smaller of the two post-cut trees. We
can sample them from the larger one if it has fewer same-level graph edges.
This increases our chances of finding a replacement edge if there is one. As
long as we don't push down any forest edges in the larger tree, we're safe.
- With an extra augmentation, we can determine whether there is probably a
replacement edge. This helps us because if there is probably no replacement
edge, then we can save some time by skipping edge sampling entirely. (If the
sampling budget is at least the number of same-level graph edges, then we
should also refrain from pushing down any edges, as in a previously mentioned
optimization.)
Assign each ConnEdge a random integer ID. Store the XOR of the IDs of all
adjacent same-level graph edges in each of the vertices. Augment the Euler
tour trees with the XOR of those values for all canonical visits to vertices.
The XOR stored in a post-cut tree's root node is equal to the XOR of all of
the replacement edges' IDs, because each non-replacement edge is in two
adjacency lists and cancels itself out, while each replacement edge is in one
adjacency list. Thus, the XOR is 0 if there is no replacement edge, and it is
non-zero with probability 1 - 1 / 2^32 if there is at least one replacement
edge.
- If one of the post-cut trees has a same-level forest edge and the other does
not, and the difference in the number of same-level graph edges is not that
large, we should favor the one that does not, because it's expensive to push
forest edges. Also, there's no need to sample if we pick a tree that has no
same-level forest edges.
- I wonder if there's a good way to estimate the cost of pushing down a given
set of forest edges. For example, is there a strong correlation between the
number of forest edges and the cost of pushing them? We could use a larger
sampling budget the greater this cost is.
- During sampling, it might help to push down edges whose endpoints are
connected in the next-lower level, and to not count them against the sampling
budget. By paying for some of the edges, we're able to sample more edges, so
we're less likely to have to push down the forest edges.
The downside is that checking whether the endpoints are in the same connected
component takes O(log N) time. To mitigate this, we should refrain from
attempting to push down edges until necessary. That is, we should spend the
entire sampling budget first, then search the edges we sampled for an edge
that we can push down. After finding one such edge, we should sample another
edge, then search for another edge to push down, etc.
- When we are done sampling edges, it might still be beneficial to iterate over
the rest of the same-level graph edges in a (semi-)random order. This does
increase the amount of time that iteration takes. However, using a random
order could be helpful if the sequence of updates has some sort of pattern
affecting the location of replacement edges. For example, if there is a
tendency to put replacement edges near the end of the Euler tour, or to
cluster replacement edges so that they are close to each other in the Euler
tour, then random iteration should tend to locate a replacement edge faster
than in-order iteration. (If we know from a previously mentioned optimization
that there is probably no replacement edge, then we shouldn't bother to
iterate over the edges in random order.)

View File

@@ -0,0 +1,36 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package baritone.builder.utils.com.github.btrekkie.red_black_node;
/**
* Wraps a value using reference equality. In other words, two references are equal only if their values are the same
* object instance, as in ==.
*
* @param <T> The type of value.
*/
class Reference<T> {
/**
* The value this wraps.
*/
private final T value;
public Reference(T value) {
this.value = value;
}
public boolean equals(Object obj) {
if (!(obj instanceof Reference)) {
return false;
}
Reference<?> reference = (Reference<?>) obj;
return value == reference.value;
}
@Override
public int hashCode() {
return System.identityHashCode(value);
}
}

View File

@@ -0,0 +1,68 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
public class BlockStatePlacementOptionTest {
@Test
public void sanityCheck() {
// standing at 1,0,0
// block to be placed at 0,0,0
// placing against 0,0,-1
// eye is at 1, 1.62, 0
// north or west
StringBuilder sanity = new StringBuilder();
for (PlayerVantage vantage : new PlayerVantage[]{PlayerVantage.STRICT_CENTER, PlayerVantage.LOOSE_CENTER}) {
for (Face playerFacing : new Face[]{Face.NORTH, Face.EAST, Face.WEST}) {
sanity.append(vantage).append(playerFacing);
List<Raytracer.Raytrace> traces = BlockStatePlacementOption.get(Face.NORTH, Half.BOTTOM, Optional.of(playerFacing), Optional.empty()).computeTraceOptions(new PlaceAgainstData(Face.SOUTH, Half.EITHER, false), 1, 0, 0, vantage, 4);
sanity.append(traces.size());
sanity.append(" ");
if (!traces.isEmpty()) {
for (double d : new double[]{traces.get(0).playerEye.x, traces.get(0).playerEye.z}) {
double base = d > 1 ? 1.5 : 0.5;
boolean a = d == base - BlockStatePlacementOption.LOOSE_CENTER_DISTANCE;
boolean b = d == base;
boolean c = d == base + BlockStatePlacementOption.LOOSE_CENTER_DISTANCE;
if (!a && !b && !c) {
throw new IllegalStateException("Wrong " + d);
}
sanity.append(a).append(" ").append(b).append(" ").append(c).append(" ");
}
}
sanity.append(traces.stream().mapToDouble(Raytracer.Raytrace::centerDistApprox).distinct().count());
sanity.append(";");
}
}
String res = sanity.toString();
String should = "STRICT_CENTERNORTH0 0;STRICT_CENTEREAST0 0;STRICT_CENTERWEST3 false true false false true false 1;LOOSE_CENTERNORTH2 true false false false true false 1;LOOSE_CENTEREAST0 0;LOOSE_CENTERWEST13 false true false false true false 2;";
if (!res.equals(should)) {
System.out.println(res);
System.out.println(should);
throw new IllegalStateException(res);
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import org.junit.Test;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class DependencyGraphScaffoldingOverlayTest {
@Test
public void testLarge() throws InterruptedException {
Random RAND = new Random(5021);
long aaa = System.currentTimeMillis();
BlockStateCachedData[][][] test = new BlockStateCachedData[64][64][64];
int numRealBlocks = 0;
for (int x = 0; x < test.length; x++) {
for (int y = 0; y < test[0].length; y++) {
for (int z = 0; z < test[0][0].length; z++) {
if (RAND.nextInt(10) < 2) {
test[x][y][z] = FakeStates.probablyCanBePlaced(RAND);
numRealBlocks++;
} else {
test[x][y][z] = FakeStates.AIR;
}
}
}
}
PackedBlockStateCuboid states = new PackedBlockStateCuboid(test);
PlaceOrderDependencyGraph graph = new PlaceOrderDependencyGraph(states);
long aa = System.currentTimeMillis();
DependencyGraphScaffoldingOverlay scaffolding = new DependencyGraphScaffoldingOverlay(graph);
long a = System.currentTimeMillis();
StringBuilder report = new StringBuilder(scaffolding.getCollapsedGraph().getComponents().size() + "\n");
int goal = numRealBlocks + 200000;
while (numRealBlocks < goal) {
int x = RAND.nextInt(((CuboidBounds) scaffolding.bounds()).sizeX);
int y = RAND.nextInt(((CuboidBounds) scaffolding.bounds()).sizeY);
int z = RAND.nextInt(((CuboidBounds) scaffolding.bounds()).sizeZ);
long pos = BetterBlockPos.toLong(x, y, z);
if (scaffolding.air(pos)) {
scaffolding.enable(pos);
numRealBlocks++;
if (numRealBlocks % 10000 == 0) {
report.append(numRealBlocks).append(' ').append(scaffolding.getCollapsedGraph().getComponents().size()).append('\n');
scaffolding.recheckEntireCollapsedGraph();
}
}
}
assertEquals("38832\n" +
"60000 41471\n" +
"70000 43558\n" +
"80000 44176\n" +
"90000 43575\n" +
"100000 41808\n" +
"110000 39108\n" +
"120000 35351\n" +
"130000 30807\n" +
"140000 25772\n" +
"150000 20387\n" +
"160000 15149\n" +
"170000 10482\n" +
"180000 7067\n" +
"190000 4767\n" +
"200000 3155\n" +
"210000 2011\n" +
"220000 1281\n" +
"230000 755\n" +
"240000 439\n" +
"250000 270\n", report.toString());
System.out.println("Done " + (System.currentTimeMillis() - a) + "ms after " + (a - aa) + "ms after " + (aa - aaa) + "ms");
assertEquals(238, scaffolding.getCollapsedGraph().getComponents().size());
}
}

View File

@@ -0,0 +1,48 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class FaceTest {
@Test
public void testOpts() {
for (int i = 0; i < Face.OPTS.size(); i++) {
assertEquals(i, Face.OPTS.indexOf(Face.OPTS.get(i)));
assertEquals(i, (int) Face.OPTS.get(i).map(face -> face.index).orElse(Face.NUM_FACES));
}
}
@Test
public void ensureValid() {
for (Face face : Face.VALUES) {
assertEquals(face.x, face.toMC().getXOffset());
assertEquals(face.y, face.toMC().getYOffset());
assertEquals(face.z, face.toMC().getZOffset());
}
assertEquals(Face.UP.offset, BetterBlockPos.toLong(0, 1, 0));
assertEquals(Face.DOWN.offset, BetterBlockPos.toLong(0, -1, 0));
assertEquals(Face.NORTH.offset, BetterBlockPos.toLong(0, 0, -1));
assertEquals(Face.SOUTH.offset, BetterBlockPos.toLong(0, 0, 1));
assertEquals(Face.EAST.offset, BetterBlockPos.toLong(1, 0, 0));
assertEquals(Face.WEST.offset, BetterBlockPos.toLong(-1, 0, 0));
}
}

View File

@@ -0,0 +1,126 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class NavigableSurfaceBlipTest {
@Test
public void testBasicSolo() {
CountingSurface surface = new CountingSurface(10, 10, 10);
for (int i = 0; i <= Blip.FULL_BLOCK; i++) {
surface.setBlock(0, 0, 0, FakeStates.BY_HEIGHT[i]);
assertEquals(i != 0 && i != Blip.FULL_BLOCK, surface.surfaceSize(new BetterBlockPos(0, 0, 0)).isPresent());
}
}
@Test
public void testBasicConnected() {
CountingSurface surface = new CountingSurface(10, 10, 10);
surface.setBlock(1, 0, 0, FakeStates.SOLID);
for (int i = 0; i <= Blip.FULL_BLOCK; i++) {
surface.setBlock(0, 0, 0, FakeStates.BY_HEIGHT[i]);
assertEquals(i == 0 ? 1 : 2, surface.requireSurfaceSize(1, 1, 0));
}
for (int i = 0; i <= Blip.FULL_BLOCK; i++) {
surface.setBlock(0, 1, 0, FakeStates.BY_HEIGHT[i]);
assertEquals(2, surface.requireSurfaceSize(1, 1, 0));
}
for (int i = 0; i <= Blip.FULL_BLOCK; i++) {
surface.setBlock(0, 2, 0, FakeStates.BY_HEIGHT[i]);
assertEquals(i <= Blip.JUMP - Blip.FULL_BLOCK ? 2 : 1, surface.requireSurfaceSize(1, 1, 0));
}
}
@Test
public void testStaircaseWithoutBlips() {
CountingSurface surface = new CountingSurface(10, 10, 10);
surface.setBlock(0, 0, 1, FakeStates.SOLID);
int startY = 0;
for (int x = 0; x < 5; x++) {
// increase height of the (x,0) column as much as possible
int y = startY;
while (true) {
surface.setBlock(x, y, 0, FakeStates.SOLID);
if (!surface.connected(new BetterBlockPos(0, 1, 1), new BetterBlockPos(x, y + 1, 0))) {
surface.setBlock(x, y, 0, FakeStates.AIR);
y--;
break;
}
y++;
}
startY = y;
assertEquals(x + 1, y); // only +1 because blocks start at zero, so (0, 1, 0) is already two blocks high, since (0, 0, 1) is the one block high starting point
}
}
private static void fillColumnToHeight(CountingSurface surface, int x, int z, int yHeightBlips) {
for (int i = 0; i < yHeightBlips / Blip.FULL_BLOCK; i++) {
surface.setBlock(x, i, z, FakeStates.SOLID);
}
surface.setBlock(x, yHeightBlips / Blip.FULL_BLOCK, z, FakeStates.BY_HEIGHT[yHeightBlips % Blip.FULL_BLOCK]);
}
@Test
public void testStaircaseWithFullBlockBlips() {
CountingSurface surface = new CountingSurface(10, 10, 10);
surface.setBlock(0, 0, 1, FakeStates.SOLID);
int step = Blip.FULL_BLOCK;
int startY = step;
for (int x = 0; x < 5; x++) {
// increase height of the (x,0) column as much as possible
int y = startY;
while (true) {
fillColumnToHeight(surface, x, 0, y);
if (!surface.connected(new BetterBlockPos(0, 1, 1), new BetterBlockPos(x, y / Blip.FULL_BLOCK, 0))) {
fillColumnToHeight(surface, x, 0, y - step);
y -= step;
break;
}
y += step;
}
startY = y;
assertEquals((x + 2) * Blip.FULL_BLOCK, y); // +2 because we start at 16 blips due to (0,0,1) being full, so (0,1,0) being full on top of (0,0,0) being full means x=0 should be 32 blips high
}
}
@Test
public void testStaircaseBlips() {
CountingSurface surface = new CountingSurface(10, 10, 10);
surface.setBlock(0, 0, 1, FakeStates.SOLID);
int startY = 1;
for (int x = 0; x < 5; x++) {
// increase height of the (x,0) column as much as possible
int y = startY;
while (true) {
fillColumnToHeight(surface, x, 0, y);
if (!surface.connected(new BetterBlockPos(0, 1, 1), new BetterBlockPos(x, y / Blip.FULL_BLOCK, 0))) {
fillColumnToHeight(surface, x, 0, y - 1);
y--;
break;
}
y++;
}
startY = y;
assertEquals((x + 1) * Blip.JUMP + Blip.FULL_BLOCK, y); // we start on a full block at (0,0,1) then everything after that (inclusive of x=0, therefore the +1) adds one more jump
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,175 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
import static org.junit.Assert.*;
public class PlayerPhysicsTest {
@Test
public void testAssumptions() {
if (Blip.PLAYER_HEIGHT_SLIGHT_OVERESTIMATE >= Blip.TWO_BLOCKS || Blip.PLAYER_HEIGHT_SLIGHT_OVERESTIMATE + Blip.HALF_BLOCK <= Blip.TWO_BLOCKS) {
throw new IllegalStateException("Assumptions made in playerTravelCollides");
}
if (Blip.TALLEST_BLOCK - Blip.FULL_BLOCK + Blip.JUMP - Blip.TWO_BLOCKS >= 0) {
throw new IllegalStateException("Assumption made in bidirectionalPlayerTravel");
}
int maxFeet = Blip.FULL_BLOCK - 1; // 15
int couldJumpUpTo = maxFeet + Blip.JUMP; // 35
int maxWithinAB = couldJumpUpTo - Blip.TWO_BLOCKS; // 3
if (PlayerPhysics.protrudesIntoThirdBlock(maxWithinAB)) {
// btw this is literally only 1 blip away from being true lol
throw new IllegalStateException("Oh no, if this is true then playerTravelCollides needs to check another layer above EF");
}
}
@Test
public void testBasic() {
assertEquals(16, PlayerPhysics.highestCollision(FakeStates.SCAFFOLDING, FakeStates.SCAFFOLDING));
Column normal = new Column();
normal.underneath = FakeStates.SCAFFOLDING;
normal.feet = FakeStates.AIR;
normal.head = FakeStates.AIR;
normal.above = FakeStates.AIR;
normal.aboveAbove = FakeStates.SCAFFOLDING;
normal.init();
Column up = new Column();
up.underneath = FakeStates.SCAFFOLDING;
up.feet = FakeStates.SCAFFOLDING;
up.head = FakeStates.AIR;
up.above = FakeStates.AIR;
up.aboveAbove = FakeStates.SCAFFOLDING;
up.init();
assertEquals(PlayerPhysics.Collision.VOXEL_LEVEL, PlayerPhysics.playerTravelCollides(normal, normal));
assertEquals(PlayerPhysics.Collision.JUMP_TO_VOXEL_UP, PlayerPhysics.playerTravelCollides(normal, up));
}
private static BlockStateCachedData[] makeColToHeight(int height) {
height += Blip.FULL_BLOCK * 3; // i don't truck with negative division / modulo
if (height < 0) {
throw new IllegalStateException();
}
int fullBlocks = height / Blip.FULL_BLOCK;
BlockStateCachedData[] ret = new BlockStateCachedData[7];
for (int i = 0; i < fullBlocks; i++) {
ret[i] = FakeStates.SCAFFOLDING;
}
ret[fullBlocks] = FakeStates.BY_HEIGHT[height % Blip.FULL_BLOCK];
for (int i = fullBlocks + 1; i < ret.length; i++) {
ret[i] = FakeStates.AIR;
}
return ret;
}
private static Column toCol(BlockStateCachedData[] fromCol) {
Column col = new Column();
col.underneath = fromCol[2];
col.feet = fromCol[3];
col.head = fromCol[4];
col.above = fromCol[5];
col.aboveAbove = fromCol[6];
col.init();
return col;
}
@Test
public void testMakeCol() {
for (int i = 0; i < Blip.FULL_BLOCK; i++) {
Column col = toCol(makeColToHeight(i));
assertTrue(col.standing());
assertEquals(i, (int) col.feetBlips);
}
assertFalse(toCol(makeColToHeight(-1)).standing());
assertFalse(toCol(makeColToHeight(Blip.FULL_BLOCK)).standing());
}
@Test
public void testPlayerPhysics() {
for (int startHeight = 0; startHeight < Blip.FULL_BLOCK; startHeight++) {
for (int startCeil = 5; startCeil <= 7; startCeil++) {
BlockStateCachedData[] fromCol = makeColToHeight(startHeight);
if (startCeil < fromCol.length) {
fromCol[startCeil] = FakeStates.SCAFFOLDING;
}
Column from = toCol(fromCol);
assertEquals(!from.standing(), startCeil == 5 && startHeight > 3);
if (!from.standing()) {
System.out.println("Not standing " + startHeight + " " + startCeil);
continue;
}
for (int endHeight = startHeight - 3 * Blip.FULL_BLOCK; endHeight <= startHeight + 3 * Blip.FULL_BLOCK; endHeight++) {
for (int endCeil = 5; endCeil <= 7; endCeil++) {
BlockStateCachedData[] toCol = makeColToHeight(endHeight);
if (endCeil < toCol.length) {
toCol[endCeil] = FakeStates.SCAFFOLDING;
}
Column to = toCol(toCol);
int endVoxel = (endHeight + Blip.FULL_BLOCK * 10) / Blip.FULL_BLOCK - 10; // hate negative division rounding to zero, punch negative division rounding to zero
PlayerPhysics.Collision col = PlayerPhysics.playerTravelCollides(from, to);
Integer dy = PlayerPhysics.bidirectionalPlayerTravel(from, to, toCol[1], toCol[0]);
System.out.println(startHeight + " " + startCeil + " " + endCeil + " " + (endHeight - startHeight) + " " + col + " " + endVoxel + " " + dy);
int minCeilRelative = Math.min(startCeil, endCeil) - 3;
int maxBlip = Math.max(startHeight, endHeight);
if (maxBlip + Blip.PLAYER_HEIGHT_SLIGHT_OVERESTIMATE > minCeilRelative * Blip.FULL_BLOCK) {
assertEquals(PlayerPhysics.Collision.BLOCKED, col);
continue;
}
assertEquals(col == PlayerPhysics.Collision.BLOCKED, endHeight > startHeight + Blip.JUMP);
assertEquals(col == PlayerPhysics.Collision.FALL, endVoxel < 0);
assertEquals(dy == null, endHeight > startHeight + Blip.JUMP || endHeight < startHeight - Blip.JUMP);
if (dy != null) {
assertEquals(endVoxel, (int) dy);
Integer reverse = -dy;
BlockStateCachedData[] shiftedTo = shift(toCol, reverse);
BlockStateCachedData[] shiftedFrom = shift(fromCol, reverse);
// make sure it's actually bidirectional
assertEquals(reverse, PlayerPhysics.bidirectionalPlayerTravel(toCol(shiftedTo), toCol(shiftedFrom), shiftedFrom[1], shiftedFrom[0]));
}
if (col != PlayerPhysics.Collision.BLOCKED && col != PlayerPhysics.Collision.FALL) {
assertEquals(col.voxelVerticalOffset(), endVoxel);
assertEquals(col.requiresJump(), endHeight > startHeight + Blip.HALF_BLOCK);
}
}
}
}
}
}
private static BlockStateCachedData[] shift(BlockStateCachedData[] col, int dy) {
BlockStateCachedData[] ret = new BlockStateCachedData[col.length];
for (int i = 0; i < ret.length; i++) {
int j = i - dy;
if (j >= 0 && j < col.length) {
ret[i] = col[j];
} else {
ret[i] = FakeStates.OUT_OF_BOUNDS;
}
}
return ret;
}
}

View File

@@ -0,0 +1,77 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import baritone.api.utils.BetterBlockPos;
import org.junit.Test;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class ScaffolderTest {
@Test
public void test() {
BlockStateCachedData[][][] test = new BlockStateCachedData[8][8][8];
PackedBlockStateCuboid.fillWithAir(test);
for (int x = 0; x < test.length; x++) {
for (int z = 0; z < test[0][0].length; z++) {
test[x][0][z] = FakeStates.SOLID;
test[x][1][z] = FakeStates.SOLID;
}
}
test[5][5][5] = FakeStates.SOLID;
test[5][5][6] = FakeStates.SOLID;
test[0][5][5] = FakeStates.SOLID;
Consumer<DependencyGraphScaffoldingOverlay> debug = dgso -> {
for (int y = 0; y < test[0].length; y++) {
System.out.println("Layer " + y);
for (int x = 0; x < test.length; x++) {
for (int z = 0; z < test[0][0].length; z++) {
long pos = BetterBlockPos.toLong(x, y, z);
if (dgso.real(pos)) {
System.out.print("A" + dgso.getCollapsedGraph().getComponentLocations().get(pos).deletedIntoRecursive());
} else {
System.out.print(" ");
}
}
System.out.println();
}
}
};
PackedBlockStateCuboid states = new PackedBlockStateCuboid(test);
PlaceOrderDependencyGraph graph = new PlaceOrderDependencyGraph(states);
System.out.println("N " + Face.NORTH.z);
System.out.println("S " + Face.SOUTH.z);
for (int z = 0; z < test[0][0].length; z++) {
//System.out.println(states.get(states.bounds.toIndex(0, 0, z)));
System.out.println(z + " " + graph.outgoingEdge(BetterBlockPos.toLong(0, 0, z), Face.NORTH) + " " + graph.outgoingEdge(BetterBlockPos.toLong(0, 0, z), Face.SOUTH));
}
DependencyGraphAnalyzer.prevalidate(graph);
DependencyGraphAnalyzer.prevalidateExternalToInteriorSearch(graph);
DependencyGraphScaffoldingOverlay scaffolding = new DependencyGraphScaffoldingOverlay(graph);
System.out.println("Hewwo");
scaffolding.getCollapsedGraph().getComponents().forEach((key, value) -> {
System.out.println(key);
System.out.println(value.getPositions().stream().map(BetterBlockPos::fromLong).collect(Collectors.toList()));
});
System.out.println();
debug.accept(scaffolding);
Scaffolder.Output out = Scaffolder.run(graph, DijkstraScaffolder.INSTANCE);
debug.accept(out.secretInternalForTesting());
}
}

View File

@@ -0,0 +1,51 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
import java.util.Random;
public class TarjansAlgorithmTest {
@Test
public void test() {
// the correctness test is already in there just gotta ask for it
Random RAND = new Random(5021);
for (int i = 0; i < 100; i++) {
BlockStateCachedData[][][] test = new BlockStateCachedData[20][20][20];
int density = i <= 10 ? i : i % 4; // high density is slow so only do them once
for (int x = 0; x < test.length; x++) {
for (int y = 0; y < test[0].length; y++) {
for (int z = 0; z < test[0][0].length; z++) {
if (RAND.nextInt(10) < density) {
test[x][y][z] = FakeStates.probablyCanBePlaced(RAND);
} else {
test[x][y][z] = FakeStates.AIR;
}
}
}
}
PackedBlockStateCuboid states = new PackedBlockStateCuboid(test);
PlaceOrderDependencyGraph graph = new PlaceOrderDependencyGraph(states);
DependencyGraphScaffoldingOverlay overlay = new DependencyGraphScaffoldingOverlay(graph); // this runs tarjan's twice, but the alternative ruins the abstraction layers too much :)
TarjansAlgorithm.TarjansResult result = TarjansAlgorithm.run(overlay);
TarjansAlgorithm.sanityCheckResult(overlay, result);
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
public class TestCuboidBounds {
@Test
public void testBounds() {
new CuboidBounds(10, 10, 10).sanityCheck();
}
}

View File

@@ -0,0 +1,32 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class Vec3dTest {
@Test
public void testFlatDir() {
for (Face face : Face.HORIZONTALS) {
Face flat = new Vec3d(face.x, face.y, face.z).flatDirection();
assertEquals(flat, face);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import org.junit.Test;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class WorldStateTest {
@Test
public void testZobrist() {
Random rand = new Random(5021);
for (int i = 0; i < 1000; i++) {
long pos = rand.nextLong();
long zobrist = WorldState.zobrist(pos);
long un = WorldState.unzobrist(zobrist);
assertEquals(pos, un);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/dynamic-connectivity/
*/
package baritone.builder.connectivity;
import baritone.builder.utils.com.github.btrekkie.connectivity.Augmentation;
/**
* Stores two values: a sum and a maximum. Used for testing augmentation in ConnGraph.
*/
class SumAndMax {
/**
* An Augmentation that combines two SumAndMaxes into one.
*/
public static final Augmentation AUGMENTATION = new Augmentation() {
@Override
public Object combine(Object value1, Object value2) {
SumAndMax sumAndMax1 = (SumAndMax) value1;
SumAndMax sumAndMax2 = (SumAndMax) value2;
return new SumAndMax(sumAndMax1.sum + sumAndMax2.sum, Math.max(sumAndMax1.max, sumAndMax2.max));
}
};
public final int sum;
public final int max;
public SumAndMax(int sum, int max) {
this.sum = sum;
this.max = max;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SumAndMax)) {
return false;
}
SumAndMax sumAndMax = (SumAndMax) obj;
return sum == sumAndMax.sum && max == sumAndMax.max;
}
@Override
public int hashCode() {
return 31 * sum + max;
}
}

View File

@@ -0,0 +1,51 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.arbitrary_order_collection;
import java.util.Comparator;
/**
* Provides objects ordered in an arbitrary, but consistent fashion, through the createValue() method. To determine the
* relative order of two values, use ArbitraryOrderValue.compareTo. We may only compare values on which we have not
* called "remove" that were created in the same ArbitraryOrderCollection instance. Note that despite the name,
* ArbitraryOrderCollection does not implement Collection.
*/
/* We implement an ArbitraryOrderCollection using a red-black tree. We order the nodes arbitrarily.
*/
public class ArbitraryOrderCollection {
/**
* The Comparator for ordering ArbitraryOrderNodes.
*/
private static final Comparator<ArbitraryOrderNode> NODE_COMPARATOR = new Comparator<ArbitraryOrderNode>() {
@Override
public int compare(ArbitraryOrderNode node1, ArbitraryOrderNode node2) {
return 0;
}
};
/**
* The root node of the tree.
*/
private ArbitraryOrderNode root = new ArbitraryOrderNode();
/**
* Adds and returns a new value for ordering.
*/
public ArbitraryOrderValue createValue() {
ArbitraryOrderNode node = new ArbitraryOrderNode();
root = root.insert(node, true, NODE_COMPARATOR);
return new ArbitraryOrderValue(node);
}
/**
* Removes the specified value from this collection. Assumes we obtained the value by calling createValue() on this
* instance of ArbitraryOrderCollection. After calling "remove" on a value, we may no longer compare it to other
* values.
*/
public void remove(ArbitraryOrderValue value) {
root = value.node.remove();
}
}

View File

@@ -0,0 +1,15 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.arbitrary_order_collection;
import baritone.builder.utils.com.github.btrekkie.red_black_node.RedBlackNode;
/**
* A node in an ArbitraryOrderCollection tree. See ArbitraryOrderCollection.
*/
class ArbitraryOrderNode extends RedBlackNode<ArbitraryOrderNode> {
}

View File

@@ -0,0 +1,26 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.arbitrary_order_collection;
/**
* A value in an ArbitraryOrderCollection. To determine the relative order of two values in the same collection, call
* compareTo.
*/
public class ArbitraryOrderValue implements Comparable<ArbitraryOrderValue> {
/**
* The node that establishes this value's relative position.
*/
final ArbitraryOrderNode node;
ArbitraryOrderValue(ArbitraryOrderNode node) {
this.node = node;
}
@Override
public int compareTo(ArbitraryOrderValue other) {
return node.compareTo(other.node);
}
}

View File

@@ -0,0 +1,78 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.arbitrary_order_collection.test;
import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderCollection;
import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderValue;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ArbitraryOrderCollectionTest {
/**
* Tests ArbitraryOrderCollection.
*/
@Test
public void test() {
ArbitraryOrderCollection collection = new ArbitraryOrderCollection();
List<ArbitraryOrderValue> values1 = new ArrayList<ArbitraryOrderValue>(5);
ArbitraryOrderValue value = collection.createValue();
assertEquals(0, value.compareTo(value));
values1.add(value);
for (int i = 0; i < 4; i++) {
values1.add(collection.createValue());
}
Collections.sort(values1);
List<ArbitraryOrderValue> values2 = new ArrayList<ArbitraryOrderValue>(10);
for (int i = 0; i < 10; i++) {
value = collection.createValue();
values2.add(value);
}
for (int i = 0; i < 5; i++) {
collection.remove(values2.get(2 * i));
}
assertEquals(0, values1.get(0).compareTo(values1.get(0)));
assertTrue(values1.get(0).compareTo(values1.get(1)) < 0);
assertTrue(values1.get(1).compareTo(values1.get(0)) > 0);
assertTrue(values1.get(4).compareTo(values1.get(2)) > 0);
assertTrue(values1.get(0).compareTo(values1.get(4)) < 0);
collection = new ArbitraryOrderCollection();
values1 = new ArrayList<ArbitraryOrderValue>(1000);
for (int i = 0; i < 1000; i++) {
value = collection.createValue();
values1.add(value);
}
for (int i = 0; i < 500; i++) {
collection.remove(values1.get(2 * i));
}
values2 = new ArrayList<ArbitraryOrderValue>(500);
for (int i = 0; i < 500; i++) {
values2.add(values1.get(2 * i + 1));
}
for (int i = 0; i < 500; i++) {
values2.get(0).compareTo(values2.get(i));
}
Collections.sort(values2);
for (int i = 0; i < 500; i++) {
collection.createValue();
}
for (int i = 0; i < 499; i++) {
assertTrue(values2.get(i).compareTo(values2.get(i + 1)) < 0);
assertTrue(values2.get(i + 1).compareTo(values2.get(i)) > 0);
}
for (int i = 1; i < 500; i++) {
assertEquals(0, values2.get(i).compareTo(values2.get(i)));
assertTrue(values2.get(0).compareTo(values2.get(i)) < 0);
assertTrue(values2.get(i).compareTo(values2.get(0)) > 0);
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.interval_tree;
/**
* An interval tree data structure, which supports adding or removing an interval and finding an arbitrary interval that
* contains a specified value.
*/
/* The interval tree is ordered in ascending order of the start an interval, with ties broken by the end of the
* interval. Each node is augmented with the maximum ending value of an interval in the subtree rooted at the node.
*/
public class IntervalTree {
/**
* The root node of the tree.
*/
private IntervalTreeNode root = IntervalTreeNode.LEAF;
/**
* Adds the specified interval to this.
*/
public void addInterval(IntervalTreeInterval interval) {
root = root.insert(new IntervalTreeNode(interval), true, null);
}
/**
* Removes the specified interval from this, if it is present.
*
* @param interval The interval.
* @return Whether the interval was present.
*/
public boolean removeInterval(IntervalTreeInterval interval) {
IntervalTreeNode node = root;
while (!node.isLeaf()) {
if (interval.start < node.interval.start) {
node = node.left;
} else if (interval.start > node.interval.start) {
node = node.right;
} else if (interval.end < node.interval.end) {
node = node.left;
} else if (interval.end > node.interval.end) {
node = node.right;
} else {
root = node.remove();
return true;
}
}
return false;
}
/**
* Returns an aribtrary IntervalTreeInterval in this that contains the specified value. Returns null if there is no
* such interval.
*/
public IntervalTreeInterval findInterval(double value) {
IntervalTreeNode node = root;
while (!node.isLeaf()) {
if (value >= node.interval.start && value <= node.interval.end) {
return node.interval;
} else if (value <= node.left.maxEnd) {
node = node.left;
} else {
node = node.right;
}
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.interval_tree;
/**
* An inclusive range of values [start, end]. Two intervals are equal if they have the same starting and ending values.
*/
public class IntervalTreeInterval {
/**
* The smallest value in the range.
*/
public final double start;
/**
* The largest value in the range.
*/
public final double end;
public IntervalTreeInterval(double start, double end) {
if (start > end) {
throw new IllegalArgumentException("The end of the range must be at most the start");
}
this.start = start;
this.end = end;
}
public boolean equals(Object obj) {
if (!(obj instanceof IntervalTreeInterval)) {
return false;
}
IntervalTreeInterval interval = (IntervalTreeInterval) obj;
return start == interval.start && end == interval.end;
}
}

View File

@@ -0,0 +1,81 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.interval_tree;
import baritone.builder.utils.com.github.btrekkie.red_black_node.RedBlackNode;
/**
* A node in an IntervalTree. See the comments for the implementation of IntervalTree. Its compareTo method orders
* nodes as suggested in the comments for the implementation of IntervalTree.
*/
class IntervalTreeNode extends RedBlackNode<IntervalTreeNode> {
/**
* The dummy leaf node.
*/
public static final IntervalTreeNode LEAF = new IntervalTreeNode();
/**
* The interval stored in this node.
*/
public IntervalTreeInterval interval;
/**
* The maximum ending value of an interval in the subtree rooted at this node.
*/
public double maxEnd;
public IntervalTreeNode(IntervalTreeInterval interval) {
this.interval = interval;
maxEnd = interval.end;
}
/**
* Constructs a new dummy leaf node.
*/
private IntervalTreeNode() {
interval = null;
maxEnd = Double.NEGATIVE_INFINITY;
}
@Override
public boolean augment() {
double newMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd));
if (newMaxEnd == maxEnd) {
return false;
} else {
maxEnd = newMaxEnd;
return true;
}
}
@Override
public void assertNodeIsValid() {
double expectedMaxEnd;
if (isLeaf()) {
expectedMaxEnd = Double.NEGATIVE_INFINITY;
} else {
expectedMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd));
}
if (maxEnd != expectedMaxEnd) {
throw new RuntimeException("The node's maxEnd does not match that of the children");
}
}
@Override
public int compareTo(IntervalTreeNode other) {
if (interval.start != interval.end) {
return Double.compare(interval.start, other.interval.start);
} else {
return Double.compare(interval.end, other.interval.end);
}
}
@Override
public void assertSubtreeIsValid() {
super.assertSubtreeIsValid();
assertOrderIsValid(null);
}
}

View File

@@ -0,0 +1,51 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.interval_tree.test;
import com.github.btrekkie.interval_tree.IntervalTree;
import com.github.btrekkie.interval_tree.IntervalTreeInterval;
import org.junit.Test;
import static org.junit.Assert.*;
public class IntervalTreeTest {
/**
* Tests IntervalTree.
*/
@Test
public void test() {
IntervalTree tree = new IntervalTree();
assertNull(tree.findInterval(0.5));
assertNull(tree.findInterval(-1));
tree.addInterval(new IntervalTreeInterval(5, 7));
tree.addInterval(new IntervalTreeInterval(42, 48));
tree.addInterval(new IntervalTreeInterval(-1, 2));
tree.addInterval(new IntervalTreeInterval(6, 12));
tree.addInterval(new IntervalTreeInterval(21, 23));
assertTrue(tree.removeInterval(new IntervalTreeInterval(-1, 2)));
assertFalse(tree.removeInterval(new IntervalTreeInterval(-1, 2)));
tree.addInterval(new IntervalTreeInterval(-6, -2));
assertEquals(new IntervalTreeInterval(6, 12), tree.findInterval(8));
assertNull(tree.findInterval(0));
assertEquals(new IntervalTreeInterval(21, 23), tree.findInterval(21));
assertEquals(new IntervalTreeInterval(42, 48), tree.findInterval(48));
IntervalTreeInterval interval = tree.findInterval(6.5);
assertTrue(new IntervalTreeInterval(5, 7).equals(interval) || new IntervalTreeInterval(6, 12).equals(interval));
tree = new IntervalTree();
for (int i = 0; i < 500; i++) {
tree.addInterval(new IntervalTreeInterval(2 * i, 2 * i + 1));
}
for (int i = 0; i < 250; i++) {
tree.removeInterval(new IntervalTreeInterval(4 * i + 2, 4 * i + 3));
}
assertNull(tree.findInterval(123.5));
assertEquals(new IntervalTreeInterval(124, 125), tree.findInterval(124.5));
assertEquals(new IntervalTreeInterval(776, 777), tree.findInterval(776));
assertEquals(new IntervalTreeInterval(0, 1), tree.findInterval(0.5));
assertEquals(new IntervalTreeInterval(996, 997), tree.findInterval(997));
}
}

View File

@@ -0,0 +1,179 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.red_black_node.test;
import org.junit.Test;
import java.util.Comparator;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests RedBlackNode. Most of the testing for RedBlackNode takes place in TreeListTest, IntervalTreeTest,
* SubArrayMinTest, and ArbitraryOrderCollectionTest, which test realistic use cases of RedBlackNode. TreeListTest
* tests most of the RedBlackNode methods, while IntervalTreeTest tests the "insert" method, SubArrayMinTest tests
* "lca", ArbitraryOrderCollectionTest tests compareTo, and RedBlackNodeTest tests assertSubtreeIsValid() and
* assertOrderIsValid.
*/
public class RedBlackNodeTest {
/**
* Returns whether the subtree rooted at the specified node is valid, as in TestRedBlackNode.assertSubtreeIsValid().
*/
private boolean isSubtreeValid(TestRedBlackNode node) {
try {
node.assertSubtreeIsValid();
return true;
} catch (RuntimeException exception) {
return false;
}
}
/**
* Returns whether the nodes in the subtree rooted at the specified node are ordered correctly, as in
* TestRedBlackNode.assertOrderIsValid.
*
* @param comparator A comparator indicating how the nodes should be ordered. If this is null, we use the nodes'
* natural ordering, as in TestRedBlackNode.compare.
*/
private boolean isOrderValid(TestRedBlackNode node, Comparator<TestRedBlackNode> comparator) {
try {
node.assertOrderIsValid(null);
return true;
} catch (RuntimeException exception) {
return false;
}
}
/**
* Tests RedBlackNode.assertSubtreeIsValid() and RedBlackNode.assertOrderIsValid.
*/
@Test
public void testAssertIsValid() {
// Create a perfectly balanced tree of height 3
TestRedBlackNode node0 = new TestRedBlackNode(0);
TestRedBlackNode node1 = new TestRedBlackNode(1);
TestRedBlackNode node2 = new TestRedBlackNode(2);
TestRedBlackNode node3 = new TestRedBlackNode(3);
TestRedBlackNode node4 = new TestRedBlackNode(4);
TestRedBlackNode node5 = new TestRedBlackNode(5);
TestRedBlackNode node6 = new TestRedBlackNode(6);
node0.parent = node1;
node0.left = TestRedBlackNode.LEAF;
node0.right = TestRedBlackNode.LEAF;
node1.parent = node3;
node1.left = node0;
node1.right = node2;
node1.isRed = true;
node2.parent = node1;
node2.left = TestRedBlackNode.LEAF;
node2.right = TestRedBlackNode.LEAF;
node3.left = node1;
node3.right = node5;
node4.parent = node5;
node4.left = TestRedBlackNode.LEAF;
node4.right = TestRedBlackNode.LEAF;
node5.parent = node3;
node5.left = node4;
node5.right = node6;
node5.isRed = true;
node6.parent = node5;
node6.left = TestRedBlackNode.LEAF;
node6.right = TestRedBlackNode.LEAF;
node3.left = node3;
node3.right = node3;
node3.parent = node3;
assertFalse(isSubtreeValid(node3));
node3.left = node1;
node3.right = node5;
node3.parent = null;
node0.parent = node3;
assertFalse(isSubtreeValid(node3));
node0.parent = node1;
node1.right = node0;
assertFalse(isSubtreeValid(node3));
node1.right = node2;
node5.isRed = false;
assertFalse(isSubtreeValid(node3));
assertTrue(isSubtreeValid(node5));
node5.isRed = true;
node3.isRed = true;
assertFalse(isSubtreeValid(node3));
assertTrue(isSubtreeValid(node5));
node3.isRed = false;
node0.isRed = true;
node2.isRed = true;
node4.isRed = true;
node6.isRed = true;
assertFalse(isSubtreeValid(node3));
node0.isRed = false;
node2.isRed = false;
node4.isRed = false;
node6.isRed = false;
TestRedBlackNode.LEAF.isRed = true;
assertFalse(isSubtreeValid(node3));
TestRedBlackNode.LEAF.isRed = false;
TestRedBlackNode.LEAF.isValid = false;
assertFalse(isSubtreeValid(node3));
assertFalse(isSubtreeValid(TestRedBlackNode.LEAF));
TestRedBlackNode.LEAF.isValid = true;
node1.isValid = false;
assertFalse(isSubtreeValid(node3));
node1.isValid = true;
node3.value = 2;
node2.value = 3;
assertFalse(isOrderValid(node3, null));
assertFalse(
isOrderValid(node3, new Comparator<TestRedBlackNode>() {
@Override
public int compare(TestRedBlackNode node1, TestRedBlackNode node2) {
return node1.value - node2.value;
}
}));
node3.value = 3;
node2.value = 2;
node2.value = 4;
node4.value = 2;
assertFalse(isOrderValid(node3, null));
node2.value = 2;
node4.value = 4;
node0.value = 1;
node1.value = 0;
assertFalse(isOrderValid(node3, null));
node0.value = 0;
node1.value = 1;
// Do all of the assertions for which the tree is supposed to be valid at the end, to make sure we didn't make a
// mistake undoing any of the modifications
assertTrue(isSubtreeValid(node3));
assertTrue(isSubtreeValid(node1));
assertTrue(isSubtreeValid(node0));
assertTrue(isSubtreeValid(TestRedBlackNode.LEAF));
assertTrue(isOrderValid(node3, null));
assertTrue(isOrderValid(node1, null));
assertTrue(isOrderValid(node0, null));
assertTrue(isOrderValid(TestRedBlackNode.LEAF, null));
assertTrue(
isOrderValid(node3, new Comparator<TestRedBlackNode>() {
@Override
public int compare(TestRedBlackNode node1, TestRedBlackNode node2) {
return node1.value - node2.value;
}
}));
}
}

View File

@@ -0,0 +1,51 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.red_black_node.test;
import baritone.builder.utils.com.github.btrekkie.red_black_node.RedBlackNode;
/**
* A RedBlackNode for RedBlackNodeTest.
*/
class TestRedBlackNode extends RedBlackNode<TestRedBlackNode> {
/**
* The dummy leaf node.
*/
public static final TestRedBlackNode LEAF = new TestRedBlackNode();
/**
* The value stored in this node. "value" is unspecified if this is a leaf node.
*/
public int value;
/**
* Whether this node is considered valid, as in assertNodeIsValid().
*/
public boolean isValid = true;
public TestRedBlackNode(int value) {
this.value = value;
}
/**
* Constructs a new dummy leaf node.
*/
private TestRedBlackNode() {
}
@Override
public void assertNodeIsValid() {
if (!isValid) {
throw new RuntimeException("isValid is false");
}
}
@Override
public int compareTo(TestRedBlackNode other) {
return value - other.value;
}
}

View File

@@ -0,0 +1,104 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.sub_array_min;
/**
* A list of integers. SubArrayMin provides the ability to quickly determine the minimum value in a given sublist.
*/
/* We implement SubArrayMin using a red-black tree augmented by subtree size and minimum value. Using the subtree size
* augmentation, we can find the node at a given index.
*/
public class SubArrayMin {
/**
* The root node.
*/
private SubArrayMinNode root = SubArrayMinNode.LEAF;
/**
* Appends the specified value to the end of the list.
*/
public void add(int value) {
SubArrayMinNode newNode = new SubArrayMinNode(value);
newNode.left = SubArrayMinNode.LEAF;
newNode.right = SubArrayMinNode.LEAF;
if (root.isLeaf()) {
root = newNode;
newNode.augment();
} else {
SubArrayMinNode node = root.max();
node.right = newNode;
newNode.parent = node;
newNode.isRed = true;
root = newNode.fixInsertion();
}
}
/**
* Returns the node for the element with the specified index. Assumes "index" is in the range [0, root.size).
*/
private SubArrayMinNode getNode(int index) {
if (index < 0 || index >= root.size) {
throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + ")");
}
int rank = index;
SubArrayMinNode node = root;
while (rank != node.left.size) {
if (rank < node.left.size) {
node = node.left;
} else {
rank -= node.left.size + 1;
node = node.right;
}
}
return node;
}
/**
* Returns the minimum value in the subarray starting at index startIndex and ending at index endIndex - 1,
* inclusive. Assumes startIndex < endIndex, and assumes this contains indices startIndex and endIndex - 1.
*/
public int min(int startIndex, int endIndex) {
if (startIndex >= endIndex) {
throw new IllegalArgumentException("The start index must be less than the end index");
}
SubArrayMinNode start = getNode(startIndex);
SubArrayMinNode end = getNode(endIndex - 1);
SubArrayMinNode lca = start.lca(end);
int min = Math.min(lca.value, Math.min(start.value, end.value));
if (start != lca) {
if (start.right.min < min) {
min = start.right.min;
}
for (SubArrayMinNode node = start; node.parent != lca; node = node.parent) {
if (node.parent.left == node) {
if (node.parent.value < min) {
min = node.parent.value;
}
if (node.parent.right.min < min) {
min = node.parent.right.min;
}
}
}
}
if (end != lca) {
if (end.left.min < min) {
min = end.left.min;
}
for (SubArrayMinNode node = end; node.parent != lca; node = node.parent) {
if (node.parent.right == node) {
if (node.parent.value < min) {
min = node.parent.value;
}
if (node.parent.left.min < min) {
min = node.parent.left.min;
}
}
}
}
return min;
}
}

View File

@@ -0,0 +1,71 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.sub_array_min;
import baritone.builder.utils.com.github.btrekkie.red_black_node.RedBlackNode;
/**
* A node in a SubArrayMin object. See the comments for the implementation of that class.
*/
class SubArrayMinNode extends RedBlackNode<SubArrayMinNode> {
/**
* The dummy leaf node.
*/
public static final SubArrayMinNode LEAF = new SubArrayMinNode();
/**
* The element stored in the node. The value is unspecified if this is a leaf node.
*/
public final int value;
/**
* The number of elements in the subtree rooted at this node.
*/
public int size;
/**
* The minimum element in the subtree rooted at this node. This is Integer.MAX_VALUE if this is a leaf node.
*/
public int min;
public SubArrayMinNode(int value) {
this.value = value;
}
private SubArrayMinNode() {
value = 0;
min = Integer.MAX_VALUE;
}
@Override
public boolean augment() {
int newSize = left.size + right.size + 1;
int newMin = Math.min(value, Math.min(left.min, right.min));
if (newSize == size && newMin == min) {
return false;
} else {
size = newSize;
min = newMin;
return true;
}
}
@Override
public void assertNodeIsValid() {
int expectedSize;
int expectedMin;
if (isLeaf()) {
expectedSize = 0;
expectedMin = Integer.MAX_VALUE;
} else {
expectedSize = left.size + right.size + 1;
expectedMin = Math.min(value, Math.min(left.min, right.min));
}
if (size != expectedSize || min != expectedMin) {
throw new RuntimeException("The node's size or minimum value does not match that of the children");
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* This file was originally written by btrekkie under the MIT license, which is compatible with the LGPL license for this usage within Baritone
* https://github.com/btrekkie/RedBlackNode/
*/
package com.github.btrekkie.sub_array_min.test;
import com.github.btrekkie.sub_array_min.SubArrayMin;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class SubArrayMinTest {
/**
* Tests SubArrayMin.
*/
@Test
public void test() {
SubArrayMin sam = new SubArrayMin();
sam.add(12);
sam.add(42);
sam.add(-3);
sam.add(16);
sam.add(5);
sam.add(8);
sam.add(4);
assertEquals(-3, sam.min(0, 7));
assertEquals(12, sam.min(0, 2));
assertEquals(-3, sam.min(2, 4));
assertEquals(12, sam.min(0, 1));
assertEquals(5, sam.min(3, 6));
assertEquals(4, sam.min(4, 7));
sam = new SubArrayMin();
for (int i = 0; i < 1000; i++) {
sam.add(-Integer.bitCount(i));
}
assertEquals(0, sam.min(0, 1));
assertEquals(-4, sam.min(0, 30));
assertEquals(-9, sam.min(0, 1000));
assertEquals(-9, sam.min(123, 777));
assertEquals(-8, sam.min(777, 888));
assertEquals(-6, sam.min(777, 788));
assertEquals(-9, sam.min(900, 1000));
}
}