diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/Node.kt b/src/main/kotlin/org/cobalt/api/pathfinder/Node.kt new file mode 100644 index 0000000..1cdab39 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/Node.kt @@ -0,0 +1,70 @@ +package org.cobalt.api.pathfinder + + +import org.cobalt.api.pathfinder.pathing.heuristic.HeuristicContext +import org.cobalt.api.pathfinder.pathing.heuristic.HeuristicWeights +import org.cobalt.api.pathfinder.pathing.heuristic.IHeuristicStrategy +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class Node( + private val position: PathPosition, + start: PathPosition, + target: PathPosition, + heuristicWeights: HeuristicWeights, + heuristicStrategy: IHeuristicStrategy, + private val depth: Int, +) : Comparable { + + private val hCost: Double = heuristicStrategy.calculate( + HeuristicContext(position, start, target, heuristicWeights) + ) + + private var gCost: Double = 0.0 + private var parent: Node? = null + + fun getPosition(): PathPosition = position + + fun getHeuristic(): Double = hCost + + fun getParent(): Node? = parent + + fun getDepth(): Int = depth + + fun setGCost(gCost: Double) { + this.gCost = gCost + } + + fun setParent(parent: Node?) { + this.parent = parent + } + + fun isTarget(target: PathPosition): Boolean = this.position == target + + fun getFCost(): Double = getGCost() + getHeuristic() + + fun getGCost(): Double { + return if (this.parent == null) 0.0 else this.gCost + } + + override fun equals(other: Any?): Boolean { + if (other == null || this::class != other::class) return false + other as Node + return position == other.position + } + + override fun hashCode(): Int = position.hashCode() + + override fun compareTo(other: Node): Int { + val fCostComparison = this.getFCost().compareTo(other.getFCost()) + if (fCostComparison != 0) { + return fCostComparison + } + + val heuristicComparison = this.getHeuristic().compareTo(other.getHeuristic()) + if (heuristicComparison != 0) { + return heuristicComparison + } + + return this.depth.compareTo(other.depth) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/PathExecutor.kt b/src/main/kotlin/org/cobalt/api/pathfinder/PathExecutor.kt new file mode 100644 index 0000000..aeabe4a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/PathExecutor.kt @@ -0,0 +1,163 @@ +package org.cobalt.api.pathfinder + +import java.awt.Color +import net.minecraft.client.Minecraft +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 +import org.cobalt.api.event.EventBus +import org.cobalt.api.event.annotation.SubscribeEvent +import org.cobalt.api.event.impl.client.TickEvent +import org.cobalt.api.event.impl.render.WorldRenderEvent +import org.cobalt.api.pathfinder.factory.impl.AStarPathfinderFactory +import org.cobalt.api.pathfinder.pathing.NeighborStrategies +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.processing.impl.MinecraftPathProcessor +import org.cobalt.api.pathfinder.pathing.result.Path +import org.cobalt.api.pathfinder.pathing.result.PathState +import org.cobalt.api.pathfinder.provider.impl.MinecraftNavigationProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition +import org.cobalt.api.util.ChatUtils +import org.cobalt.api.util.render.Render3D + +/* + * TODO: im lazy right now, but chunk and world caching would be alot better, + * if someone could help me do this it would be a great help :)) + */ +object PathExecutor { + + private val mc: Minecraft = Minecraft.getInstance() + private var currentPath: Path? = null + private var currentWaypointIndex: Int = 0 + + init { + EventBus.register(this) + } + + fun start(x: Double, y: Double, z: Double) { + val player = mc.player ?: return + val start = PathPosition(player.x, player.y, player.z) + val target = PathPosition(x, y, z) + + val factory = AStarPathfinderFactory() + + val processor = MinecraftPathProcessor() + val config = + PathfinderConfiguration.builder() + .provider(MinecraftNavigationProvider()) + .maxIterations(20000) + .async(true) + .neighborStrategy(NeighborStrategies.HORIZONTAL_DIAGONAL_AND_VERTICAL) + .nodeValidationProcessors(listOf(processor)) + .nodeCostProcessors(listOf(processor)) + .build() + + val pathfinder = factory.createPathfinder(config) + + ChatUtils.sendDebug("Calculating path to $x, $y, $z...") + val startTime = System.currentTimeMillis() + pathfinder.findPath(start, target).thenAccept { result -> + val duration = System.currentTimeMillis() - startTime + if (result.successful()) { + currentPath = result.getPath() + currentWaypointIndex = 0 + + val state = result.getPathState() + val path = result.getPath() + + if (state == PathState.FOUND) { + ChatUtils.sendMessage( + "§aPath found! §7Calculated in §f${duration}ms §8(${path.length()} nodes)" + ) + } else { + /* + * partial paths can happen, i would recommend improving this functionality + * to whoever maintainer wants to maintain my horrible shitcode of a pathfinder. + * i think partial paths should be refactored, i will write all the cases now + * iteration limit -> self explanatory xd (or if u cant use ur brain + * then its just if the algorithm takes too many iterations to find a goal. + * this is set literaaally like 20 lines above you...) + * fallback -> pf searches everywhere and couldnt find a possible way to get there. + * this can happen in the case of unloaded chunks, or an obstruction + */ + ChatUtils.sendMessage( + "§ePartial path found! §7Calculated in §f${duration}ms §8(${path.length()} nodes)" + ) + } + } else { + ChatUtils.sendMessage("§cFailed to find path: ${result.getPathState()}") + } + } + } + + fun stop() { + currentPath = null + currentWaypointIndex = 0 + } + + /* + * as of writing this, there is intentionally no moving, rotations, + * or jumping yet. i ask that you make sure that the algo works good + * before implementing them, aswell as rots + */ + @SubscribeEvent + fun onTick(@Suppress("UNUSED_PARAMETER") event: TickEvent.Start) { + val path = currentPath ?: return + val player = mc.player ?: return + + val waypoints = path.collect().toList() + if (currentWaypointIndex >= waypoints.size) { + ChatUtils.sendMessage("Reached the end!") // this is kinda icky, someone change this lol + stop() + return + } + + val targetPos = waypoints[currentWaypointIndex].mid() + val targetVec = Vec3(targetPos.x, targetPos.y, targetPos.z) + + val horizontalDistSq = + (player.x - targetVec.x) * (player.x - targetVec.x) + + (player.z - targetVec.z) * (player.z - targetVec.z) + if (horizontalDistSq < 0.25) { + currentWaypointIndex++ + } + } + + @SubscribeEvent + fun onRender(event: WorldRenderEvent.Last) { + val path = currentPath ?: return + val waypoints = path.collect().toList() + + if (waypoints.size < 2) return + + for (i in 0 until waypoints.size - 1) { + val start = waypoints[i].mid() + val end = waypoints[i + 1].mid() + + Render3D.drawLine( + event.context, + Vec3(start.x, start.y, start.z), + Vec3(end.x, end.y, end.z), + Color.CYAN, + true, + 2.0f + ) + } + + if (currentWaypointIndex < waypoints.size) { + val currentPos = waypoints[currentWaypointIndex].mid() + Render3D.drawBox( + event.context, + AABB( + currentPos.x - 0.25, + currentPos.y - 0.25, + currentPos.z - 0.25, + currentPos.x + 0.25, + currentPos.y + 0.25, + currentPos.z + 0.25 + ), + Color.GREEN, + true + ) + } + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderFactory.kt b/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderFactory.kt new file mode 100644 index 0000000..6c45153 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderFactory.kt @@ -0,0 +1,22 @@ +package org.cobalt.api.pathfinder.factory + +import org.cobalt.api.pathfinder.pathing.Pathfinder +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration + +interface PathfinderFactory { + fun createPathfinder(): Pathfinder = createPathfinder(PathfinderConfiguration.DEFAULT) + + fun createPathfinder(configuration: PathfinderConfiguration): Pathfinder = + throw UnsupportedOperationException( + "This factory does not support creating pathfinders with a configuration." + ) + + fun createPathfinder( + configuration: PathfinderConfiguration, + initializer: PathfinderInitializer, + ): Pathfinder { + val pathfinder = createPathfinder(configuration) + initializer.initialize(pathfinder, configuration) + return pathfinder + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderInitializer.kt b/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderInitializer.kt new file mode 100644 index 0000000..378ec5e --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/factory/PathfinderInitializer.kt @@ -0,0 +1,8 @@ +package org.cobalt.api.pathfinder.factory + +import org.cobalt.api.pathfinder.pathing.Pathfinder +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration + +fun interface PathfinderInitializer { + fun initialize(pathfinder: Pathfinder, configuration: PathfinderConfiguration) +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/factory/impl/AStarPathfinderFactory.kt b/src/main/kotlin/org/cobalt/api/pathfinder/factory/impl/AStarPathfinderFactory.kt new file mode 100644 index 0000000..e8be3e0 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/factory/impl/AStarPathfinderFactory.kt @@ -0,0 +1,13 @@ +package org.cobalt.api.pathfinder.factory.impl + +import org.cobalt.api.pathfinder.factory.PathfinderFactory +import org.cobalt.api.pathfinder.pathfinder.AStarPathfinder +import org.cobalt.api.pathfinder.pathing.Pathfinder +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration + +class AStarPathfinderFactory : PathfinderFactory { + + override fun createPathfinder(configuration: PathfinderConfiguration): Pathfinder { + return AStarPathfinder(configuration) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AStarPathfinder.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AStarPathfinder.kt new file mode 100644 index 0000000..bcab41f --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AStarPathfinder.kt @@ -0,0 +1,239 @@ +package org.cobalt.api.pathfinder.pathfinder + +import it.unimi.dsi.fastutil.longs.Long2DoubleMap +import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap +import it.unimi.dsi.fastutil.longs.Long2ObjectMap +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import java.util.function.LongFunction +import kotlin.math.abs +import kotlin.math.max +import net.minecraft.util.Mth +import org.cobalt.api.pathfinder.Node +import org.cobalt.api.pathfinder.pathfinder.heap.PrimitiveMinHeap +import org.cobalt.api.pathfinder.pathfinder.processing.EvaluationContextImpl +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.processing.context.EvaluationContext +import org.cobalt.api.pathfinder.pathing.processing.context.SearchContext +import org.cobalt.api.pathfinder.util.GridRegionData +import org.cobalt.api.pathfinder.util.RegionKey +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class AStarPathfinder(configuration: PathfinderConfiguration) : AbstractPathfinder(configuration) { + + private val currentSession = ThreadLocal() + + override fun insertStartNode(node: Node, fCost: Double, openSet: PrimitiveMinHeap) { + val session = getSessionOrThrow() + val packedPos = RegionKey.pack(node.getPosition()) + openSet.insertOrUpdate(packedPos, fCost) + session.openSetNodes[packedPos] = node + } + + override fun extractBestNode(openSet: PrimitiveMinHeap): Node { + val session = getSessionOrThrow() + val packedPos = openSet.extractMin() + val node = session.openSetNodes[packedPos]!! + session.openSetNodes.remove(packedPos) + return node + } + + override fun initializeSearch() { + currentSession.set(PathfindingSession()) + } + + override fun processSuccessors( + requestStart: PathPosition, + requestTarget: PathPosition, + currentNode: Node, + openSet: PrimitiveMinHeap, + searchContext: SearchContext, + ) { + val session = getSessionOrThrow() + val offsets = neighborStrategy.getOffsets(currentNode.getPosition()) + + for (offset in offsets) { + val neighborPos = currentNode.getPosition().add(offset) + val packedPos = RegionKey.pack(neighborPos) + + if (openSet.contains(packedPos)) { + val existing = session.openSetNodes[packedPos]!! + updateExistingNode(existing, packedPos, currentNode, searchContext, openSet) + continue + } + + val regionData = session.getOrCreateRegionData(neighborPos) + if (regionData.getBloomFilter().mightContain(neighborPos) && + regionData.getRegionalExaminedPositions().contains(packedPos) + ) { + + var shouldReopen = false + if (pathfinderConfiguration.shouldReopenClosedNodes()) { + val oldCost = session.closedSetGCosts[packedPos] + + val tempNeighbor = + createNeighborNode(neighborPos, requestStart, requestTarget, currentNode) + val context = + EvaluationContextImpl( + searchContext, + tempNeighbor, + currentNode, + pathfinderConfiguration.heuristicStrategy + ) + val newGCost = calculateGCost(context) + + if (oldCost.isNaN() || newGCost + Math.ulp(newGCost) < oldCost) { + session.closedSetGCosts[packedPos] = newGCost + shouldReopen = true + } + } + + if (!shouldReopen) continue + } + + val neighbor = createNeighborNode(neighborPos, requestStart, requestTarget, currentNode) + neighbor.setParent(currentNode) + + val context = + EvaluationContextImpl( + searchContext, + neighbor, + currentNode, + pathfinderConfiguration.heuristicStrategy + ) + + if (!isValidByCustomProcessors(context)) { + continue + } + + val gCost = calculateGCost(context) + neighbor.setGCost(gCost) + val fCost = neighbor.getFCost() + val heapKey = calculateHeapKey(neighbor, fCost) + + openSet.insertOrUpdate(packedPos, heapKey) + session.openSetNodes[packedPos] = neighbor + } + } + + private fun updateExistingNode( + existing: Node, + packedPos: Long, + currentNode: Node, + searchContext: SearchContext, + openSet: PrimitiveMinHeap, + ) { + val context = + EvaluationContextImpl( + searchContext, + existing, + currentNode, + pathfinderConfiguration.heuristicStrategy + ) + + val newG = calculateGCost(context) + val tol = Math.ulp(max(abs(newG), abs(existing.getGCost()))) + + if (newG + tol >= existing.getGCost()) return + + if (!isValidByCustomProcessors(context)) { + return + } + + existing.setParent(currentNode) + existing.setGCost(newG) + val newF = existing.getFCost() + val newKey = calculateHeapKey(existing, newF) + val oldKey = openSet.getCost(packedPos) + + if (newKey + Math.ulp(newKey) < oldKey) { + openSet.insertOrUpdate(packedPos, newKey) + } else if (abs(newKey - oldKey) <= Math.ulp(newKey)) { + openSet.insertOrUpdate(packedPos, oldKey - Math.ulp(oldKey)) + } + } + + private fun createNeighborNode( + position: PathPosition, + start: PathPosition, + target: PathPosition, + parent: Node, + ): Node { + return Node( + position, + start, + target, + pathfinderConfiguration.heuristicWeights, + pathfinderConfiguration.heuristicStrategy, + parent.getDepth() + 1 + ) + } + + private fun isValidByCustomProcessors(context: EvaluationContext): Boolean { + if (validationProcessors.isNullOrEmpty()) { + return true + } + + for (validator in validationProcessors) { + if (!validator.isValid(context)) { + return false + } + } + return true + } + + private fun calculateGCost(context: EvaluationContext): Double { + val baseCost = context.getBaseTransitionCost() + val additionalCost = + costProcessors?.sumOf { it.calculateCostContribution(context).value } ?: 0.0 + + val transitionCost = max(0.0, baseCost + additionalCost) + return context.getPathCostToPreviousPosition() + transitionCost + } + + override fun markNodeAsExpanded(node: Node) { + val session = getSessionOrThrow() + val position = node.getPosition() + val packedPos = RegionKey.pack(position) + + session.openSetNodes.remove(packedPos) + + if (pathfinderConfiguration.shouldReopenClosedNodes()) { + session.closedSetGCosts[packedPos] = node.getGCost() + } + + val regionData = session.getOrCreateRegionData(position) + regionData.getBloomFilter().put(position) + regionData.getRegionalExaminedPositions().add(packedPos) + } + + override fun performAlgorithmCleanup() { + currentSession.remove() + } + + private fun getSessionOrThrow(): PathfindingSession { + return currentSession.get() + ?: throw IllegalStateException( + "Pathfinding session not initialized. Call initializeSearch() first." + ) + } + + private inner class PathfindingSession { + val visitedRegions: Long2ObjectMap = Long2ObjectOpenHashMap() + val openSetNodes: Long2ObjectMap = Long2ObjectOpenHashMap() + val closedSetGCosts: Long2DoubleMap = + Long2DoubleOpenHashMap().apply { defaultReturnValue(Double.NaN) } + + fun getOrCreateRegionData(position: PathPosition): GridRegionData { + val cellSize = pathfinderConfiguration.gridCellSize + val rX = Mth.floorDiv(position.flooredX, cellSize) + val rY = Mth.floorDiv(position.flooredY, cellSize) + val rZ = Mth.floorDiv(position.flooredZ, cellSize) + val regionKey = RegionKey.pack(rX, rY, rZ) + + return visitedRegions.computeIfAbsent( + regionKey, + LongFunction { GridRegionData(pathfinderConfiguration) } + ) + } + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AbstractPathfinder.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AbstractPathfinder.kt new file mode 100644 index 0000000..0d288e8 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/AbstractPathfinder.kt @@ -0,0 +1,331 @@ +package org.cobalt.api.pathfinder.pathfinder + +import java.util.* +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.abs +import kotlin.math.max +import org.cobalt.api.pathfinder.Node +import org.cobalt.api.pathfinder.pathfinder.heap.PrimitiveMinHeap +import org.cobalt.api.pathfinder.pathfinder.processing.EvaluationContextImpl +import org.cobalt.api.pathfinder.pathfinder.processing.SearchContextImpl +import org.cobalt.api.pathfinder.pathing.INeighborStrategy +import org.cobalt.api.pathfinder.pathing.Pathfinder +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.pathing.hook.PathfinderHook +import org.cobalt.api.pathfinder.pathing.hook.PathfindingContext +import org.cobalt.api.pathfinder.pathing.processing.CostProcessor +import org.cobalt.api.pathfinder.pathing.processing.Processor +import org.cobalt.api.pathfinder.pathing.processing.ValidationProcessor +import org.cobalt.api.pathfinder.pathing.processing.context.SearchContext +import org.cobalt.api.pathfinder.pathing.result.Path +import org.cobalt.api.pathfinder.pathing.result.PathState +import org.cobalt.api.pathfinder.pathing.result.PathfinderResult +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.result.PathImpl +import org.cobalt.api.pathfinder.result.PathfinderResultImpl +import org.cobalt.api.pathfinder.wrapper.Depth +import org.cobalt.api.pathfinder.wrapper.PathPosition + +abstract class AbstractPathfinder( + protected val pathfinderConfiguration: PathfinderConfiguration, +) : Pathfinder { + + companion object { + private val EMPTY_PATH_POSITIONS: Set = LinkedHashSet(0) + private const val TIE_BREAKER_WEIGHT = 1e-6 + + private val PATHING_EXECUTOR_SERVICE: ExecutorService = + Executors.newWorkStealingPool(max(1, Runtime.getRuntime().availableProcessors() / 2)) + + init { + Runtime.getRuntime().addShutdownHook(Thread { shutdownExecutor() }) + } + + private fun shutdownExecutor() { + PATHING_EXECUTOR_SERVICE.shutdown() + try { + if (!PATHING_EXECUTOR_SERVICE.awaitTermination(5, TimeUnit.SECONDS)) { + PATHING_EXECUTOR_SERVICE.shutdownNow() + } + } catch (e: InterruptedException) { + PATHING_EXECUTOR_SERVICE.shutdownNow() + Thread.currentThread().interrupt() + } + } + } + + protected val navigationPointProvider: NavigationPointProvider = pathfinderConfiguration.provider + protected val validationProcessors: List? = + pathfinderConfiguration.getNodeValidationProcessors() + protected val costProcessors: List? = + pathfinderConfiguration.getNodeCostProcessors() + protected val neighborStrategy: INeighborStrategy = pathfinderConfiguration.neighborStrategy + + private val pathfinderHooks: MutableSet = Collections.synchronizedSet(HashSet()) + + private val abortRequested = AtomicBoolean(false) + + override fun findPath( + start: PathPosition, + target: PathPosition, + context: EnvironmentContext?, + ): CompletionStage { + this.abortRequested.set(false) + return initiatePathing(start, target, context) + } + + override fun abort() { + this.abortRequested.set(true) + } + + override fun registerPathfindingHook(hook: PathfinderHook) { + this.pathfinderHooks.add(hook) + } + + private fun initiatePathing( + start: PathPosition, + target: PathPosition, + environmentContext: EnvironmentContext?, + ): CompletionStage { + val effectiveStart = start.floor() + val effectiveTarget = target.floor() + + return if (pathfinderConfiguration.async) { + CompletableFuture.supplyAsync( + { + executePathingAlgorithm(effectiveStart, effectiveTarget, environmentContext) + }, + PATHING_EXECUTOR_SERVICE + ) + .exceptionally { throwable -> handlePathingException(start, target, throwable) } + } else { + try { + CompletableFuture.completedFuture( + executePathingAlgorithm(effectiveStart, effectiveTarget, environmentContext) + ) + } catch (e: Exception) { + CompletableFuture.completedFuture(handlePathingException(start, target, e)) + } + } + } + + private fun executePathingAlgorithm( + start: PathPosition, + target: PathPosition, + environmentContext: EnvironmentContext?, + ): PathfinderResult { + initializeSearch() + + val searchContext = + SearchContextImpl( + start, + target, + this.pathfinderConfiguration, + this.navigationPointProvider, + environmentContext + ) + + val processors = getProcessors() + + try { + processors.forEach { it.initializeSearch(searchContext) } + + val startNode = createStartNode(start, target) + val startNodeContext = + EvaluationContextImpl( + searchContext, + startNode, + null, + pathfinderConfiguration.heuristicStrategy + ) + + if (!validationProcessors.isNullOrEmpty()) { + val isStartNodeInvalid = validationProcessors.any { !it.isValid(startNodeContext) } + if (isStartNodeInvalid) { + return PathfinderResultImpl( + PathState.FAILED, + PathImpl(start, target, EMPTY_PATH_POSITIONS) + ) + } + } + + val openSet = PrimitiveMinHeap(1024) + val startKey = + try { + calculateHeapKey(startNode, startNode.getFCost()) + } catch (t: Throwable) { + startNode.getFCost() + } + + insertStartNode(startNode, startKey, openSet) + + var currentDepth = 0 + var bestFallbackNode = startNode + + while (!openSet.isEmpty() && currentDepth < pathfinderConfiguration.maxIterations) { + currentDepth++ + + if (this.abortRequested.get()) { + return createAbortedResult(start, target, bestFallbackNode) + } + + val currentNode = extractBestNode(openSet) + markNodeAsExpanded(currentNode) + + pathfinderHooks.forEach { hook -> + hook.onPathfindingStep( + PathfindingContext(currentNode.getPosition(), Depth.of(currentDepth)) + ) + } + + if (currentNode.getHeuristic() < bestFallbackNode.getHeuristic()) { + bestFallbackNode = currentNode + } + + if (hasReachedPathLengthLimit(currentNode)) { + return PathfinderResultImpl( + PathState.LENGTH_LIMITED, + reconstructPath(start, target, currentNode) + ) + } + + if (currentNode.isTarget(target)) { + return PathfinderResultImpl(PathState.FOUND, reconstructPath(start, target, currentNode)) + } + + processSuccessors(start, target, currentNode, openSet, searchContext) + } + + return determinePostLoopResult(currentDepth, start, target, bestFallbackNode) + } catch (e: Exception) { + return PathfinderResultImpl(PathState.FAILED, PathImpl(start, target, EMPTY_PATH_POSITIONS)) + } finally { + val finalizeErrors = mutableListOf() + processors.forEach { processor -> + try { + processor.finalizeSearch(searchContext) + } catch (e: Exception) { + finalizeErrors.add(e) + } + } + performAlgorithmCleanup() + } + } + + fun calculateHeapKey(neighbor: Node, fCost: Double): Double { + val heuristic = neighbor.getHeuristic() + val tieBreaker = TIE_BREAKER_WEIGHT * (heuristic / (abs(fCost) + 1)) + var heapKey = fCost - tieBreaker + + if (heapKey.isNaN() || heapKey.isInfinite()) { + heapKey = fCost + } + + return heapKey + } + + private fun getProcessors(): List { + val processors = mutableListOf() + + validationProcessors?.let { processors.addAll(it) } + costProcessors?.let { processors.addAll(it) } + + return processors + } + + private fun createAbortedResult( + start: PathPosition, + target: PathPosition, + fallbackNode: Node, + ): PathfinderResult { + this.abortRequested.set(false) + return PathfinderResultImpl(PathState.ABORTED, reconstructPath(start, target, fallbackNode)) + } + + private fun handlePathingException( + originalStart: PathPosition, + originalTarget: PathPosition, + @Suppress("UNUSED_PARAMETER") throwable: Throwable, + ): PathfinderResult { + return PathfinderResultImpl( + PathState.FAILED, + PathImpl(originalStart, originalTarget, EMPTY_PATH_POSITIONS) + ) + } + + protected fun createStartNode(startPos: PathPosition, targetPos: PathPosition): Node { + return Node( + startPos, + startPos, + targetPos, + pathfinderConfiguration.heuristicWeights, + pathfinderConfiguration.heuristicStrategy, + 0 + ) + } + + private fun hasReachedPathLengthLimit(currentNode: Node): Boolean { + val maxLength = pathfinderConfiguration.maxLength + return maxLength > 0 && currentNode.getDepth() >= maxLength + } + + private fun determinePostLoopResult( + depthReached: Int, + start: PathPosition, + target: PathPosition, + fallbackNode: Node, + ): PathfinderResult { + return when { + depthReached >= pathfinderConfiguration.maxIterations -> { + PathfinderResultImpl( + PathState.MAX_ITERATIONS_REACHED, + reconstructPath(start, target, fallbackNode) + ) + } + + pathfinderConfiguration.fallback -> { + PathfinderResultImpl(PathState.FALLBACK, reconstructPath(start, target, fallbackNode)) + } + + else -> { + PathfinderResultImpl(PathState.FAILED, PathImpl(start, target, EMPTY_PATH_POSITIONS)) + } + } + } + + protected fun reconstructPath(start: PathPosition, target: PathPosition, endNode: Node): Path { + if (endNode.getParent() == null && endNode.getDepth() == 0) { + return PathImpl(start, target, listOf(endNode.getPosition())) + } + + val pathPositions = tracePathPositionsFromNode(endNode) + return PathImpl(start, target, pathPositions) + } + + private fun tracePathPositionsFromNode(leafNode: Node): List { + return generateSequence(leafNode) { it.getParent() } + .map { it.getPosition() } + .toList() + .reversed() + } + + protected abstract fun insertStartNode(node: Node, fCost: Double, openSet: PrimitiveMinHeap) + + protected abstract fun extractBestNode(openSet: PrimitiveMinHeap): Node + + protected abstract fun initializeSearch() + + protected abstract fun markNodeAsExpanded(node: Node) + + protected abstract fun performAlgorithmCleanup() + + protected abstract fun processSuccessors( + requestStart: PathPosition, + requestTarget: PathPosition, + currentNode: Node, + openSet: PrimitiveMinHeap, + searchContext: SearchContext, + ) +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/heap/PrimitiveMinHeap.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/heap/PrimitiveMinHeap.kt new file mode 100644 index 0000000..c997ef5 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/heap/PrimitiveMinHeap.kt @@ -0,0 +1,140 @@ +package org.cobalt.api.pathfinder.pathfinder.heap + +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap + +class PrimitiveMinHeap(initialCapacity: Int) { + + private val nodeToIndexMap: Long2IntOpenHashMap = + Long2IntOpenHashMap(initialCapacity).apply { defaultReturnValue(-1) } + + private var nodes: LongArray = LongArray(initialCapacity + 1) + private var costs: DoubleArray = DoubleArray(initialCapacity + 1) + private var size = 0 + + fun isEmpty(): Boolean = size == 0 + + fun size(): Int = size + + fun clear() { + size = 0 + nodeToIndexMap.clear() + } + + fun peekMin(): Long { + if (size == 0) throw NoSuchElementException() + return nodes[1] + } + + fun peekMinCost(): Double { + if (size == 0) throw NoSuchElementException() + return costs[1] + } + + fun contains(packedNode: Long): Boolean = nodeToIndexMap.containsKey(packedNode) + + fun getCost(packedNode: Long): Double { + val index = nodeToIndexMap.get(packedNode) + return if (index == -1) Double.MAX_VALUE else costs[index] + } + + fun insertOrUpdate(packedNode: Long, cost: Double) { + val existingIndex = nodeToIndexMap.get(packedNode) + + if (existingIndex != -1) { + if (cost < costs[existingIndex]) { + costs[existingIndex] = cost + siftUp(existingIndex) + } + } else { + ensureCapacity() + size++ + nodes[size] = packedNode + costs[size] = cost + nodeToIndexMap.put(packedNode, size) + siftUp(size) + } + } + + fun extractMin(): Long { + if (size == 0) throw NoSuchElementException() + + val minNode = nodes[1] + nodeToIndexMap.remove(minNode) + + val lastNode = nodes[size] + val lastCost = costs[size] + nodes[1] = lastNode + costs[1] = lastCost + size-- + + if (size > 0) { + nodeToIndexMap.put(lastNode, 1) + siftDown(1) + } + + return minNode + } + + private fun ensureCapacity() { + if (size >= nodes.size - 1) { + val newCap = nodes.size * 2 + nodes = nodes.copyOf(newCap) + costs = costs.copyOf(newCap) + } + } + + private fun siftUp(index: Int) { + var current = index + val nodeToMove = nodes[current] + val costToMove = costs[current] + + while (current > 1) { + val parentIndex = current shr 1 + val parentCost = costs[parentIndex] + + if (costToMove < parentCost) { + nodes[current] = nodes[parentIndex] + costs[current] = parentCost + nodeToIndexMap.put(nodes[current], current) + current = parentIndex + } else { + break + } + } + + nodes[current] = nodeToMove + costs[current] = costToMove + nodeToIndexMap.put(nodeToMove, current) + } + + private fun siftDown(index: Int) { + var current = index + val nodeToMove = nodes[current] + val costToMove = costs[current] + val half = size shr 1 + + while (current <= half) { + var childIndex = current shl 1 + var childCost = costs[childIndex] + val rightIndex = childIndex + 1 + + if (rightIndex <= size && costs[rightIndex] < childCost) { + childIndex = rightIndex + childCost = costs[rightIndex] + } + + if (costToMove > childCost) { + nodes[current] = nodes[childIndex] + costs[current] = childCost + nodeToIndexMap.put(nodes[current], current) + current = childIndex + } else { + break + } + } + + nodes[current] = nodeToMove + costs[current] = costToMove + nodeToIndexMap.put(nodeToMove, current) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/EvaluationContextImpl.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/EvaluationContextImpl.kt new file mode 100644 index 0000000..96c0fe6 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/EvaluationContextImpl.kt @@ -0,0 +1,49 @@ +package org.cobalt.api.pathfinder.pathfinder.processing + +import kotlin.math.max +import org.cobalt.api.pathfinder.Node +import org.cobalt.api.pathfinder.pathing.heuristic.IHeuristicStrategy +import org.cobalt.api.pathfinder.pathing.processing.context.EvaluationContext +import org.cobalt.api.pathfinder.pathing.processing.context.SearchContext +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class EvaluationContextImpl( + private val searchContext: SearchContext, + private val engineNode: Node, + private val parentEngineNode: Node?, + private val heuristicStrategy: IHeuristicStrategy, +) : EvaluationContext { + + override fun getCurrentPathPosition(): PathPosition = engineNode.getPosition() + + override fun getPreviousPathPosition(): PathPosition? = parentEngineNode?.getPosition() + + override fun getCurrentNodeDepth(): Int = engineNode.getDepth() + + override fun getCurrentNodeHeuristicValue(): Double = engineNode.getHeuristic() + + override fun getPathCostToPreviousPosition(): Double { + return parentEngineNode?.getGCost() ?: 0.0 + } + + override fun getBaseTransitionCost(): Double { + if (parentEngineNode == null) return 0.0 + + val from = parentEngineNode.getPosition() + val to = engineNode.getPosition() + val baseCost = heuristicStrategy.calculateTransitionCost(from, to) + + if (baseCost.isNaN() || baseCost.isInfinite()) { + throw IllegalStateException( + "Heuristic transition cost produced an invalid numeric value: $baseCost" + ) + } + + return max(baseCost, 0.0) + } + + override fun getSearchContext(): SearchContext = searchContext + + override fun getGrandparentPathPosition(): PathPosition? = + parentEngineNode?.getParent()?.getPosition() +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/SearchContextImpl.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/SearchContextImpl.kt new file mode 100644 index 0000000..2da018d --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathfinder/processing/SearchContextImpl.kt @@ -0,0 +1,30 @@ +package org.cobalt.api.pathfinder.pathfinder.processing + +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.pathing.processing.context.SearchContext +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class SearchContextImpl( + private val startPathPosition: PathPosition, + private val targetPathPosition: PathPosition, + private val pathfinderConfiguration: PathfinderConfiguration, + private val navigationPointProvider: NavigationPointProvider, + private val environmentContext: EnvironmentContext?, +) : SearchContext { + + private val sharedData: MutableMap = HashMap() + + override fun getStartPathPosition(): PathPosition = startPathPosition + + override fun getTargetPathPosition(): PathPosition = targetPathPosition + + override fun getPathfinderConfiguration(): PathfinderConfiguration = pathfinderConfiguration + + override fun getNavigationPointProvider(): NavigationPointProvider = navigationPointProvider + + override fun getSharedData(): MutableMap = sharedData + + override fun getEnvironmentContext(): EnvironmentContext? = environmentContext +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/INeighborStrategy.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/INeighborStrategy.kt new file mode 100644 index 0000000..870cbac --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/INeighborStrategy.kt @@ -0,0 +1,12 @@ +package org.cobalt.api.pathfinder.pathing + +import org.cobalt.api.pathfinder.wrapper.PathPosition +import org.cobalt.api.pathfinder.wrapper.PathVector + +fun interface INeighborStrategy { + fun getOffsets(): Iterable + + fun getOffsets(currentPosition: PathPosition): Iterable { + return getOffsets() + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/NeighborStrategies.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/NeighborStrategies.kt new file mode 100644 index 0000000..0c63d0a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/NeighborStrategies.kt @@ -0,0 +1,48 @@ +package org.cobalt.api.pathfinder.pathing + +import org.cobalt.api.pathfinder.wrapper.PathVector + +object NeighborStrategies { + private val VERTICAL_AND_HORIZONTAL_OFFSETS = + listOf( + PathVector(1.0, 0.0, 0.0), + PathVector(-1.0, 0.0, 0.0), + PathVector(0.0, 0.0, 1.0), + PathVector(0.0, 0.0, -1.0), + PathVector(0.0, 1.0, 0.0), + PathVector(0.0, -1.0, 0.0) + ) + + private val DIAGONAL_3D_OFFSETS = buildList { + for (x in -1..1) { + for (y in -1..1) { + for (z in -1..1) { + if (x == 0 && y == 0 && z == 0) continue + add(PathVector(x.toDouble(), y.toDouble(), z.toDouble())) + } + } + } + } + + private val HORIZONTAL_DIAGONAL_AND_VERTICAL_OFFSETS = + listOf( + PathVector(1.0, 0.0, 0.0), + PathVector(-1.0, 0.0, 0.0), + PathVector(0.0, 0.0, 1.0), + PathVector(0.0, 0.0, -1.0), + PathVector(0.0, 1.0, 0.0), + PathVector(0.0, -1.0, 0.0), + PathVector(1.0, 0.0, 1.0), + PathVector(1.0, 0.0, -1.0), + PathVector(-1.0, 0.0, 1.0), + PathVector(-1.0, 0.0, -1.0) + ) + + val VERTICAL_AND_HORIZONTAL = INeighborStrategy { VERTICAL_AND_HORIZONTAL_OFFSETS } + + val DIAGONAL_3D = INeighborStrategy { DIAGONAL_3D_OFFSETS } + + val HORIZONTAL_DIAGONAL_AND_VERTICAL = INeighborStrategy { + HORIZONTAL_DIAGONAL_AND_VERTICAL_OFFSETS + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/Pathfinder.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/Pathfinder.kt new file mode 100644 index 0000000..1615a8c --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/Pathfinder.kt @@ -0,0 +1,23 @@ +package org.cobalt.api.pathfinder.pathing + +import java.util.concurrent.CompletionStage +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.pathing.hook.PathfinderHook +import org.cobalt.api.pathfinder.pathing.result.PathfinderResult +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface Pathfinder { + fun findPath(start: PathPosition, target: PathPosition): CompletionStage { + return findPath(start, target, null) + } + + fun findPath( + start: PathPosition, + target: PathPosition, + context: EnvironmentContext?, + ): CompletionStage + + fun abort() + + fun registerPathfindingHook(hook: PathfinderHook) +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/PathfindingProgress.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/PathfindingProgress.kt new file mode 100644 index 0000000..2917314 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/PathfindingProgress.kt @@ -0,0 +1,13 @@ +package org.cobalt.api.pathfinder.pathing + +import org.cobalt.api.pathfinder.wrapper.PathPosition + +data class PathfindingProgress( + private val start: PathPosition, + private val current: PathPosition, + private val target: PathPosition, +) { + fun startPosition(): PathPosition = start + fun currentPosition(): PathPosition = current + fun targetPosition(): PathPosition = target +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/calc/DistanceCalculator.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/calc/DistanceCalculator.kt new file mode 100644 index 0000000..9b599e0 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/calc/DistanceCalculator.kt @@ -0,0 +1,7 @@ +package org.cobalt.api.pathfinder.pathing.calc + +import org.cobalt.api.pathfinder.pathing.PathfindingProgress + +fun interface DistanceCalculator { + fun calculate(progress: PathfindingProgress): M? +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/configuration/PathfinderConfiguration.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/configuration/PathfinderConfiguration.kt new file mode 100644 index 0000000..b14ac13 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/configuration/PathfinderConfiguration.kt @@ -0,0 +1,185 @@ +package org.cobalt.api.pathfinder.pathing.configuration + +import org.cobalt.api.pathfinder.pathing.INeighborStrategy +import org.cobalt.api.pathfinder.pathing.NeighborStrategies +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.pathing.heuristic.HeuristicStrategies +import org.cobalt.api.pathfinder.pathing.heuristic.HeuristicWeights +import org.cobalt.api.pathfinder.pathing.heuristic.IHeuristicStrategy +import org.cobalt.api.pathfinder.pathing.processing.CostProcessor +import org.cobalt.api.pathfinder.pathing.processing.ValidationProcessor +import org.cobalt.api.pathfinder.provider.NavigationPoint +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition + +data class PathfinderConfiguration( + val maxIterations: Int, + val maxLength: Int, + val async: Boolean, + val fallback: Boolean, + val provider: NavigationPointProvider, + val heuristicWeights: HeuristicWeights, + val validationProcessors: List, + val costProcessors: List, + val neighborStrategy: INeighborStrategy, + val gridCellSize: Int, + val bloomFilterSize: Int, + val bloomFilterFpp: Double, + val heuristicStrategy: IHeuristicStrategy, + val reopenClosedNodes: Boolean, +) { + companion object { + val DEFAULT: PathfinderConfiguration = builder().build() + + fun deepCopy(pathfinderConfiguration: PathfinderConfiguration): PathfinderConfiguration { + return builder() + .maxIterations(pathfinderConfiguration.maxIterations) + .maxLength(pathfinderConfiguration.maxLength) + .async(pathfinderConfiguration.async) + .fallback(pathfinderConfiguration.fallback) + .provider(pathfinderConfiguration.provider) + .heuristicWeights(pathfinderConfiguration.heuristicWeights) + .nodeValidationProcessors(pathfinderConfiguration.validationProcessors) + .nodeCostProcessors(pathfinderConfiguration.costProcessors) + .neighborStrategy(pathfinderConfiguration.neighborStrategy) + .gridCellSize(pathfinderConfiguration.gridCellSize) + .bloomFilterSize(pathfinderConfiguration.bloomFilterSize) + .bloomFilterFpp(pathfinderConfiguration.bloomFilterFpp) + .heuristicStrategy(pathfinderConfiguration.heuristicStrategy) + .reopenClosedNodes(pathfinderConfiguration.reopenClosedNodes) + .build() + } + + fun builder(): PathfinderConfigurationBuilder { + return PathfinderConfigurationBuilder() + } + } + + fun getNodeCostProcessors(): List = costProcessors + + fun getNodeValidationProcessors(): List = validationProcessors + + fun shouldReopenClosedNodes(): Boolean = reopenClosedNodes +} + +class PathfinderConfigurationBuilder { + private var maxIterations: Int = 5000 + private var maxLength: Int = 0 + private var async: Boolean = false + private var fallback: Boolean = true + private var provider: NavigationPointProvider = + object : NavigationPointProvider { + override fun getNavigationPoint( + position: PathPosition, + environmentContext: EnvironmentContext?, + ): NavigationPoint { + return object : NavigationPoint { + override fun isTraversable(): Boolean = true + override fun hasFloor(): Boolean = true + override fun getFloorLevel(): Double = 0.0 + override fun isClimbable(): Boolean = false + override fun isLiquid(): Boolean = false + } + } + } + private var heuristicWeights: HeuristicWeights = HeuristicWeights.DEFAULT_WEIGHTS + private var validationProcessors: List = emptyList() + private var costProcessors: List = emptyList() + private var neighborStrategy: INeighborStrategy = NeighborStrategies.VERTICAL_AND_HORIZONTAL + private var gridCellSize: Int = 12 + private var bloomFilterSize: Int = 1000 + private var bloomFilterFpp: Double = 0.01 + private var heuristicStrategy: IHeuristicStrategy = HeuristicStrategies.LINEAR + private var reopenClosedNodes: Boolean = false + + fun maxIterations(maxIterations: Int): PathfinderConfigurationBuilder { + this.maxIterations = maxIterations + return this + } + + fun maxLength(maxLength: Int): PathfinderConfigurationBuilder { + this.maxLength = maxLength + return this + } + + fun async(async: Boolean): PathfinderConfigurationBuilder { + this.async = async + return this + } + + fun fallback(allowingFallback: Boolean): PathfinderConfigurationBuilder { + this.fallback = allowingFallback + return this + } + + fun provider(provider: NavigationPointProvider): PathfinderConfigurationBuilder { + this.provider = provider + return this + } + + fun heuristicWeights(heuristicWeights: HeuristicWeights): PathfinderConfigurationBuilder { + this.heuristicWeights = heuristicWeights + return this + } + + fun nodeValidationProcessors( + validationProcessors: List, + ): PathfinderConfigurationBuilder { + this.validationProcessors = validationProcessors + return this + } + + fun nodeCostProcessors(costProcessors: List): PathfinderConfigurationBuilder { + this.costProcessors = costProcessors + return this + } + + fun neighborStrategy(neighborStrategy: INeighborStrategy): PathfinderConfigurationBuilder { + this.neighborStrategy = neighborStrategy + return this + } + + fun gridCellSize(gridCellSize: Int): PathfinderConfigurationBuilder { + this.gridCellSize = gridCellSize + return this + } + + fun bloomFilterSize(bloomFilterSize: Int): PathfinderConfigurationBuilder { + this.bloomFilterSize = bloomFilterSize + return this + } + + fun bloomFilterFpp(bloomFilterFpp: Double): PathfinderConfigurationBuilder { + this.bloomFilterFpp = bloomFilterFpp + return this + } + + fun heuristicStrategy(heuristicStrategy: IHeuristicStrategy): PathfinderConfigurationBuilder { + this.heuristicStrategy = heuristicStrategy + return this + } + + fun reopenClosedNodes(reopenClosedNodes: Boolean): PathfinderConfigurationBuilder { + this.reopenClosedNodes = reopenClosedNodes + return this + } + + fun build(): PathfinderConfiguration { + return PathfinderConfiguration( + maxIterations = this.maxIterations, + maxLength = this.maxLength, + async = this.async, + fallback = this.fallback, + provider = this.provider, + heuristicWeights = this.heuristicWeights, + validationProcessors = this.validationProcessors.toList(), + costProcessors = this.costProcessors.toList(), + neighborStrategy = this.neighborStrategy, + gridCellSize = this.gridCellSize, + bloomFilterSize = this.bloomFilterSize, + bloomFilterFpp = this.bloomFilterFpp, + heuristicStrategy = this.heuristicStrategy, + reopenClosedNodes = this.reopenClosedNodes + ) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/context/EnvironmentContext.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/context/EnvironmentContext.kt new file mode 100644 index 0000000..84012c6 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/context/EnvironmentContext.kt @@ -0,0 +1,3 @@ +package org.cobalt.api.pathfinder.pathing.context + +interface EnvironmentContext diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicContext.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicContext.kt new file mode 100644 index 0000000..ac28264 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicContext.kt @@ -0,0 +1,34 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +import org.cobalt.api.pathfinder.pathing.PathfindingProgress +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class HeuristicContext { + private val pathfindingProgress: PathfindingProgress + private val heuristicWeights: HeuristicWeights + + constructor( + position: PathPosition, + startPosition: PathPosition, + targetPosition: PathPosition, + heuristicWeights: HeuristicWeights, + ) { + this.pathfindingProgress = PathfindingProgress(startPosition, position, targetPosition) + this.heuristicWeights = heuristicWeights + } + + constructor(pathfindingProgress: PathfindingProgress, heuristicWeights: HeuristicWeights) { + this.pathfindingProgress = pathfindingProgress + this.heuristicWeights = heuristicWeights + } + + fun getPathfindingProgress(): PathfindingProgress = pathfindingProgress + + fun position(): PathPosition = pathfindingProgress.currentPosition() + + fun startPosition(): PathPosition = pathfindingProgress.startPosition() + + fun targetPosition(): PathPosition = pathfindingProgress.targetPosition() + + fun heuristicWeights(): HeuristicWeights = heuristicWeights +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicStrategies.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicStrategies.kt new file mode 100644 index 0000000..964af18 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicStrategies.kt @@ -0,0 +1,6 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +object HeuristicStrategies { + val LINEAR: IHeuristicStrategy = LinearHeuristicStrategy() + val SQUARED: IHeuristicStrategy = SquaredHeuristicStrategy() +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicWeights.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicWeights.kt new file mode 100644 index 0000000..b4c76bc --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/HeuristicWeights.kt @@ -0,0 +1,12 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +data class HeuristicWeights( + val manhattanWeight: Double, + val octileWeight: Double, + val perpendicularWeight: Double, + val heightWeight: Double, +) { + companion object { + val DEFAULT_WEIGHTS = HeuristicWeights(0.0, 1.0, 0.0, 0.0) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/IHeuristicStrategy.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/IHeuristicStrategy.kt new file mode 100644 index 0000000..6a8ee35 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/IHeuristicStrategy.kt @@ -0,0 +1,8 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface IHeuristicStrategy { + fun calculate(heuristicContext: HeuristicContext): Double + fun calculateTransitionCost(from: PathPosition, to: PathPosition): Double +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/InternalHeuristicUtils.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/InternalHeuristicUtils.kt new file mode 100644 index 0000000..f99a106 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/InternalHeuristicUtils.kt @@ -0,0 +1,50 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +import kotlin.math.sqrt +import org.cobalt.api.pathfinder.pathing.PathfindingProgress + +internal object InternalHeuristicUtils { + private const val EPSILON = 1e-9 + + fun calculatePerpendicularDistanceSq(progress: PathfindingProgress): Double { + val s = progress.startPosition() + val c = progress.currentPosition() + val t = progress.targetPosition() + + val sx = s.centeredX + val sy = s.centeredY + val sz = s.centeredZ + val cx = c.centeredX + val cy = c.centeredY + val cz = c.centeredZ + val tx = t.centeredX + val ty = t.centeredY + val tz = t.centeredZ + + val lineX = tx - sx + val lineY = ty - sy + val lineZ = tz - sz + val lineSq = lineX * lineX + lineY * lineY + lineZ * lineZ + + if (lineSq < EPSILON) { + val dx = cx - sx + val dy = cy - sy + val dz = cz - sz + return dx * dx + dy * dy + dz * dz + } + + val toX = cx - sx + val toY = cy - sy + val toZ = cz - sz + val crossX = toY * lineZ - toZ * lineY + val crossY = toZ * lineX - toX * lineZ + val crossZ = toX * lineY - toY * lineX + val crossSq = crossX * crossX + crossY * crossY + crossZ * crossZ + + return crossSq / lineSq + } + + fun calculatePerpendicularDistance(progress: PathfindingProgress): Double { + return sqrt(calculatePerpendicularDistanceSq(progress)) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/LinearHeuristicStrategy.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/LinearHeuristicStrategy.kt new file mode 100644 index 0000000..4e22dcc --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/LinearHeuristicStrategy.kt @@ -0,0 +1,69 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +import kotlin.math.abs +import kotlin.math.sqrt +import org.cobalt.api.pathfinder.pathing.calc.DistanceCalculator +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class LinearHeuristicStrategy : IHeuristicStrategy { + companion object { + private const val D1 = 1.0 + private val D2 = sqrt(2.0) + private val D3 = sqrt(3.0) + } + + private val perpendicularCalc = + DistanceCalculator { progress -> + InternalHeuristicUtils.calculatePerpendicularDistance(progress) + } + + private val octileCalc = + DistanceCalculator { progress -> + val dx = abs(progress.currentPosition().flooredX - progress.targetPosition().flooredX) + val dy = abs(progress.currentPosition().flooredY - progress.targetPosition().flooredY) + val dz = abs(progress.currentPosition().flooredZ - progress.targetPosition().flooredZ) + + val min = minOf(dx, dy, dz) + val max = maxOf(dx, dy, dz) + val mid = dx + dy + dz - min - max + + (D3 - D2) * min + (D2 - D1) * mid + D1 * max + } + + private val manhattanCalc = + DistanceCalculator { progress -> + val position = progress.currentPosition() + val target = progress.targetPosition() + + (abs(position.flooredX - target.flooredX) + + abs(position.flooredY - target.flooredY) + + abs(position.flooredZ - target.flooredZ)) + .toDouble() + } + + private val heightCalc = + DistanceCalculator { progress -> + val position = progress.currentPosition() + val target = progress.targetPosition() + + abs(position.flooredY - target.flooredY).toDouble() + } + + override fun calculate(context: HeuristicContext): Double { + val progress = context.getPathfindingProgress() + val weights = context.heuristicWeights() + + return manhattanCalc.calculate(progress)!! * weights.manhattanWeight + + octileCalc.calculate(progress)!! * weights.octileWeight + + perpendicularCalc.calculate(progress)!! * weights.perpendicularWeight + + heightCalc.calculate(progress)!! * weights.heightWeight + } + + override fun calculateTransitionCost(from: PathPosition, to: PathPosition): Double { + val dx = to.centeredX - from.centeredX + val dy = to.centeredY - from.centeredY + val dz = to.centeredZ - from.centeredZ + + return sqrt(dx * dx + dy * dy + dz * dz) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/SquaredHeuristicStrategy.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/SquaredHeuristicStrategy.kt new file mode 100644 index 0000000..a741a09 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/heuristic/SquaredHeuristicStrategy.kt @@ -0,0 +1,70 @@ +package org.cobalt.api.pathfinder.pathing.heuristic + +import kotlin.math.abs +import kotlin.math.sqrt +import org.cobalt.api.pathfinder.pathing.calc.DistanceCalculator +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class SquaredHeuristicStrategy : IHeuristicStrategy { + companion object { + private const val D1 = 1.0 + private val D2 = sqrt(2.0) + private val D3 = sqrt(3.0) + } + + private val perpendicularCalc = + DistanceCalculator { progress -> + InternalHeuristicUtils.calculatePerpendicularDistanceSq(progress) + } + + private val octileCalc = + DistanceCalculator { progress -> + val dx = abs(progress.currentPosition().flooredX - progress.targetPosition().flooredX) + val dy = abs(progress.currentPosition().flooredY - progress.targetPosition().flooredY) + val dz = abs(progress.currentPosition().flooredZ - progress.targetPosition().flooredZ) + + val min = minOf(dx, dy, dz) + val max = maxOf(dx, dy, dz) + val mid = dx + dy + dz - min - max + + val octile = (D3 - D2) * min + (D2 - D1) * mid + D1 * max + octile * octile + } + + private val manhattanCalc = + DistanceCalculator { progress -> + val c = progress.currentPosition() + val t = progress.targetPosition() + + val manhattan = + abs(c.flooredX - t.flooredX) + + abs(c.flooredY - t.flooredY) + + abs(c.flooredZ - t.flooredZ) + + (manhattan * manhattan).toDouble() + } + + private val heightCalc = + DistanceCalculator { progress -> + val dy = progress.currentPosition().flooredY - progress.targetPosition().flooredY + (dy * dy).toDouble() + } + + override fun calculate(context: HeuristicContext): Double { + val p = context.getPathfindingProgress() + val w = context.heuristicWeights() + + return manhattanCalc.calculate(p)!! * w.manhattanWeight + + octileCalc.calculate(p)!! * w.octileWeight + + perpendicularCalc.calculate(p)!! * w.perpendicularWeight + + heightCalc.calculate(p)!! * w.heightWeight + } + + override fun calculateTransitionCost(from: PathPosition, to: PathPosition): Double { + val dx = to.centeredX - from.centeredX + val dy = to.centeredY - from.centeredY + val dz = to.centeredZ - from.centeredZ + + return dx * dx + dy * dy + dz * dz + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfinderHook.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfinderHook.kt new file mode 100644 index 0000000..368e8b1 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfinderHook.kt @@ -0,0 +1,5 @@ +package org.cobalt.api.pathfinder.pathing.hook + +interface PathfinderHook { + fun onPathfindingStep(pathfindingContext: PathfindingContext) +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfindingContext.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfindingContext.kt new file mode 100644 index 0000000..b0a0e55 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/hook/PathfindingContext.kt @@ -0,0 +1,12 @@ +package org.cobalt.api.pathfinder.pathing.hook + +import org.cobalt.api.pathfinder.wrapper.Depth +import org.cobalt.api.pathfinder.wrapper.PathPosition + +data class PathfindingContext( + private val currentPosition: PathPosition, + private val depth: Depth, +) { + fun currentPosition(): PathPosition = currentPosition + fun getDepth(): Depth = depth +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Cost.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Cost.kt new file mode 100644 index 0000000..5bc03ce --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Cost.kt @@ -0,0 +1,14 @@ +package org.cobalt.api.pathfinder.pathing.processing + +data class Cost private constructor(val value: Double) { + companion object { + val ZERO = Cost(0.0) + + fun of(value: Double): Cost { + if (value.isNaN() || value < 0) { + throw IllegalArgumentException("Cost must be a positive number or 0") + } + return Cost(value) + } + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/CostProcessor.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/CostProcessor.kt new file mode 100644 index 0000000..56cd4e8 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/CostProcessor.kt @@ -0,0 +1,7 @@ +package org.cobalt.api.pathfinder.pathing.processing + +import org.cobalt.api.pathfinder.pathing.processing.context.EvaluationContext + +interface CostProcessor : Processor { + fun calculateCostContribution(context: EvaluationContext): Cost +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Processor.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Processor.kt new file mode 100644 index 0000000..4ff6951 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/Processor.kt @@ -0,0 +1,11 @@ +package org.cobalt.api.pathfinder.pathing.processing + +import org.cobalt.api.pathfinder.pathing.processing.context.SearchContext + +interface Processor { + fun initializeSearch(context: SearchContext) { + } + + fun finalizeSearch(context: SearchContext) { + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/ValidationProcessor.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/ValidationProcessor.kt new file mode 100644 index 0000000..bec934a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/ValidationProcessor.kt @@ -0,0 +1,7 @@ +package org.cobalt.api.pathfinder.pathing.processing + +import org.cobalt.api.pathfinder.pathing.processing.context.EvaluationContext + +interface ValidationProcessor : Processor { + fun isValid(context: EvaluationContext): Boolean +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/EvaluationContext.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/EvaluationContext.kt new file mode 100644 index 0000000..0ed124c --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/EvaluationContext.kt @@ -0,0 +1,41 @@ +package org.cobalt.api.pathfinder.pathing.processing.context + +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface EvaluationContext { + fun getCurrentPathPosition(): PathPosition + fun getPreviousPathPosition(): PathPosition? + fun getCurrentNodeDepth(): Int + fun getCurrentNodeHeuristicValue(): Double + fun getPathCostToPreviousPosition(): Double + fun getBaseTransitionCost(): Double + fun getSearchContext(): SearchContext + fun getGrandparentPathPosition(): PathPosition? + + fun getPathfinderConfiguration(): PathfinderConfiguration { + return getSearchContext().getPathfinderConfiguration() + } + + fun getNavigationPointProvider(): NavigationPointProvider { + return getSearchContext().getNavigationPointProvider() + } + + fun getSharedData(): MutableMap { + return getSearchContext().getSharedData() + } + + fun getStartPathPosition(): PathPosition { + return getSearchContext().getStartPathPosition() + } + + fun getTargetPathPosition(): PathPosition { + return getSearchContext().getTargetPathPosition() + } + + fun getEnvironmentContext(): EnvironmentContext? { + return getSearchContext().getEnvironmentContext() + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/SearchContext.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/SearchContext.kt new file mode 100644 index 0000000..bbe6fa3 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/context/SearchContext.kt @@ -0,0 +1,15 @@ +package org.cobalt.api.pathfinder.pathing.processing.context + +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface SearchContext { + fun getStartPathPosition(): PathPosition + fun getTargetPathPosition(): PathPosition + fun getPathfinderConfiguration(): PathfinderConfiguration + fun getNavigationPointProvider(): NavigationPointProvider + fun getSharedData(): MutableMap + fun getEnvironmentContext(): EnvironmentContext? +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/impl/MinecraftPathProcessor.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/impl/MinecraftPathProcessor.kt new file mode 100644 index 0000000..edfcb9c --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/processing/impl/MinecraftPathProcessor.kt @@ -0,0 +1,125 @@ +package org.cobalt.api.pathfinder.pathing.processing.impl + +import kotlin.math.sqrt +import net.minecraft.client.Minecraft +import net.minecraft.core.BlockPos +import org.cobalt.api.pathfinder.pathing.processing.Cost +import org.cobalt.api.pathfinder.pathing.processing.CostProcessor +import org.cobalt.api.pathfinder.pathing.processing.ValidationProcessor +import org.cobalt.api.pathfinder.pathing.processing.context.EvaluationContext + +/* + * most logic in this file is derived from minecraft code + * or writeups on pathfinding algorithms, if you want to help contribute + * id prefer for you to keep it the same idea or whatever, but if not + * please write a comment explaining WHY you did it that way. i dont like + * magic numbers that i cant understand. + */ +class MinecraftPathProcessor : CostProcessor, ValidationProcessor { + + private val mc: Minecraft = Minecraft.getInstance() + + companion object { + private const val DEFAULT_MOB_JUMP_HEIGHT = 1.125 // WalkNodeEvaluator + } + + override fun isValid(context: EvaluationContext): Boolean { + val provider = context.getSearchContext().getNavigationPointProvider() + val pos = context.getCurrentPathPosition() + val prev = context.getPreviousPathPosition() + val env = context.getSearchContext().getEnvironmentContext() + + val currentPoint = provider.getNavigationPoint(pos, env) + + if (!currentPoint.isTraversable()) return false + if (prev == null) return true + + val prevPoint = provider.getNavigationPoint(prev, env) + val dy = pos.y - prev.y + val dx = pos.flooredX - prev.flooredX + val dz = pos.flooredZ - prev.flooredZ + + if (dy > DEFAULT_MOB_JUMP_HEIGHT) return false + + if (Math.abs(dx) == 1 && Math.abs(dz) == 1) { + val corner1Pos = prev.add(dx.toDouble(), 0.0, 0.0) + val corner2Pos = prev.add(0.0, 0.0, dz.toDouble()) + val c1Point = provider.getNavigationPoint(corner1Pos, env) + val c2Point = provider.getNavigationPoint(corner2Pos, env) + + // node3.y <= node.y && node2.y <= node.y + if (!c1Point.isTraversable() || !c2Point.isTraversable()) return false + } + + return when { + dy < -0.5 -> true // falling + dy > 0.5 -> + prevPoint.hasFloor() || + currentPoint.isClimbable() || + currentPoint.isLiquid() // jumping/climbing + else -> + currentPoint.hasFloor() || + prevPoint.hasFloor() || + currentPoint.isClimbable() || + prevPoint.isClimbable() || + currentPoint.isLiquid() || + prevPoint.isLiquid() + } + } + + override fun calculateCostContribution(context: EvaluationContext): Cost { + val level = mc.level ?: return Cost.ZERO + val currentPos = context.getCurrentPathPosition() + val prevPos = context.getPreviousPathPosition() ?: return Cost.ZERO + val provider = context.getSearchContext().getNavigationPointProvider() + val env = context.getSearchContext().getEnvironmentContext() + + val currentPoint = provider.getNavigationPoint(currentPos, env) + val prevPoint = provider.getNavigationPoint(prevPos, env) + + val dy = currentPoint.getFloorLevel() - prevPoint.getFloorLevel() + var additionalCost = 0.0 + + if (dy > 0.1) { + additionalCost += 0.5 * dy + } else if (dy < -0.1) { + additionalCost += 0.1 * Math.abs(dy) + } + + val blockPos = BlockPos(currentPos.flooredX, currentPos.flooredY, currentPos.flooredZ) + + // i dont want it to like tight corners so more cost + var crampedPenalty = 0.0 + for (i in 2..3) { + if (level.getBlockState(blockPos.above(i)).canOcclude()) { + crampedPenalty += 0.1 / i.toDouble() + } + } + if (level.getBlockState(blockPos.west()).canOcclude() || + level.getBlockState(blockPos.east()).canOcclude() || + level.getBlockState(blockPos.north()).canOcclude() || + level.getBlockState(blockPos.south()).canOcclude() + ) { + crampedPenalty += 0.05 + } + additionalCost += crampedPenalty + + // just make stuff smoother no more zigzags + val gpPos = context.getGrandparentPathPosition() + if (gpPos != null) { + val v1x = prevPos.x - gpPos.x + val v1z = prevPos.z - gpPos.z + val v2x = currentPos.x - prevPos.x + val v2z = currentPos.z - prevPos.z + val dot = v1x * v2x + v1z * v2z + val mag1 = sqrt(v1x * v1x + v1z * v1z) + val mag2 = sqrt(v2x * v2x + v2z * v2z) + if (mag1 > 0.1 && mag2 > 0.1) { + val normalizedDot = dot / (mag1 * mag2) + if (normalizedDot < 0.99) additionalCost += 0.05 + } + } + + return Cost.of(additionalCost) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/Path.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/Path.kt new file mode 100644 index 0000000..69ce867 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/Path.kt @@ -0,0 +1,10 @@ +package org.cobalt.api.pathfinder.pathing.result + +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface Path : Iterable { + fun length(): Int + fun getStart(): PathPosition + fun getEnd(): PathPosition + fun collect(): Collection +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathState.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathState.kt new file mode 100644 index 0000000..b8a4c18 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathState.kt @@ -0,0 +1,10 @@ +package org.cobalt.api.pathfinder.pathing.result + +enum class PathState { + ABORTED, + FOUND, + FAILED, + FALLBACK, + LENGTH_LIMITED, + MAX_ITERATIONS_REACHED +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathfinderResult.kt b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathfinderResult.kt new file mode 100644 index 0000000..53e5814 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/pathing/result/PathfinderResult.kt @@ -0,0 +1,9 @@ +package org.cobalt.api.pathfinder.pathing.result + +interface PathfinderResult { + fun successful(): Boolean + fun hasFailed(): Boolean + fun hasFallenBack(): Boolean + fun getPathState(): PathState + fun getPath(): Path +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPoint.kt b/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPoint.kt new file mode 100644 index 0000000..1720983 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPoint.kt @@ -0,0 +1,9 @@ +package org.cobalt.api.pathfinder.provider + +interface NavigationPoint { + fun isTraversable(): Boolean + fun hasFloor(): Boolean + fun getFloorLevel(): Double + fun isClimbable(): Boolean + fun isLiquid(): Boolean +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPointProvider.kt b/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPointProvider.kt new file mode 100644 index 0000000..ef354d3 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/provider/NavigationPointProvider.kt @@ -0,0 +1,12 @@ +package org.cobalt.api.pathfinder.provider + +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.wrapper.PathPosition + +interface NavigationPointProvider { + fun getNavigationPoint(position: PathPosition): NavigationPoint { + return getNavigationPoint(position, null) + } + + fun getNavigationPoint(position: PathPosition, environmentContext: EnvironmentContext?): NavigationPoint +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/provider/impl/MinecraftNavigationProvider.kt b/src/main/kotlin/org/cobalt/api/pathfinder/provider/impl/MinecraftNavigationProvider.kt new file mode 100644 index 0000000..354bd6a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/provider/impl/MinecraftNavigationProvider.kt @@ -0,0 +1,142 @@ +package org.cobalt.api.pathfinder.provider.impl + +import net.minecraft.client.Minecraft +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction.Axis +import net.minecraft.tags.BlockTags +import net.minecraft.tags.FluidTags +import net.minecraft.world.level.Level +import net.minecraft.world.level.block.* +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.pathfinder.PathComputationType +import net.minecraft.world.phys.shapes.CollisionContext +import org.cobalt.api.pathfinder.pathing.context.EnvironmentContext +import org.cobalt.api.pathfinder.provider.NavigationPoint +import org.cobalt.api.pathfinder.provider.NavigationPointProvider +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class MinecraftNavigationProvider : NavigationPointProvider { + + private val mc: Minecraft = Minecraft.getInstance() + + override fun getNavigationPoint( + position: PathPosition, + environmentContext: EnvironmentContext?, + ): NavigationPoint { + val level = + mc.level + ?: return object : NavigationPoint { + override fun isTraversable() = false + override fun hasFloor() = false + override fun getFloorLevel() = 0.0 + override fun isClimbable() = false + override fun isLiquid() = false + } + + val x = position.flooredX + val y = position.flooredY + val z = position.flooredZ + val blockPos = BlockPos(x, y, z) + + val feetState = level.getBlockState(blockPos) + val headState = level.getBlockState(blockPos.above()) + val belowState = level.getBlockState(blockPos.below()) + + val canPassFeetVal = canWalkThrough(level, feetState, blockPos) + val canPassHeadVal = canWalkThrough(level, headState, blockPos.above()) + val hasStableFloorVal = canWalkOn(level, belowState, blockPos.below()) + val floorLevelVal = calculateFloorLevel(level, blockPos) + val isClimbingVal = feetState.block is LadderBlock || feetState.block is VineBlock + val isLiquidVal = !feetState.fluidState.isEmpty + + return object : NavigationPoint { + override fun isTraversable(): Boolean = canPassFeetVal && canPassHeadVal + override fun hasFloor(): Boolean = hasStableFloorVal + override fun getFloorLevel(): Double = floorLevelVal + override fun isClimbable(): Boolean = isClimbingVal + override fun isLiquid(): Boolean = isLiquidVal + } + } + + private fun canWalkThrough(level: Level, state: BlockState, pos: BlockPos): Boolean { + if (state.isAir) return true + + if (state.`is`(BlockTags.TRAPDOORS) || + state.`is`(Blocks.LILY_PAD) || + state.`is`(Blocks.BIG_DRIPLEAF) + ) { + return true + } + + if (state.`is`(Blocks.POWDER_SNOW) || + state.`is`(Blocks.CACTUS) || + state.`is`(Blocks.SWEET_BERRY_BUSH) || + state.`is`(Blocks.HONEY_BLOCK) || + state.`is`(Blocks.COCOA) || + state.`is`(Blocks.WITHER_ROSE) || + state.`is`(Blocks.POINTED_DRIPSTONE) + ) { + return true + } + + val block = state.block + if (block is DoorBlock) { + return if (state.getValue(DoorBlock.OPEN)) true else block.type().canOpenByHand() + } + + if (block is FenceGateBlock) { + return state.getValue(FenceGateBlock.OPEN) + } + + if (block is BaseRailBlock || block is LeavesBlock) { + return true + } + + if (state.`is`(BlockTags.FENCES) || state.`is`(BlockTags.WALLS)) { + return false + } + + return state.isPathfindable(PathComputationType.LAND) || state.fluidState.`is`(FluidTags.WATER) + } + + private fun canWalkOn(level: Level, state: BlockState, pos: BlockPos): Boolean { + val block = state.block + if (state.isCollisionShapeFullBlock(level, pos) && + block != Blocks.MAGMA_BLOCK && + block != Blocks.BUBBLE_COLUMN && + block != Blocks.HONEY_BLOCK + ) { + return true + } + + return block is AzaleaBlock || + block is LadderBlock || + block is VineBlock || + block == Blocks.FARMLAND || + block == Blocks.DIRT_PATH || + block == Blocks.SOUL_SAND || + block == Blocks.CHEST || + block == Blocks.ENDER_CHEST || + block == Blocks.GLASS || + block is StairBlock || + block is SlabBlock || + block is BaseRailBlock || + !state.fluidState.isEmpty + } + + private fun calculateFloorLevel(level: Level, pos: BlockPos): Double { + val state = level.getFluidState(pos) + if (state.`is`(FluidTags.WATER)) { + return pos.y.toDouble() + 0.5 + } + + val belowPos = pos.below() + val belowState = level.getBlockState(belowPos) + val shape = belowState.getCollisionShape(level, belowPos, CollisionContext.empty()) + return if (shape.isEmpty) { + belowPos.y.toDouble() + } else { + belowPos.y.toDouble() + shape.max(Axis.Y) + } + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/result/PathImpl.kt b/src/main/kotlin/org/cobalt/api/pathfinder/result/PathImpl.kt new file mode 100644 index 0000000..8a24e46 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/result/PathImpl.kt @@ -0,0 +1,21 @@ +package org.cobalt.api.pathfinder.result + +import org.cobalt.api.pathfinder.pathing.result.Path +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class PathImpl( + private val start: PathPosition, + private val end: PathPosition, + private val positions: Collection, +) : Path { + + override fun getStart(): PathPosition = start + + override fun getEnd(): PathPosition = end + + override fun iterator(): Iterator = positions.iterator() + + override fun length(): Int = positions.size + + override fun collect(): Collection = positions.toList() +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/result/PathfinderResultImpl.kt b/src/main/kotlin/org/cobalt/api/pathfinder/result/PathfinderResultImpl.kt new file mode 100644 index 0000000..90f2d7f --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/result/PathfinderResultImpl.kt @@ -0,0 +1,29 @@ +package org.cobalt.api.pathfinder.result + +import org.cobalt.api.pathfinder.pathing.result.Path +import org.cobalt.api.pathfinder.pathing.result.PathState +import org.cobalt.api.pathfinder.pathing.result.PathfinderResult + +class PathfinderResultImpl( + private val pathState: PathState, + private val path: Path, +) : PathfinderResult { + + override fun successful(): Boolean { + return pathState == PathState.FOUND || + pathState == PathState.FALLBACK || + pathState == PathState.MAX_ITERATIONS_REACHED + } + + override fun hasFailed(): Boolean { + return pathState == PathState.FAILED || + pathState == PathState.ABORTED || + pathState == PathState.LENGTH_LIMITED + } + + override fun hasFallenBack(): Boolean = pathState == PathState.FALLBACK + + override fun getPathState(): PathState = pathState + + override fun getPath(): Path = path +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/util/GridRegionData.kt b/src/main/kotlin/org/cobalt/api/pathfinder/util/GridRegionData.kt new file mode 100644 index 0000000..78f3697 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/util/GridRegionData.kt @@ -0,0 +1,33 @@ +package org.cobalt.api.pathfinder.util + +import com.google.common.hash.BloomFilter +import com.google.common.hash.Funnel +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import it.unimi.dsi.fastutil.longs.LongSet +import org.cobalt.api.pathfinder.pathing.configuration.PathfinderConfiguration +import org.cobalt.api.pathfinder.wrapper.PathPosition + +class GridRegionData { + private val bloomFilter: BloomFilter + private val regionalExaminedPositions: LongSet + + constructor(bloomFilterSize: Int, bloomFilterFpp: Double) { + val pathPositionFunnel = + Funnel { pathPosition, into -> + into.putInt(pathPosition.flooredX) + .putInt(pathPosition.flooredY) + .putInt(pathPosition.flooredZ) + } + + bloomFilter = BloomFilter.create(pathPositionFunnel, bloomFilterSize, bloomFilterFpp) + this.regionalExaminedPositions = LongOpenHashSet() + } + + constructor( + configuration: PathfinderConfiguration, + ) : this(configuration.bloomFilterSize, configuration.bloomFilterFpp) + + fun getBloomFilter(): BloomFilter = bloomFilter + + fun getRegionalExaminedPositions(): LongSet = regionalExaminedPositions +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/util/RegionKey.kt b/src/main/kotlin/org/cobalt/api/pathfinder/util/RegionKey.kt new file mode 100644 index 0000000..49cf952 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/util/RegionKey.kt @@ -0,0 +1,18 @@ +package org.cobalt.api.pathfinder.util + +import org.cobalt.api.pathfinder.wrapper.PathPosition + +object RegionKey { + private const val MASK_Y = 0xFFFL // 12 Bit + private const val MASK_XZ = 0x3FFFFFFL // 26 Bit + private const val SHIFT_Z = 12 + private const val SHIFT_X = 38 // 12 + 26 + + fun pack(pos: PathPosition): Long = pack(pos.flooredX, pos.flooredY, pos.flooredZ) + + fun pack(x: Int, y: Int, z: Int): Long { + return ((x.toLong() and MASK_XZ) shl SHIFT_X) or + ((z.toLong() and MASK_XZ) shl SHIFT_Z) or + (y.toLong() and MASK_Y) + } +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/Depth.kt b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/Depth.kt new file mode 100644 index 0000000..5aac33f --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/Depth.kt @@ -0,0 +1,14 @@ +package org.cobalt.api.pathfinder.wrapper + +@ConsistentCopyVisibility +data class Depth private constructor(private var value: Int) { + companion object { + fun of(value: Int): Depth = Depth(value) + } + + fun increment() { + value++ + } + + fun getValue(): Int = value +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathPosition.kt b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathPosition.kt new file mode 100644 index 0000000..44d567a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathPosition.kt @@ -0,0 +1,67 @@ +package org.cobalt.api.pathfinder.wrapper + +import kotlin.math.sqrt +import net.minecraft.util.Mth + +data class PathPosition(val x: Double, val y: Double, val z: Double) { + + val flooredX: Int + get() = Mth.floor(x) + val flooredY: Int + get() = Mth.floor(y) + val flooredZ: Int + get() = Mth.floor(z) + + val centeredX: Double + get() = flooredX + 0.5 + val centeredY: Double + get() = flooredY + 0.5 + val centeredZ: Double + get() = flooredZ + 0.5 + + fun distanceSquared(other: PathPosition): Double { + return Mth.square(x - other.x) + Mth.square(y - other.y) + Mth.square(z - other.z) + } + + fun distance(other: PathPosition): Double = sqrt(distanceSquared(other)) + + fun setX(x: Double): PathPosition = copy(x = x) + fun setY(y: Double): PathPosition = copy(y = y) + fun setZ(z: Double): PathPosition = copy(z = z) + + fun add(x: Double, y: Double, z: Double): PathPosition = + PathPosition(this.x + x, this.y + y, this.z + z) + + fun add(vector: PathVector): PathPosition = add(vector.x, vector.y, vector.z) + + fun subtract(x: Double, y: Double, z: Double): PathPosition = + PathPosition(this.x - x, this.y - y, this.z - z) + + fun subtract(vector: PathVector): PathPosition = subtract(vector.x, vector.y, vector.z) + + fun toVector(): PathVector = PathVector(x, y, z) + + fun floor(): PathPosition = + PathPosition(flooredX.toDouble(), flooredY.toDouble(), flooredZ.toDouble()) + + fun mid(): PathPosition = PathPosition(flooredX + 0.5, flooredY + 0.5, flooredZ + 0.5) + + fun midPoint(end: PathPosition): PathPosition { + return PathPosition((x + end.x) / 2, (y + end.y) / 2, (z + end.z) / 2) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PathPosition) return false + return flooredX == other.flooredX && flooredY == other.flooredY && flooredZ == other.flooredZ + } + + override fun hashCode(): Int { + var result = flooredX + result = 31 * result + flooredY + result = 31 * result + flooredZ + return result + } + + override fun toString(): String = "PathPosition(x=$x, y=$y, z=$z)" +} diff --git a/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathVector.kt b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathVector.kt new file mode 100644 index 0000000..faa99c7 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/pathfinder/wrapper/PathVector.kt @@ -0,0 +1,48 @@ +package org.cobalt.api.pathfinder.wrapper + +import kotlin.math.sqrt +import net.minecraft.util.Mth + +data class PathVector(val x: Double, val y: Double, val z: Double) { + + companion object { + fun computeDistance(A: PathVector, B: PathVector, C: PathVector): Double { + val d = C.subtract(B).divide(C.distance(B)) + val v = A.subtract(B) + val t = v.dot(d) + val P = B.add(d.multiply(t)) + return P.distance(A) + } + } + + fun dot(other: PathVector): Double = x * other.x + y * other.y + z * other.z + + fun length(): Double = sqrt(Mth.square(x) + Mth.square(y) + Mth.square(z)) + + fun distance(other: PathVector): Double = + sqrt(Mth.square(x - other.x) + Mth.square(y - other.y) + Mth.square(z - other.z)) + + fun setX(x: Double): PathVector = copy(x = x) + fun setY(y: Double): PathVector = copy(y = y) + fun setZ(z: Double): PathVector = copy(z = z) + + fun subtract(other: PathVector): PathVector = PathVector(x - other.x, y - other.y, z - other.z) + + fun multiply(value: Double): PathVector = PathVector(x * value, y * value, z * value) + + fun normalize(): PathVector { + val magnitude = length() + return PathVector(x / magnitude, y / magnitude, z / magnitude) + } + + fun divide(value: Double): PathVector = PathVector(x / value, y / value, z / value) + + fun add(other: PathVector): PathVector = PathVector(x + other.x, y + other.y, z + other.z) + + fun getCrossProduct(o: PathVector): PathVector { + val crossX = y * o.z - o.y * z + val crossY = z * o.x - o.z * x + val crossZ = x * o.y - o.x * y + return PathVector(crossX, crossY, crossZ) + } +} diff --git a/src/main/kotlin/org/cobalt/internal/command/MainCommand.kt b/src/main/kotlin/org/cobalt/internal/command/MainCommand.kt index 865305a..4d2e50c 100644 --- a/src/main/kotlin/org/cobalt/internal/command/MainCommand.kt +++ b/src/main/kotlin/org/cobalt/internal/command/MainCommand.kt @@ -5,16 +5,14 @@ import org.cobalt.api.command.Command import org.cobalt.api.command.annotation.DefaultHandler import org.cobalt.api.command.annotation.SubCommand import org.cobalt.api.notification.NotificationManager +import org.cobalt.api.pathfinder.PathExecutor import org.cobalt.api.rotation.EasingType import org.cobalt.api.rotation.RotationExecutor import org.cobalt.api.rotation.strategy.TimedEaseStrategy import org.cobalt.api.util.helper.Rotation import org.cobalt.internal.ui.screen.UIConfig -internal object MainCommand : Command( - name = "cobalt", - aliases = arrayOf("cb") -) { +internal object MainCommand : Command(name = "cobalt", aliases = arrayOf("cb")) { @DefaultHandler fun main() { @@ -24,12 +22,12 @@ internal object MainCommand : Command( @SubCommand fun rotate(yaw: Double, pitch: Double, duration: Int) { RotationExecutor.rotateTo( - Rotation(yaw.toFloat(), pitch.toFloat()), - TimedEaseStrategy( - yawEasing = EasingType.EASE_OUT_EXPO, - pitchEasing = EasingType.EASE_OUT_EXPO, - duration = duration.toLong() - ) + Rotation(yaw.toFloat(), pitch.toFloat()), + TimedEaseStrategy( + yawEasing = EasingType.EASE_OUT_EXPO, + pitchEasing = EasingType.EASE_OUT_EXPO, + duration = duration.toLong() + ) ) } @@ -39,18 +37,27 @@ internal object MainCommand : Command( val pitch = Random.nextFloat() * 180f - 90f RotationExecutor.rotateTo( - Rotation(yaw, pitch), - TimedEaseStrategy( - yawEasing = EasingType.EASE_OUT_EXPO, - pitchEasing = EasingType.EASE_OUT_EXPO, - duration = 400L - ) + Rotation(yaw, pitch), + TimedEaseStrategy( + yawEasing = EasingType.EASE_OUT_EXPO, + pitchEasing = EasingType.EASE_OUT_EXPO, + duration = 400L + ) ) } + @SubCommand + fun start(x: Double, y: Double, z: Double) { + PathExecutor.start(x, y, z) + } + + @SubCommand + fun stop() { + PathExecutor.stop() + } + @SubCommand fun notification(title: String, description: String) { NotificationManager.sendNotification(title, description) } - }