diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index b264a3fd..02e50474 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1661,6 +1661,9 @@ Fix pathfinding voxel hashing calculating (05/04/2025) Path to nearest valid voxel instead of a non-walkable voxel Blocks factor into voxel pathfinding +Debugging pathfinding code +New AI behaviors + - Will explore for resources if local ones aren't available @@ -1728,6 +1731,8 @@ Rearchitecting - Main render is a ui element (that we can have multiple of) - Shader injecting consts from the engine itself (ie max lights is dynamically injected, that way never have to worry about .glsl and .java not aligning) - Cache busting for particle atlas cache + - Convert behavior tree nodes to use static evaluation methods instead of constructing objects + - This will make stepping through the logic for a tree SIGNIFICANTLY more legible with debugger Code cleanup - Rename "BehaviorTree" to be "Component" (what it actually is) diff --git a/src/main/java/electrosphere/server/ai/nodes/plan/PathfindingNode.java b/src/main/java/electrosphere/server/ai/nodes/plan/PathfindingNode.java index 107ecef1..130c4ea4 100644 --- a/src/main/java/electrosphere/server/ai/nodes/plan/PathfindingNode.java +++ b/src/main/java/electrosphere/server/ai/nodes/plan/PathfindingNode.java @@ -22,9 +22,14 @@ public class PathfindingNode implements AITreeNode { /** - * The value used to check if the entity is close to a pathing point + * The value used to check if the entity is close to a pathing point horizontally */ - public static final double CLOSENESS_CHECK_BOUND = 0.3f; + public static final double CLOSENESS_CHECK_BOUND_HORIZONTAL = 0.3f; + + /** + * The value used to check if the entity is close to a pathing point vertically + */ + public static final double CLOSENESS_CHECK_BOUND_VERTICAL = 0.5f; /** * The blackboard key to lookup the target entity under @@ -43,7 +48,32 @@ public class PathfindingNode implements AITreeNode { @Override - public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + //make sure that the solved pathfinding data is for the point we want + if(PathfindingNode.hasPathfindingData(blackboard)){ + PathingProgressiveData pathingProgressiveData = PathfindingNode.getPathfindingData(blackboard); + Vector3d actualPoint = pathingProgressiveData.getPoints().get(pathingProgressiveData.getPoints().size() - 1); + Object targetRaw = blackboard.get(this.targetEntityKey); + Vector3d targetPos = null; + if(targetRaw == null){ + throw new Error("Target undefined!"); + } + if(targetRaw instanceof Vector3d){ + targetPos = (Vector3d)targetRaw; + } else if(targetRaw instanceof Entity){ + targetPos = EntityUtils.getPosition((Entity)targetRaw); + } else if(targetRaw instanceof Structure){ + targetPos = ((Structure)targetRaw).getPos(); + } else { + throw new Error("Unsupported target type " + targetRaw); + } + if(actualPoint.distance(targetPos) > CLOSENESS_CHECK_BOUND_HORIZONTAL){ + PathfindingNode.clearPathfindingData(blackboard); + PathfindingNode.clearPathfindingPoint(blackboard); + } + } + + //create a path if we don't already have one if(!PathfindingNode.hasPathfindingData(blackboard)){ Object targetRaw = blackboard.get(this.targetEntityKey); Vector3d targetPos = null; @@ -82,16 +112,41 @@ public class PathfindingNode implements AITreeNode { if(pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size()){ currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint()); } - double dist = currentPathPos.distance(entityPos); + double vertDist = Math.abs(currentPathPos.y - entityPos.y); + double horizontalDist = Math.sqrt((currentPathPos.x - entityPos.x) * (currentPathPos.x - entityPos.x) + (currentPathPos.z - entityPos.z) * (currentPathPos.z - entityPos.z)); while( currentPathPos != null && - dist < CLOSENESS_CHECK_BOUND && + vertDist < CLOSENESS_CHECK_BOUND_VERTICAL && + horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL && pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size() - 1 ){ pathingProgressiveData.setCurrentPoint(pathingProgressiveData.getCurrentPoint() + 1); currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint()); - dist = currentPathPos.distance(entityPos); } + + //if we're close enough to the final pathing point, always path to actual final point + if( + vertDist < CLOSENESS_CHECK_BOUND_VERTICAL && + horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL && + pathingProgressiveData.getCurrentPoint() == pathingProgressiveData.getPoints().size() - 1 + ){ + Object targetRaw = blackboard.get(this.targetEntityKey); + Vector3d targetPos = null; + if(targetRaw == null){ + throw new Error("Target undefined!"); + } + if(targetRaw instanceof Vector3d){ + targetPos = (Vector3d)targetRaw; + } else if(targetRaw instanceof Entity){ + targetPos = EntityUtils.getPosition((Entity)targetRaw); + } else if(targetRaw instanceof Structure){ + targetPos = ((Structure)targetRaw).getPos(); + } else { + throw new Error("Unsupported target type " + targetRaw); + } + currentPathPos = targetPos; + } + PathfindingNode.setPathfindingPoint(blackboard, currentPathPos); return AITreeNodeResult.SUCCESS; diff --git a/src/main/java/electrosphere/server/ai/nodes/plan/TargetExploreNode.java b/src/main/java/electrosphere/server/ai/nodes/plan/TargetExploreNode.java new file mode 100644 index 00000000..1846ff0c --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/plan/TargetExploreNode.java @@ -0,0 +1,61 @@ +package electrosphere.server.ai.nodes.plan; + +import java.util.Random; + +import org.joml.Vector3d; +import org.joml.Vector3i; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityUtils; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.datacell.Realm; +import electrosphere.server.datacell.ServerWorldData; + +/** + * Generates a point to explore towards + */ +public class TargetExploreNode implements AITreeNode { + + /** + * The key to store the point under + */ + String targetKey; + + /** + * Distance to travel in whatever direction + */ + static final double OFFSET_DIST = 50; + + /** + * constructor + * @param targetKey The key to store the point under + */ + public TargetExploreNode(String targetKey){ + this.targetKey = targetKey; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + Vector3d targetPos = null; + + if(!blackboard.has(targetKey)){ + Vector3d entPos = new Vector3d(EntityUtils.getPosition(entity)); + Random rand = new Random(); + Realm realm = Globals.realmManager.getEntityRealm(entity); + Vector3d offsetVec = new Vector3d(rand.nextDouble(),0,rand.nextDouble()).normalize().mul(OFFSET_DIST); + targetPos = entPos.add(offsetVec); + //solve for height via world data + Vector3i voxelPos = ServerWorldData.convertRealToVoxelSpace(targetPos); + Vector3i chunkPos = ServerWorldData.convertRealToChunkSpace(targetPos); + double height = realm.getServerWorldData().getServerTerrainManager().getElevation(chunkPos.x, chunkPos.z, voxelPos.x, voxelPos.z); + targetPos.y = height; + + //store + blackboard.put(targetKey, targetPos); + } + return AITreeNodeResult.SUCCESS; + } + +} diff --git a/src/main/java/electrosphere/server/ai/trees/creature/MoveToTree.java b/src/main/java/electrosphere/server/ai/trees/creature/MoveToTree.java index 665e2e05..bc3abee1 100644 --- a/src/main/java/electrosphere/server/ai/trees/creature/MoveToTree.java +++ b/src/main/java/electrosphere/server/ai/trees/creature/MoveToTree.java @@ -24,6 +24,36 @@ public class MoveToTree { */ public static final String TREE_NAME = "MoveTo"; + /** + * Default distance to be within + */ + static final double DEFAULT_DIST = 0.5f; + + /** + * Creates a move-to-target tree + * @param targetKey The key to lookup the target under + * @return The root node of the move-to-target tree + */ + public static AITreeNode create(String targetKey){ + return new SelectorNode( + new SequenceNode( + //check if in range of target + new TargetRangeCheckNode(DEFAULT_DIST, targetKey), + new DataDeleteNode(BlackboardKeys.PATHFINDING_POINT), + new DataDeleteNode(BlackboardKeys.PATHFINDING_DATA), + //if in range, stop moving fowards and return SUCCESS + new SucceederNode(new MoveStopNode()) + ), + + //not in range of target, keep moving towards it + new SequenceNode( + PathfindingNode.createPathEntity(targetKey), + new FaceTargetNode(BlackboardKeys.PATHFINDING_POINT), + new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD)) + ) + ); + } + /** * Creates a move-to-target tree * @param dist The target distance to be within @@ -31,7 +61,7 @@ public class MoveToTree { * @return The root node of the move-to-target tree */ public static AITreeNode create(double dist, String targetKey){ - if(dist < PathfindingNode.CLOSENESS_CHECK_BOUND){ + if(dist < PathfindingNode.CLOSENESS_CHECK_BOUND_HORIZONTAL){ throw new Error("Dist less than minimal amount! " + dist); } return new SelectorNode( diff --git a/src/main/java/electrosphere/server/ai/trees/creature/explore/ExploreTree.java b/src/main/java/electrosphere/server/ai/trees/creature/explore/ExploreTree.java new file mode 100644 index 00000000..37263773 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/trees/creature/explore/ExploreTree.java @@ -0,0 +1,37 @@ +package electrosphere.server.ai.trees.creature.explore; + +import electrosphere.server.ai.blackboard.BlackboardKeys; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.meta.DataDeleteNode; +import electrosphere.server.ai.nodes.meta.collections.SequenceNode; +import electrosphere.server.ai.nodes.meta.debug.PublishStatusNode; +import electrosphere.server.ai.nodes.plan.TargetExploreNode; +import electrosphere.server.ai.trees.creature.MoveToTree; + +/** + * A tree for exploring new chunks + */ +public class ExploreTree { + + /** + * Name of the tree + */ + public static final String TREE_NAME = "ExploreTree"; + + /** + * Creates an explore tree + * @return The root node of the explore tree + */ + public static AITreeNode create(){ + return new SequenceNode( + new PublishStatusNode("Explore"), + //resolve point to explore towards + new TargetExploreNode(BlackboardKeys.MOVE_TO_TARGET), + //move towards the point + MoveToTree.create(BlackboardKeys.MOVE_TO_TARGET), + //clear position after moving towards it + new DataDeleteNode(BlackboardKeys.MOVE_TO_TARGET) + ); + } + +} diff --git a/src/main/java/electrosphere/server/ai/trees/creature/resource/AcquireItemTree.java b/src/main/java/electrosphere/server/ai/trees/creature/resource/AcquireItemTree.java index 76d59cc6..52c8aeaa 100644 --- a/src/main/java/electrosphere/server/ai/trees/creature/resource/AcquireItemTree.java +++ b/src/main/java/electrosphere/server/ai/trees/creature/resource/AcquireItemTree.java @@ -16,6 +16,7 @@ import electrosphere.server.ai.nodes.meta.decorators.SucceederNode; import electrosphere.server.ai.nodes.plan.SolveSourcingTreeNode; import electrosphere.server.ai.nodes.plan.TargetEntityCategoryNode; import electrosphere.server.ai.trees.creature.MoveToTree; +import electrosphere.server.ai.trees.creature.explore.ExploreTree; /** * A tree to acquire an item @@ -71,6 +72,11 @@ public class AcquireItemTree { new TargetEntityCategoryNode(BlackboardKeys.HARVEST_TARGET_TYPE), FellTree.create(BlackboardKeys.ENTITY_TARGET), new RunnerNode(null) + ), + new SequenceNode( + new PublishStatusNode("Explore new chunks for resources"), + //Failed to find sources of material in existing chunks, must move for new chunks + ExploreTree.create() ) ), new SucceederNode(null) diff --git a/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java b/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java index 67d4a86b..72e0a62a 100644 --- a/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java +++ b/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java @@ -1097,6 +1097,10 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager throw new Error("Failed to find tracking data for " + start); } Vector3d nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST); + if(nearestValidGoal == null){ + nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST); + throw new Error("Failed to resolve valid point near " + end.x + "," + end.y + "," + end.z); + } List points = this.pathfinder.findPath(this, start, nearestValidGoal, VoxelPathfinder.DEFAULT_MAX_COST); return points; } diff --git a/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java b/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java index 32f98ba3..e7917ec4 100644 --- a/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java +++ b/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java @@ -35,7 +35,7 @@ public class VoxelPathfinder { /** * Maximum distance to scan for a walkable position */ - public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 3; + public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 5; /** * The heuristic lookup table @@ -53,6 +53,9 @@ public class VoxelPathfinder { public List findPath(VoxelCellManager voxelCellManager, Vector3d startPoint, Vector3d endPoint, long maxCost){ List rVal = null; + if(startPoint == null || endPoint == null){ + throw new Error("Points undefined! " + startPoint + " " + endPoint); + } if(startPoint.distance(endPoint) > MAX_DIST){ throw new Error("Distance is outside range provided! " + startPoint.distance(endPoint) + " vs " + MAX_DIST); } @@ -905,7 +908,7 @@ public class VoxelPathfinder { for(int x = 0; x < 4; x++){ for(int y = 0; y < 4; y++){ for(int z = 0; z < 4; z++){ - blockPos.set(voxelPos); + blockPos.set(voxelPos).mul(BlockChunkData.BLOCKS_PER_UNIT_DISTANCE); currChunk.set(chunkPos); offsets.set( x, @@ -953,6 +956,7 @@ public class VoxelPathfinder { Vector3d realPos; Vector3i offsets = new Vector3i(); + while(true){ scanned = 0; for(int x = -radius; x <= radius; x++){ @@ -961,7 +965,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(-radius,x,y); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -973,7 +977,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(radius,x,y); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -985,7 +989,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(x,-radius,y); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -997,7 +1001,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(x,radius,y); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -1009,7 +1013,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(x,y,-radius); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -1021,7 +1025,7 @@ public class VoxelPathfinder { currChunk.set(originChunk); offsets.set(x,y,radius); VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); - realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk); if(realPos.distance(targetPoint) < maxScanRadius){ scanned++; if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ @@ -1047,31 +1051,45 @@ public class VoxelPathfinder { */ private static void clampVoxelOffsets(Vector3i voxelPos, Vector3i chunkPos, Vector3i offsets){ //calculate chunk offsets - voxelPos.x = (voxelPos.x + offsets.x); - voxelPos.y = (voxelPos.y + offsets.y); - voxelPos.z = (voxelPos.z + offsets.z); - if(voxelPos.x < 0){ - voxelPos.x = -1; + int storageX = (voxelPos.x + offsets.x); + int storageY = (voxelPos.y + offsets.y); + int storageZ = (voxelPos.z + offsets.z); + if(storageX < 0){ + storageX = -1; } else { - voxelPos.x = voxelPos.x / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + storageX = storageX / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; } - if(voxelPos.y < 0){ - voxelPos.y = -1; + if(storageY < 0){ + storageY = -1; } else { - voxelPos.y = voxelPos.y / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + storageY = storageY / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; } - if(voxelPos.z < 0){ - voxelPos.z = -1; + if(storageZ < 0){ + storageZ = -1; } else { - voxelPos.z = voxelPos.z / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + storageZ = storageZ / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; } //update world position - chunkPos.x = chunkPos.x + voxelPos.x; - chunkPos.y = chunkPos.y + voxelPos.y; - chunkPos.z = chunkPos.z + voxelPos.z; + chunkPos.x = chunkPos.x + storageX; + chunkPos.y = chunkPos.y + storageY; + chunkPos.z = chunkPos.z + storageZ; voxelPos.x = (voxelPos.x + offsets.x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; voxelPos.y = (voxelPos.y + offsets.y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; voxelPos.z = (voxelPos.z + offsets.z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + + if( + voxelPos.x < 0 || voxelPos.y < 0 || voxelPos.z < 0 || + chunkPos.x < 0 || chunkPos.y < 0 || chunkPos.z < 0 || + chunkPos.x > 65536 || chunkPos.y > 65536 || chunkPos.z > 65536 + ){ + String message = "Failed to clamp \n" + + "voxelPos: " + voxelPos.x + "," + voxelPos.y + "," + voxelPos.z + "\n" + + "chunkPos: " + chunkPos.x + "," + chunkPos.y + "," + chunkPos.z + "\n" + + "offsets: " + offsets.x + "," + offsets.y + "," + offsets.z + "\n" + + "storage: " + storageX + "," + storageY + "," + storageZ + "\n" + + ""; + throw new Error(message); + } } @@ -1083,28 +1101,28 @@ public class VoxelPathfinder { */ private static void clampBlockOffsets(Vector3i blockPos, Vector3i chunkPos, Vector3i offsets){ //calculate chunk offsets - blockPos.x = (blockPos.x + offsets.x); - blockPos.y = (blockPos.y + offsets.y); - blockPos.z = (blockPos.z + offsets.z); - if(blockPos.x < 0){ - blockPos.x = -1; + int storageX = (blockPos.x + offsets.x); + int storageY = (blockPos.y + offsets.y); + int storageZ = (blockPos.z + offsets.z); + if(storageX < 0){ + storageX = -1; } else { - blockPos.x = blockPos.x / BlockChunkData.CHUNK_DATA_WIDTH; + storageX = storageX / BlockChunkData.CHUNK_DATA_WIDTH; } - if(blockPos.y < 0){ - blockPos.y = -1; + if(storageY < 0){ + storageY = -1; } else { - blockPos.y = blockPos.y / BlockChunkData.CHUNK_DATA_WIDTH; + storageY = storageY / BlockChunkData.CHUNK_DATA_WIDTH; } - if(blockPos.z < 0){ - blockPos.z = -1; + if(storageZ < 0){ + storageZ = -1; } else { - blockPos.z = blockPos.z / BlockChunkData.CHUNK_DATA_WIDTH; + storageZ = storageZ / BlockChunkData.CHUNK_DATA_WIDTH; } //update world position - chunkPos.x = chunkPos.x + blockPos.x; - chunkPos.y = chunkPos.y + blockPos.y; - chunkPos.z = chunkPos.z + blockPos.z; + chunkPos.x = chunkPos.x + storageX; + chunkPos.y = chunkPos.y + storageY; + chunkPos.z = chunkPos.z + storageZ; blockPos.x = (blockPos.x + offsets.x + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; blockPos.y = (blockPos.y + offsets.y + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; blockPos.z = (blockPos.z + offsets.z + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; diff --git a/src/main/java/electrosphere/util/math/HashUtils.java b/src/main/java/electrosphere/util/math/HashUtils.java index bc92f2ce..01844f7e 100644 --- a/src/main/java/electrosphere/util/math/HashUtils.java +++ b/src/main/java/electrosphere/util/math/HashUtils.java @@ -44,7 +44,7 @@ public class HashUtils { */ public static long hashIVec(int x, int y, int z){ if (x < 0 || x > 65536 || y < 0 || y > 65536 || z < 0 || z > 65536) { - throw new IllegalArgumentException("Values must be in range [0, 65536]"); + throw new IllegalArgumentException("Values must be in range [0, 65536] " + x + "," + y + "," + z); } return ((long) x) | ((long) y << SHIFT_Y) | ((long) z << SHIFT_Z); }