From 59e93a438db178a9811624a2bddaf8f623d5a382 Mon Sep 17 00:00:00 2001 From: elb Date: Tue, 25 Feb 2025 08:48:48 -0500 Subject: [PATCH] #650 merge Merge @xVoidByte pr with it. Lets make them compatible. --- internal/action/clear_area.go | 2 + internal/action/move.go | 10 +- internal/action/step/move.go | 24 +++- internal/context/context.go | 5 +- internal/game/memory_reader.go | 10 +- internal/pather/astar/astar.go | 197 ++++++++++++++++++++------------ internal/pather/path.go | 106 ++++++++++++++++- internal/pather/path_finder.go | 202 +++++++++++++++++++++------------ internal/pather/utils.go | 61 +++++++++- internal/run/pindleskin.go | 4 +- 10 files changed, 453 insertions(+), 168 deletions(-) diff --git a/internal/action/clear_area.go b/internal/action/clear_area.go index e91fd5532..d2fe4da83 100644 --- a/internal/action/clear_area.go +++ b/internal/action/clear_area.go @@ -29,6 +29,8 @@ func ClearAreaAroundPosition(pos data.Position, radius int, filter data.MonsterF return 0, false }, nil) } + +// TODO handle repath when stuck with hidden stash/ shrines. this is root of hidden stash stuck issue in chaos func ClearThroughPath(pos data.Position, radius int, filter data.MonsterFilter) error { ctx := context.Get() lastMovement := false diff --git a/internal/action/move.go b/internal/action/move.go index 5682883c2..33d2c3a59 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -49,6 +49,9 @@ func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error { return fmt.Errorf("area sync timeout - expected: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area) } +//TODO something is off with Diablo action.MoveToArea(area.ChaosSanctuary) , the transition between movetoarea and clearthrough path makes +// bot telestomp until timeout before resuming with the run + func MoveToArea(dst area.ID) error { ctx := context.Get() ctx.SetLastAction("MoveToArea") @@ -238,6 +241,10 @@ func MoveTo(toFunc func() (data.Position, bool)) error { return fmt.Errorf("path could not be calculated") } + //TODO if character detects monster on other side of the wall and we are past door detection + // it will ignore doors and try to move directly to it. Either line of sight fail or constantly check for doors intersects + + // Walkable logic // Handle obstacles in current area if !ctx.Data.CanTeleport() { // Handle doors in path @@ -282,7 +289,8 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } // Continue moving - WaitForAllMembersWhenLeveling() + + // WaitForAllMembersWhenLeveling is breaking walkable logic, lets not use it for now. if lastMovement { return nil diff --git a/internal/action/step/move.go b/internal/action/step/move.go index 55543db6d..cc91af874 100644 --- a/internal/action/step/move.go +++ b/internal/action/step/move.go @@ -22,10 +22,28 @@ func WithDistanceToFinish(distance int) MoveOption { } } +// TODO town pathing stucks? + func MoveTo(dest data.Position, options ...MoveOption) error { ctx := context.Get() ctx.SetLastStep("MoveTo") + //Todo if we add this it will make walkable character move like a broken disk. However is helping with waypoint interaction + // reducing walkduration seems to help with this issue. + + /* defer func() { + for { + switch ctx.Data.PlayerUnit.Mode { + case mode.Walking, mode.WalkingInTown, mode.Running, mode.CastingSkill: + utils.Sleep(50) + // ctx.RefreshGameData() + continue + default: + return + } + } + }()*/ + const ( refreshInterval = 200 * time.Millisecond timeout = 30 * time.Second @@ -33,6 +51,8 @@ func MoveTo(dest data.Position, options ...MoveOption) error { startedAt := time.Now() + //TODO use the pathcache directly from path.go ? + // Initialize or reuse path cache var pathCache *context.PathCache if ctx.CurrentGame.PathCache != nil && @@ -63,7 +83,7 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } // Add some delay between clicks to let the character move to destination - walkDuration := utils.RandomDurationMs(600, 1200) + walkDuration := utils.RandomDurationMs(700, 900) for { time.Sleep(50 * time.Millisecond) @@ -93,6 +113,8 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } else if pathCache.Path == nil || !IsPathValid(currentPos, pathCache) || (distanceToDest <= 15 && distanceToDest > pathCache.DistanceToFinish) { + //TODO this looks like the telestomp issue, IsSamePosition is true but it never enter this condition, need something else to force refresh + // Only recalculate when truly needed path, _, found := ctx.PathFinder.GetPath(dest) if !found { diff --git a/internal/context/context.go b/internal/context/context.go index 78167119c..c8a4c914e 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -63,11 +63,10 @@ type PathCache struct { Path []data.Position DestPosition data.Position StartPosition data.Position - LastRun time.Time - LastCheck time.Time PreviousPosition data.Position + LastCheck time.Time + LastRun time.Time DistanceToFinish int - LastMoveDistance int } type CurrentGameHelper struct { diff --git a/internal/game/memory_reader.go b/internal/game/memory_reader.go index eae501d75..a863bd594 100644 --- a/internal/game/memory_reader.go +++ b/internal/game/memory_reader.go @@ -66,10 +66,11 @@ func (gd *MemoryReader) FetchMapData() error { return fmt.Errorf("error fetching map data: %w", err) } - areas := make(map[area.ID]AreaData) + areas := make(map[area.ID]AreaData, len(mapData)) // Pre-allocate map size var mu sync.Mutex g := errgroup.Group{} for _, lvl := range mapData { + lvl := lvl // Capture local copy for go-routine g.Go(func() error { cg := lvl.CollisionGrid() resultGrid := make([][]CollisionType, lvl.Size.Height) @@ -77,9 +78,10 @@ func (gd *MemoryReader) FetchMapData() error { resultGrid[i] = make([]CollisionType, lvl.Size.Width) } - for y := 0; y < lvl.Size.Height; y++ { - for x := 0; x < lvl.Size.Width; x++ { - if cg[y][x] { + // Optimized grid population using range iteration + for y, row := range cg { + for x, val := range row { + if val { resultGrid[y][x] = CollisionTypeWalkable } else { resultGrid[y][x] = CollisionTypeNonWalkable diff --git a/internal/pather/astar/astar.go b/internal/pather/astar/astar.go index 9a8cc2fcf..c660bec71 100644 --- a/internal/pather/astar/astar.go +++ b/internal/pather/astar/astar.go @@ -9,16 +9,27 @@ import ( "github.com/hectorgimenez/koolo/internal/game" ) -var directions = []data.Position{ - {0, 1}, // Down - {1, 0}, // Right - {0, -1}, // Up - {-1, 0}, // Left - {1, 1}, // Down-Right (Southeast) - {-1, 1}, // Down-Left (Southwest) - {1, -1}, // Up-Right (Northeast) - {-1, -1}, // Up-Left (Northwest) -} +var ( + // Cardinal directions for movement in narrow spaces + cardinalDirections = []data.Position{ + {0, 1}, // Down + {1, 0}, // Right + {0, -1}, // Up + {-1, 0}, // Left + } + + // All possible movement directions including diagonals + allDirections = []data.Position{ + {0, 1}, // Down + {1, 0}, // Right + {0, -1}, // Up + {-1, 0}, // Left + {1, 1}, // Down-Right (Southeast) + {-1, 1}, // Down-Left (Southwest) + {1, -1}, // Up-Right (Northeast) + {-1, -1}, // Up-Left (Northwest) + } +) type Node struct { data.Position @@ -27,16 +38,12 @@ type Node struct { Index int } -func direction(from, to data.Position) (dx, dy int) { - dx = to.X - from.X - dy = to.Y - from.Y - return -} - -func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]data.Position, int, bool) { +// Find the shortest path between two points using A* algorithm with optimizations for specific game areas +func CalculatePath(g *game.Grid, areaID area.ID, start, goal data.Position, teleport bool) ([]data.Position, int, bool) { pq := make(PriorityQueue, 0) heap.Init(&pq) + // Use a 2D slice to store the cost of each node costSoFar := make([][]int, g.Width) cameFrom := make([][]data.Position, g.Width) for i := range costSoFar { @@ -52,41 +59,36 @@ func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]dat costSoFar[start.X][start.Y] = 0 neighbors := make([]data.Position, 0, 8) + nodesExplored := 0 + + // Use appropriate directions based on map type + directions := allDirections + if IsNarrowMap(areaID) { + // Restrict to cardinal directions for narrow maps to prevent pathing issues + directions = cardinalDirections + } for pq.Len() > 0 { current := heap.Pop(&pq).(*Node) + nodesExplored++ + // Early exit if we reach the goal if current.Position == goal { - var path []data.Position - for p := goal; p != start; p = cameFrom[p.X][p.Y] { - path = append([]data.Position{p}, path...) - } - path = append([]data.Position{start}, path...) - return path, len(path), true + // Validate and smooth path before returning + return validateAndSmoothPath(reconstructPath(cameFrom, start, goal)), nodesExplored, true } - updateNeighbors(g, current, &neighbors) - + updateNeighbors(g, current, directions, &neighbors, teleport) for _, neighbor := range neighbors { - - var tileCost int - tileType := g.CollisionGrid[neighbor.Y][neighbor.X] - - if area.IsTown() { - // Extra cost near edges in town - if neighbor.X <= 2 || neighbor.X >= g.Width-2 || - neighbor.Y <= 2 || neighbor.Y >= g.Height-2 { - tileCost += 20 - } - } else { - tileCost = getCost(tileType, area) + tileCost := getCost(g.CollisionGrid[neighbor.Y][neighbor.X], teleport) + if tileCost == math.MaxInt32 { + continue // Skip completely blocked tiles } newCost := costSoFar[current.X][current.Y] + tileCost - if newCost < costSoFar[neighbor.X][neighbor.Y] { costSoFar[neighbor.X][neighbor.Y] = newCost - priority := newCost + int(0.5*float64(heuristic(neighbor, goal))) + priority := newCost + heuristic(neighbor, goal) heap.Push(&pq, &Node{Position: neighbor, Cost: newCost, Priority: priority}) cameFrom[neighbor.X][neighbor.Y] = current.Position } @@ -96,53 +98,106 @@ func CalculatePath(g *game.Grid, area area.ID, start, goal data.Position) ([]dat return nil, 0, false } -// Get walkable neighbors of a given node -func updateNeighbors(grid *game.Grid, node *Node, neighbors *[]data.Position) { - *neighbors = (*neighbors)[:0] +// Builds the final path from cameFrom logic +func reconstructPath(cameFrom [][]data.Position, start, goal data.Position) []data.Position { + var path []data.Position + for p := goal; p != start; p = cameFrom[p.X][p.Y] { + path = append([]data.Position{p}, path...) + } + return append([]data.Position{start}, path...) +} + +// validateAndSmoothPath ensures teleport paths skip unwalkable middle nodes +func validateAndSmoothPath(path []data.Position) []data.Position { + if len(path) < 3 { + return path + } + + smoothed := make([]data.Position, 0, len(path)) + smoothed = append(smoothed, path[0]) + // Remove unnecessary intermediate nodes that can be skipped + for i := 1; i < len(path)-1; i++ { + if canSkipNode(path[i-1], path[i+1]) { + continue + } + smoothed = append(smoothed, path[i]) + } + + smoothed = append(smoothed, path[len(path)-1]) + return smoothed +} + +// canSkipNode checks if two nodes are adjacent enough to skip intermediate nodes +func canSkipNode(prev, next data.Position) bool { + dx := abs(next.X - prev.X) + dy := abs(next.Y - prev.Y) + return dx <= 1 && dy <= 1 +} + +// Find valid adjacent nodes considering collision detection +func updateNeighbors(grid *game.Grid, node *Node, directions []data.Position, neighbors *[]data.Position, teleport bool) { + *neighbors = (*neighbors)[:0] x, y := node.X, node.Y - gridWidth, gridHeight := grid.Width, grid.Height + // Check all possible directions for valid neighbors for _, d := range directions { newX, newY := x+d.X, y+d.Y - // Check if the new neighbor is within grid bounds - if newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight { + if newX >= 0 && newX < grid.Width && newY >= 0 && newY < grid.Height { + tileType := grid.CollisionGrid[newY][newX] + // Include non-walkable nodes when teleporting + if !teleport && tileType == game.CollisionTypeNonWalkable { + continue // Skip non-walkable tiles when not teleporting + } *neighbors = append(*neighbors, data.Position{X: newX, Y: newY}) } } } -func getCost(tileType game.CollisionType, currentArea area.ID) int { - if currentArea.IsTown() { +// Define movement cost for different collision types with teleport consideration +var tileCost = map[game.CollisionType]int{ + game.CollisionTypeWalkable: 1, // Walkable + game.CollisionTypeMonster: 16, // Monster blocking penalty + game.CollisionTypeObject: 4, // Soft blocker (barrels, etc) + game.CollisionTypeLowPriority: 20, // Preferred walkable areas + game.CollisionTypeNonWalkable: math.MaxInt32, // Completely block non-walkable +} + +// getCost returns movement cost for tile type with teleport adjustments +func getCost(tileType game.CollisionType, teleport bool) int { + if teleport { switch tileType { - case game.CollisionTypeWalkable: - return 1 - case game.CollisionTypeMonster: - return 30 // Higher cost in town + case game.CollisionTypeNonWalkable: + return 20 // Reduced cost for teleport through walls case game.CollisionTypeObject: - return 15 // Higher cost in town - case game.CollisionTypeLowPriority: - return 40 // Much higher in town - default: - return math.MaxInt32 + return 10 // Lower penalty for objects when teleporting } } + return tileCost[tileType] +} - switch tileType { - case game.CollisionTypeWalkable: - return 1 - case game.CollisionTypeMonster: - return 16 - case game.CollisionTypeObject: - return 4 - case game.CollisionTypeLowPriority: - return 20 - default: - return math.MaxInt32 +// Use heuristic distance for faster calculations +func heuristic(a, b data.Position) int { + dx := abs(a.X - b.X) + dy := abs(a.Y - b.Y) + return dx + dy +} + +func abs(x int) int { + if x < 0 { + return -x } + return x } -func heuristic(a, b data.Position) int { - dx := math.Abs(float64(a.X - b.X)) - dy := math.Abs(float64(a.Y - b.Y)) - return int(dx + dy + (math.Sqrt(2)-2)*math.Min(dx, dy)) + +// Identify areas that require restricted movement directions to prevent pathfinding issues in tight spaces +func IsNarrowMap(a area.ID) bool { + switch a { + case area.TowerCellarLevel2, area.TowerCellarLevel3, area.TowerCellarLevel4, + area.MaggotLairLevel1, area.MaggotLairLevel2, area.MaggotLairLevel3, + area.ArcaneSanctuary, area.ClawViperTempleLevel2, area.RiverOfFlame, + area.ChaosSanctuary: + return true + } + return false } diff --git a/internal/pather/path.go b/internal/pather/path.go index fe32bb4fb..2c77d2edb 100644 --- a/internal/pather/path.go +++ b/internal/pather/path.go @@ -1,32 +1,127 @@ package pather import ( + "fmt" + "math" + "sync" + "time" + "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/data/area" "github.com/hectorgimenez/koolo/internal/game" ) type Path []data.Position +var ( + // Spatial grid size for position normalization (5x5 tiles) + gridSize = 5 + + // Cache with timestamp-based eviction + pathCache = make(map[string]cacheEntry) + cacheOrder []string // Maintains access order for LRU eviction + cacheLock sync.RWMutex // Protects concurrent access to cache + maxCacheSize = 500 // Maximum number of paths to cache (increased from 100 for adaptive eviction) +) + +type cacheEntry struct { + path Path + lastUsed int64 // Unix nano timestamp +} + +// Generates normalized cache key using spatial partitioning +func cacheKey(from, to data.Position, area area.ID, teleport bool) string { + // Quantize positions to grid cells to group similar paths + normFromX := (from.X / gridSize) * gridSize + normFromY := (from.Y / gridSize) * gridSize + normToX := (to.X / gridSize) * gridSize + normToY := (to.Y / gridSize) * gridSize + + return fmt.Sprintf("%d:%d:%d:%d:%d:%t", + normFromX, normFromY, + normToX, normToY, + area, teleport) +} + +// Retrieves cached path with reverse path check +func getCachedPath(from, to data.Position, area area.ID, teleport bool) (Path, bool) { + key := cacheKey(from, to, area, teleport) + + cacheLock.RLock() + defer cacheLock.RUnlock() + + // Check direct path + if entry, exists := pathCache[key]; exists { + return entry.path, true + } + + // Check reverse path + reverseKey := cacheKey(to, from, area, teleport) + if entry, exists := pathCache[reverseKey]; exists { + // Return reversed path if available + reversed := make(Path, len(entry.path)) + for i, p := range entry.path { + reversed[len(entry.path)-1-i] = p + } + return reversed, true + } + + return nil, false +} + +// Stores path with usage timestamp +func cachePath(from, to data.Position, area area.ID, teleport bool, path Path) { + key := cacheKey(from, to, area, teleport) + + cacheLock.Lock() + defer cacheLock.Unlock() + + // Update existing entry or create new + pathCache[key] = cacheEntry{ + path: path, + lastUsed: time.Now().UnixNano(), + } + + // Adaptive eviction when exceeding capacity + if len(pathCache) >= maxCacheSize { // LRU eviction logic + var oldestKey string + var oldestTime int64 = math.MaxInt64 + + // Find least recently used entry + for k, v := range pathCache { + if v.lastUsed < oldestTime { + oldestTime = v.lastUsed + oldestKey = k + } + } + delete(pathCache, oldestKey) + } +} + +// Return the ending position of the path func (p Path) To() data.Position { + if len(p) == 0 { + return data.Position{} + } return data.Position{ X: p[len(p)-1].X, Y: p[len(p)-1].Y, } } +// Return the starting position of the path func (p Path) From() data.Position { + if len(p) == 0 { + return data.Position{} + } return data.Position{ X: p[0].X, Y: p[0].Y, } } -// Intersects checks if the given position intersects with the path, padding parameter is used to increase the area +// Intersects checks if the given position intersects with the path func (p Path) Intersects(d game.Data, position data.Position, padding int) bool { - position = data.Position{ - X: position.X - d.AreaOrigin.X, - Y: position.Y - d.AreaOrigin.Y, - } for _, point := range p { xMatch := false @@ -44,6 +139,5 @@ func (p Path) Intersects(d game.Data, position data.Position, padding int) bool return true } } - return false } diff --git a/internal/pather/path_finder.go b/internal/pather/path_finder.go index 62a95c4eb..f381afbd4 100644 --- a/internal/pather/path_finder.go +++ b/internal/pather/path_finder.go @@ -3,6 +3,7 @@ package pather import ( "fmt" "math" + "sync" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/area" @@ -12,10 +13,14 @@ import ( ) type PathFinder struct { - gr *game.MemoryReader - data *game.Data - hid *game.HID - cfg *config.CharacterCfg + gr *game.MemoryReader + data *game.Data + hid *game.HID + cfg *config.CharacterCfg + gridLock sync.Mutex // Protects grid state from concurrent access + lastGrid *game.Grid // Cache last processed grid + lastGridArea area.ID // Track area for grid cache validation + lastTeleport bool // Track last teleport state for cache validation } func NewPathFinder(gr *game.MemoryReader, data *game.Data, hid *game.HID, cfg *config.CharacterCfg) *PathFinder { @@ -28,99 +33,148 @@ func NewPathFinder(gr *game.MemoryReader, data *game.Data, hid *game.HID, cfg *c } func (pf *PathFinder) GetPath(to data.Position) (Path, int, bool) { - // First try direct path - if path, distance, found := pf.GetPathFrom(pf.data.PlayerUnit.Position, to); found { - return path, distance, true - } + currentPos := pf.data.PlayerUnit.Position + currentArea := pf.data.PlayerUnit.Area + teleportEnabled := pf.data.CanTeleport() - // If direct path fails, try to find nearby walkable position - if walkableTo, found := pf.findNearbyWalkablePosition(to); found { - return pf.GetPathFrom(pf.data.PlayerUnit.Position, walkableTo) + // Check cache with teleport status + if path, found := getCachedPath(currentPos, to, currentArea, teleportEnabled); found { + return path, len(path), true } - return nil, 0, false + // Calculate and cache new path + path, distance, found := pf.GetPathFrom(currentPos, to) + if found { + cachePath(currentPos, to, currentArea, teleportEnabled, path) + } + return path, distance, found } func (pf *PathFinder) GetPathFrom(from, to data.Position) (Path, int, bool) { a := pf.data.AreaData - // We don't want to modify the original grid - grid := a.Grid.Copy() - - // Special handling for Arcane Sanctuary (to allow pathing with platforms) - if pf.data.PlayerUnit.Area == area.ArcaneSanctuary && pf.data.CanTeleport() { - // Make all non-walkable tiles into low priority tiles for teleport pathing - for y := 0; y < len(grid.CollisionGrid); y++ { - for x := 0; x < len(grid.CollisionGrid[y]); x++ { - if grid.CollisionGrid[y][x] == game.CollisionTypeNonWalkable { - grid.CollisionGrid[y][x] = game.CollisionTypeLowPriority - } - } - } - } - // Lut Gholein map is a bit bugged, we should close this fake path to avoid pathing issues - if a.Area == area.LutGholein { - a.CollisionGrid[13][210] = game.CollisionTypeNonWalkable + teleportEnabled := pf.data.CanTeleport() + pf.gridLock.Lock() + defer pf.gridLock.Unlock() + + // Regenerate grid if teleport status changed + var grid *game.Grid + if pf.lastGrid != nil && pf.lastGridArea == a.Area && pf.lastTeleport == teleportEnabled { + grid = pf.lastGrid + } else { + grid = a.Grid.Copy() + pf.preprocessGrid(grid, teleportEnabled) + pf.lastGrid = grid + pf.lastGridArea = a.Area + pf.lastTeleport = teleportEnabled } + // Handle cross-area pathing using grids & teleport status if !a.IsInside(to) { expandedGrid, err := pf.mergeGrids(to) if err != nil { return nil, 0, false } + pf.preprocessGrid(expandedGrid, teleportEnabled) grid = expandedGrid + pf.lastGrid = grid + pf.lastGridArea = a.Area } from = grid.RelativePosition(from) to = grid.RelativePosition(to) - // Add objects to the collision grid as obstacles + // Validate positions are within grid bounds + if from.X < 0 || from.X >= grid.Width || from.Y < 0 || from.Y >= grid.Height || + to.X < 0 || to.X >= grid.Width || to.Y < 0 || to.Y >= grid.Height { + return nil, 0, false + } + + path, _, found := astar.CalculatePath(grid, a.Area, from, to, teleportEnabled) + + if config.Koolo.Debug.RenderMap { + pf.renderMap(grid, from, to, path) + } + + return path, len(path), found +} + +// Enhanced grid preprocessing with teleport optimizations +func (pf *PathFinder) preprocessGrid(grid *game.Grid, teleportEnabled bool) { + a := pf.data.AreaData + + // Teleport pathing optimizations + // if teleportEnabled { + // for y := 0; y < len(grid.CollisionGrid); y++ { + // for x := 0; x < len(grid.CollisionGrid[y]); x++ { + // // Only allow low priority for objects, keep walls blocked + // switch grid.CollisionGrid[y][x] { + // case game.CollisionTypeObject: + // grid.CollisionGrid[y][x] = game.CollisionTypeLowPriority + // } + // } + // } + // } + + // Special area handling + if a.Area == area.LutGholein { + grid.CollisionGrid[13][210] = game.CollisionTypeNonWalkable + } + + // Dynamic obstacle handling for _, o := range pf.data.AreaData.Objects { + if o.IsChest() { + relativePos := grid.RelativePosition(o.Position) + for dy := -2; dy <= 2; dy++ { + for dx := -2; dx <= 2; dx++ { + y := relativePos.Y + dy + x := relativePos.X + dx + if y >= 0 && y < len(grid.CollisionGrid) && x >= 0 && x < len(grid.CollisionGrid[y]) { + grid.CollisionGrid[y][x] = game.CollisionTypeNonWalkable + } + } + } + continue + } + if !grid.IsWalkable(o.Position) { continue } + relativePos := grid.RelativePosition(o.Position) grid.CollisionGrid[relativePos.Y][relativePos.X] = game.CollisionTypeObject for i := -2; i <= 2; i++ { for j := -2; j <= 2; j++ { - if i == 0 && j == 0 { - continue - } - if relativePos.Y+i < 0 || relativePos.Y+i >= len(grid.CollisionGrid) || relativePos.X+j < 0 || relativePos.X+j >= len(grid.CollisionGrid[relativePos.Y]) { + if i == 0 && j == 0 || relativePos.Y+i < 0 || + relativePos.Y+i >= len(grid.CollisionGrid) || + relativePos.X+j < 0 || relativePos.X+j >= len(grid.CollisionGrid[relativePos.Y]) { continue } if grid.CollisionGrid[relativePos.Y+i][relativePos.X+j] == game.CollisionTypeWalkable { grid.CollisionGrid[relativePos.Y+i][relativePos.X+j] = game.CollisionTypeLowPriority - } } } } - // Add monsters to the collision grid as obstacles for _, m := range pf.data.Monsters { if !grid.IsWalkable(m.Position) { + continue } relativePos := grid.RelativePosition(m.Position) grid.CollisionGrid[relativePos.Y][relativePos.X] = game.CollisionTypeMonster } - - path, distance, found := astar.CalculatePath(grid, pf.data.PlayerUnit.Area, from, to) - - if config.Koolo.Debug.RenderMap { - pf.renderMap(grid, from, to, path) - } - - return path, distance, found } +// Combine adjacent level grids for cross-area pathfinding func (pf *PathFinder) mergeGrids(to data.Position) (*game.Grid, error) { for _, a := range pf.data.AreaData.AdjacentLevels { destination := pf.data.Areas[a.Area] if destination.IsInside(to) { origin := pf.data.AreaData + // Calculate merged grid dimensions endX1 := origin.OffsetX + len(origin.Grid.CollisionGrid[0]) endY1 := origin.OffsetY + len(origin.Grid.CollisionGrid) endX2 := destination.OffsetX + len(destination.Grid.CollisionGrid[0]) @@ -139,16 +193,14 @@ func (pf *PathFinder) mergeGrids(to data.Position) (*game.Grid, error) { resultGrid[i] = make([]game.CollisionType, width) } - // Let's copy both grids into the result grid + // Copy both grids into the merged result grid copyGrid(resultGrid, origin.CollisionGrid, origin.OffsetX-minX, origin.OffsetY-minY) copyGrid(resultGrid, destination.CollisionGrid, destination.OffsetX-minX, destination.OffsetY-minY) grid := game.NewGrid(resultGrid, minX, minY) - return grid, nil } } - return nil, fmt.Errorf("destination grid not found") } @@ -160,12 +212,16 @@ func copyGrid(dest [][]game.CollisionType, src [][]game.CollisionType, offsetX, } } +// Enhanced path recovery with teleport adjustments func (pf *PathFinder) GetClosestWalkablePath(dest data.Position) (Path, int, bool) { return pf.GetClosestWalkablePathFrom(pf.data.PlayerUnit.Position, dest) } +// Find nearest accessible position when direct path is blocked func (pf *PathFinder) GetClosestWalkablePathFrom(from, dest data.Position) (Path, int, bool) { a := pf.data.AreaData + teleportEnabled := pf.data.CanTeleport() + // First try direct path if destination is walkable or outside known area if a.IsWalkable(dest) || !a.IsInside(dest) { path, distance, found := pf.GetPath(dest) if found { @@ -173,45 +229,41 @@ func (pf *PathFinder) GetClosestWalkablePathFrom(from, dest data.Position) (Path } } - maxRange := 20 - step := 4 - dst := 1 + // Search in expanding squares around target position + maxRange := 25 + step := 5 + if teleportEnabled { + maxRange = 40 + step = 8 + } - for dst < maxRange { + for dst := 1; dst < maxRange; dst += step { for i := -dst; i < dst; i += 1 { for j := -dst; j < dst; j += 1 { + // Check perimeter of current search radius if math.Abs(float64(i)) >= math.Abs(float64(dst)) || math.Abs(float64(j)) >= math.Abs(float64(dst)) { cgY := dest.Y - pf.data.AreaOrigin.Y + j cgX := dest.X - pf.data.AreaOrigin.X + i - if cgX > 0 && cgY > 0 && a.Height > cgY && a.Width > cgX && a.CollisionGrid[cgY][cgX] == game.CollisionTypeWalkable { - return pf.GetPathFrom(from, data.Position{ - X: dest.X + i, - Y: dest.Y + j, - }) + if cgX > 0 && cgY > 0 && a.Height > cgY && a.Width > cgX { + collisionType := a.CollisionGrid[cgY][cgX] + // Adjust collision checks for teleport + if teleportEnabled && collisionType == game.CollisionTypeLowPriority { + return pf.GetPathFrom(from, data.Position{ + X: dest.X + i, + Y: dest.Y + j, + }) + } + if collisionType == game.CollisionTypeWalkable { + return pf.GetPathFrom(from, data.Position{ + X: dest.X + i, + Y: dest.Y + j, + }) + } } } } } - dst += step } return nil, 0, false } - -func (pf *PathFinder) findNearbyWalkablePosition(target data.Position) (data.Position, bool) { - // Search in expanding squares around the target position - for radius := 1; radius <= 3; radius++ { - for x := -radius; x <= radius; x++ { - for y := -radius; y <= radius; y++ { - if x == 0 && y == 0 { - continue - } - pos := data.Position{X: target.X + x, Y: target.Y + y} - if pf.data.AreaData.IsWalkable(pos) { - return pos, true - } - } - } - } - return data.Position{}, false -} diff --git a/internal/pather/utils.go b/internal/pather/utils.go index ad0d5666b..453695f03 100644 --- a/internal/pather/utils.go +++ b/internal/pather/utils.go @@ -1,8 +1,10 @@ package pather import ( + "fmt" "math" "math/rand" + "sync" "time" "github.com/hectorgimenez/d2go/pkg/data" @@ -11,6 +13,12 @@ import ( "github.com/hectorgimenez/koolo/internal/utils" ) +var ( + // Cached walkable positions with mutex protection + walkablePosCache = make(map[string]data.Position) + walkablePosLock sync.RWMutex +) + func (pf *PathFinder) RandomMovement() { midGameX := pf.gr.GameAreaSizeX / 2 midGameY := pf.gr.GameAreaSizeY / 2 @@ -25,9 +33,44 @@ func (pf *PathFinder) DistanceFromMe(p data.Position) int { return DistanceFromPoint(pf.data.PlayerUnit.Position, p) } +// Search in expanding squares around target position with caching +func (pf *PathFinder) FindNearbyWalkablePosition(target data.Position) (data.Position, bool) { + key := fmt.Sprintf("%d:%d", target.X, target.Y) + // Check cache first with read lock + walkablePosLock.RLock() + if pos, exists := walkablePosCache[key]; exists { + walkablePosLock.RUnlock() + return pos, true + } + + walkablePosLock.RUnlock() + + // Search in expanding squares around target position + for radius := 1; radius <= 3; radius++ { + for x := -radius; x <= radius; x++ { + for y := -radius; y <= radius; y++ { + if x == 0 && y == 0 { + continue // Skip center position + } + pos := data.Position{X: target.X + x, Y: target.Y + y} + if pf.data.AreaData.IsWalkable(pos) { + // Update cache with write lock + walkablePosLock.Lock() + walkablePosCache[key] = pos + walkablePosLock.Unlock() + return pos, true + } + } + } + } + return data.Position{}, false +} + +// Create optimal room visiting order using nearest neighbor algorithm func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { distanceMatrix := make(map[data.Room]map[data.Room]int) + // Build distance matrix between all rooms for _, room1 := range pf.data.Rooms { distanceMatrix[room1] = make(map[data.Room]int) for _, room2 := range pf.data.Rooms { @@ -40,6 +83,7 @@ func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { } } + // Find current room based on player position currentRoom := data.Room{} for _, r := range pf.data.Rooms { if r.IsInside(pf.data.PlayerUnit.Position) { @@ -51,11 +95,11 @@ func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { order := []data.Room{currentRoom} visited[currentRoom] = true + // Nearest neighbor pathfinding for len(order) < len(pf.data.Rooms) { nextRoom := data.Room{} minDistance := math.MaxInt - // Find the nearest unvisited room for _, room := range pf.data.Rooms { if !visited[room] && distanceMatrix[currentRoom][room] < minDistance { nextRoom = room @@ -63,7 +107,7 @@ func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { } } - // Add the next room to the order of visit + // Find closest unvisited room order = append(order, nextRoom) visited[nextRoom] = true currentRoom = nextRoom @@ -72,6 +116,7 @@ func (pf *PathFinder) OptimizeRoomsTraverseOrder() []data.Room { return order } +// Navigate along a path considering movement constraints func (pf *PathFinder) MoveThroughPath(p Path, walkDuration time.Duration) { // Calculate the max distance we can walk in the given duration maxDistance := int(float64(25) * walkDuration.Seconds()) @@ -100,6 +145,8 @@ func (pf *PathFinder) MoveThroughPath(p Path, walkDuration time.Duration) { pf.MoveCharacter(screenCords.X, screenCords.Y) } + +// Handle movement based on character capabilities func (pf *PathFinder) MoveCharacter(x, y int) { if pf.data.CanTeleport() { pf.hid.Click(game.RightButton, x, y) @@ -127,15 +174,18 @@ func (pf *PathFinder) gameCoordsToScreenCords(playerX, playerY, destinationX, de return screenX, screenY } +// Identify areas requiring restricted movement directions func IsNarrowMap(a area.ID) bool { switch a { - case area.MaggotLairLevel1, area.MaggotLairLevel2, area.MaggotLairLevel3, area.ArcaneSanctuary, area.ClawViperTempleLevel2, area.RiverOfFlame, area.ChaosSanctuary: + case area.MaggotLairLevel1, area.MaggotLairLevel2, area.MaggotLairLevel3, + area.ArcaneSanctuary, area.ClawViperTempleLevel2, area.RiverOfFlame, + area.ChaosSanctuary: return true } - return false } +// Calculate straight-line distance between two positions (Bresenham algo) func DistanceFromPoint(from data.Position, to data.Position) int { first := math.Pow(float64(to.X-from.X), 2) second := math.Pow(float64(to.Y-from.Y), 2) @@ -143,6 +193,7 @@ func DistanceFromPoint(from data.Position, to data.Position) int { return int(math.Sqrt(first + second)) } +// Check if there's unobstructed path between two points func (pf *PathFinder) LineOfSight(origin data.Position, destination data.Position) bool { dx := int(math.Abs(float64(destination.X - origin.X))) dy := int(math.Abs(float64(destination.Y - origin.Y))) @@ -180,7 +231,7 @@ func (pf *PathFinder) LineOfSight(origin data.Position, destination data.Positio return true } -// BeyondPosition calculates a new position that is a specified distance beyond the target position when viewed from the start position +// Calculate a new position that is a specified distance beyond the target position when viewed from the start position (calculates position extended beyond target point) func (pf *PathFinder) BeyondPosition(start, target data.Position, distance int) data.Position { // Calculate direction vector dx := float64(target.X - start.X) diff --git a/internal/run/pindleskin.go b/internal/run/pindleskin.go index 1f0b0f390..6ceb5e9f9 100644 --- a/internal/run/pindleskin.go +++ b/internal/run/pindleskin.go @@ -12,8 +12,8 @@ import ( ) var fixedPlaceNearRedPortal = data.Position{ - X: 5130, - Y: 5120, + X: 5131, // Adjusted coords from X: 5130 to prevent bot stuck + Y: 5123, // Adjusted coords from Y: 5120 to prevent bot stuck } var pindleSafePosition = data.Position{