From 7b2bd1be0be4df38f93a25179e6ca1a030a23fa3 Mon Sep 17 00:00:00 2001 From: elb Date: Tue, 25 Feb 2025 09:30:47 -0500 Subject: [PATCH] remove redundant storage of path data across two separate systems The global cache in pather/path.go serves as the primary storage for path data and metadata. The PathCache structure in context.go is maintained but modified to act as a reference to the global cache --- internal/action/clear_area.go | 52 ++++++++++----- internal/action/move.go | 41 +++++++----- internal/action/step/move.go | 79 +++++++++++----------- internal/context/context.go | 44 ++++++++++++- internal/pather/path.go | 115 ++++++++++++++++++++++++++------- internal/pather/path_finder.go | 5 +- 6 files changed, 233 insertions(+), 103 deletions(-) diff --git a/internal/action/clear_area.go b/internal/action/clear_area.go index d2fe4da8..3ecc3336 100644 --- a/internal/action/clear_area.go +++ b/internal/action/clear_area.go @@ -34,54 +34,74 @@ func ClearAreaAroundPosition(pos data.Position, radius int, filter data.MonsterF func ClearThroughPath(pos data.Position, radius int, filter data.MonsterFilter) error { ctx := context.Get() lastMovement := false + currentArea := ctx.Data.PlayerUnit.Area + canTeleport := ctx.Data.CanTeleport() for { ctx.PauseIfNotPriority() + // Clear enemies at current position ClearAreaAroundPosition(ctx.Data.PlayerUnit.Position, radius, filter) if lastMovement { return nil } - var path []data.Position + // Get current position for path calculation + currentPos := ctx.Data.PlayerUnit.Position + + // Get path to destination, leveraging both context cache and global cache + var path pather.Path var found bool + + // Try context cache first for UI consistency if ctx.CurrentGame.PathCache != nil && ctx.CurrentGame.PathCache.DestPosition == pos && - step.IsPathValid(ctx.Data.PlayerUnit.Position, ctx.CurrentGame.PathCache) { + ctx.CurrentGame.PathCache.IsPathValid(currentPos) { + // Use existing path from context cache path = ctx.CurrentGame.PathCache.Path found = true } else { - - path, _, found = ctx.PathFinder.GetPath(pos) - if found { - ctx.CurrentGame.PathCache = &context.PathCache{ - Path: path, - DestPosition: pos, - StartPosition: ctx.Data.PlayerUnit.Position, - DistanceToFinish: step.DistanceToFinishMoving, + // Try to get path from global cache + path, found = pather.GetCachedPath(currentPos, pos, currentArea, canTeleport) + if !found { + // Calculate new path if not in cache + path, _, found = ctx.PathFinder.GetPath(pos) + if !found { + return fmt.Errorf("path could not be calculated") } + + // Store in global cache + pather.StorePath(currentPos, pos, currentArea, canTeleport, path, currentPos) } - } - if !found { - return fmt.Errorf("path could not be calculated") + // Update context cache for UI reference + ctx.CurrentGame.PathCache = &context.PathCache{ + Path: path, + DestPosition: pos, + StartPosition: currentPos, + DistanceToFinish: step.DistanceToFinishMoving, + } } + // Calculate movement distance for this segment movementDistance := radius - if radius > len(path) { + if movementDistance > len(path) { movementDistance = len(path) } + // Set destination to next path segment dest := data.Position{ - X: path[movementDistance-1].X + ctx.Data.AreaData.OffsetX, - Y: path[movementDistance-1].Y + ctx.Data.AreaData.OffsetY, + X: path[movementDistance-1].X + ctx.Data.AreaOrigin.X, + Y: path[movementDistance-1].Y + ctx.Data.AreaOrigin.Y, } + // Check if this is the last movement segment if len(path)-movementDistance <= step.DistanceToFinishMoving { lastMovement = true } + // Move to next segment if err := step.MoveTo(dest); err != nil { return err } diff --git a/internal/action/move.go b/internal/action/move.go index 33d2c3a5..4e5df42d 100644 --- a/internal/action/move.go +++ b/internal/action/move.go @@ -49,9 +49,6 @@ 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") @@ -215,30 +212,41 @@ func MoveTo(toFunc func() (data.Position, bool)) error { ctx.PauseIfNotPriority() currentPos := ctx.Data.PlayerUnit.Position + currentArea := ctx.Data.PlayerUnit.Area + canTeleport := ctx.Data.CanTeleport() // Get current path segment - var path []data.Position + var path pather.Path var found bool + + // Handle path caching using both context.PathCache (for UI) and global cache (for storage) if ctx.CurrentGame.PathCache != nil && ctx.CurrentGame.PathCache.DestPosition == to && - step.IsPathValid(currentPos, ctx.CurrentGame.PathCache) { + ctx.CurrentGame.PathCache.IsPathValid(currentPos) { + // Use existing path from context cache path = ctx.CurrentGame.PathCache.Path found = true } else { - - path, _, found = ctx.PathFinder.GetPath(to) - if found { - ctx.CurrentGame.PathCache = &context.PathCache{ - Path: path, - DestPosition: to, - StartPosition: currentPos, - DistanceToFinish: step.DistanceToFinishMoving, + // Try to get path from global cache + path, found = pather.GetCachedPath(currentPos, to, currentArea, canTeleport) + if !found { + // Calculate new path if not in cache + path, _, found = ctx.PathFinder.GetPath(to) + if !found { + return fmt.Errorf("path could not be calculated") } + + // Store in global cache + pather.StorePath(currentPos, to, currentArea, canTeleport, path, currentPos) } - } - if !found { - return fmt.Errorf("path could not be calculated") + // Update context cache for UI reference + ctx.CurrentGame.PathCache = &context.PathCache{ + Path: path, + DestPosition: to, + StartPosition: currentPos, + DistanceToFinish: step.DistanceToFinishMoving, + } } //TODO if character detects monster on other side of the wall and we are past door detection @@ -289,7 +297,6 @@ func MoveTo(toFunc func() (data.Position, bool)) error { } } // Continue moving - // WaitForAllMembersWhenLeveling is breaking walkable logic, lets not use it for now. if lastMovement { diff --git a/internal/action/step/move.go b/internal/action/step/move.go index cc91af87..1d6aa4cf 100644 --- a/internal/action/step/move.go +++ b/internal/action/step/move.go @@ -51,27 +51,35 @@ 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 + currentPos := ctx.Data.PlayerUnit.Position + currentArea := ctx.Data.PlayerUnit.Area + canTeleport := ctx.Data.CanTeleport() + if ctx.CurrentGame.PathCache != nil && ctx.CurrentGame.PathCache.DestPosition == dest && - IsPathValid(ctx.Data.PlayerUnit.Position, ctx.CurrentGame.PathCache) { + ctx.CurrentGame.PathCache.IsPathValid(currentPos) { // Reuse existing path cache pathCache = ctx.CurrentGame.PathCache } else { - // Create new path cache - start := ctx.Data.PlayerUnit.Position - path, _, found := ctx.PathFinder.GetPath(dest) + // Get path from global cache or calculate new one + path, found := pather.GetCachedPath(currentPos, dest, currentArea, canTeleport) if !found { - return fmt.Errorf("path not found to %v", dest) + // Calculate new path if not found in cache + path, _, found = ctx.PathFinder.GetPath(dest) + if !found { + return fmt.Errorf("path not found to %v", dest) + } + // Store in global cache + pather.StorePath(currentPos, dest, currentArea, canTeleport, path, currentPos) } + // Create new PathCache reference pathCache = &context.PathCache{ Path: path, DestPosition: dest, - StartPosition: start, + StartPosition: currentPos, DistanceToFinish: DistanceToFinishMoving, } ctx.CurrentGame.PathCache = pathCache @@ -85,6 +93,11 @@ func MoveTo(dest data.Position, options ...MoveOption) error { // Add some delay between clicks to let the character move to destination walkDuration := utils.RandomDurationMs(700, 900) + // Get last check time from global cache + lastCheck := pathCache.GetLastCheck(currentArea, canTeleport) + lastRun := pathCache.GetLastRun(currentArea, canTeleport) + previousPosition := pathCache.GetPreviousPosition(currentArea, canTeleport) + for { time.Sleep(50 * time.Millisecond) // Pause the execution if the priority is not the same as the execution priority @@ -93,9 +106,9 @@ func MoveTo(dest data.Position, options ...MoveOption) error { now := time.Now() // Refresh data and perform checks periodically - if now.Sub(pathCache.LastCheck) > refreshInterval { + if now.Sub(lastCheck) > refreshInterval { ctx.RefreshGameData() - currentPos := ctx.Data.PlayerUnit.Position + currentPos = ctx.Data.PlayerUnit.Position distanceToDest := pather.DistanceFromPoint(currentPos, dest) // Check if we've reached destination @@ -104,14 +117,14 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } // Check for stuck in same position (direct equality check is efficient) - isSamePosition := pathCache.PreviousPosition.X == currentPos.X && pathCache.PreviousPosition.Y == currentPos.Y + isSamePosition := previousPosition.X == currentPos.X && previousPosition.Y == currentPos.Y // Only recalculate path in specific cases to reduce CPU usage if isSamePosition && !ctx.Data.CanTeleport() { // If stuck in same position without teleport, make random movement ctx.PathFinder.RandomMovement() } else if pathCache.Path == nil || - !IsPathValid(currentPos, pathCache) || + !pathCache.IsPathValid(currentPos) || (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 @@ -123,12 +136,17 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } return fmt.Errorf("failed to calculate path") } + + // Update global cache and local PathCache + pather.StorePath(currentPos, dest, currentArea, canTeleport, path, currentPos) pathCache.Path = path pathCache.StartPosition = currentPos } - pathCache.PreviousPosition = currentPos - pathCache.LastCheck = now + // Update previous position and check time + previousPosition = currentPos + lastCheck = now + pathCache.UpdateLastCheck(currentArea, canTeleport, currentPos) if now.Sub(startedAt) > timeout { return fmt.Errorf("movement timeout") @@ -136,10 +154,10 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } if !ctx.Data.CanTeleport() { - if time.Since(pathCache.LastRun) < walkDuration { + if time.Since(lastRun) < walkDuration { continue } - } else if time.Since(pathCache.LastRun) < ctx.Data.PlayerCastDuration() { + } else if time.Since(lastRun) < ctx.Data.PlayerCastDuration() { continue } @@ -159,34 +177,11 @@ func MoveTo(dest data.Position, options ...MoveOption) error { } } - pathCache.LastRun = time.Now() + lastRun = time.Now() + pathCache.UpdateLastRun(currentArea, canTeleport) + if len(pathCache.Path) > 0 { ctx.PathFinder.MoveThroughPath(pathCache.Path, walkDuration) } } } - -// Validate if the path is still valid based on current position -func IsPathValid(currentPos data.Position, cache *context.PathCache) bool { - if cache == nil { - return false - } - - // Valid if we're close to start, destination, or current path - if pather.DistanceFromPoint(currentPos, cache.StartPosition) < 20 || - pather.DistanceFromPoint(currentPos, cache.DestPosition) < 20 { - return true - } - - // Check if we're near any point on the path - minDistance := 20 - for _, pathPoint := range cache.Path { - dist := pather.DistanceFromPoint(currentPos, pathPoint) - if dist < minDistance { - minDistance = dist - break - } - } - - return minDistance < 20 -} diff --git a/internal/context/context.go b/internal/context/context.go index c8a4c914..5c70f188 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -59,16 +59,53 @@ type Debug struct { LastAction string `json:"lastAction"` LastStep string `json:"lastStep"` } + type PathCache struct { Path []data.Position DestPosition data.Position StartPosition data.Position - PreviousPosition data.Position - LastCheck time.Time - LastRun time.Time DistanceToFinish int } +func (pc *PathCache) IsPathValid(currentPos data.Position) bool { + if pc == nil { + return false + } + return pather.IsPathValid(currentPos, pc.StartPosition, pc.DestPosition, pc.Path) +} + +func (pc *PathCache) GetLastRun(currentArea area.ID, canTeleport bool) time.Time { + entry, found := pather.GetCacheEntry(pc.StartPosition, pc.DestPosition, currentArea, canTeleport) + if found { + return entry.LastRun + } + return time.Time{} +} + +func (pc *PathCache) UpdateLastRun(currentArea area.ID, canTeleport bool) { + pather.UpdatePathLastRun(pc.StartPosition, pc.DestPosition, currentArea, canTeleport) +} + +func (pc *PathCache) GetLastCheck(currentArea area.ID, canTeleport bool) time.Time { + entry, found := pather.GetCacheEntry(pc.StartPosition, pc.DestPosition, currentArea, canTeleport) + if found { + return entry.LastCheck + } + return time.Time{} +} + +func (pc *PathCache) UpdateLastCheck(currentArea area.ID, canTeleport bool, currentPos data.Position) { + pather.UpdatePathLastCheck(pc.StartPosition, pc.DestPosition, currentArea, canTeleport, currentPos) +} + +func (pc *PathCache) GetPreviousPosition(currentArea area.ID, canTeleport bool) data.Position { + entry, found := pather.GetCacheEntry(pc.StartPosition, pc.DestPosition, currentArea, canTeleport) + if found { + return entry.PreviousPosition + } + return data.Position{} +} + type CurrentGameHelper struct { BlacklistedItems []data.Item PickedUpItems map[int]int @@ -174,6 +211,7 @@ func (s *Status) PauseIfNotPriority() { time.Sleep(time.Millisecond * 10) } } + func (ctx *Context) WaitForGameToLoad() { // Get only the loading screen state from OpenMenus for ctx.GameReader.GetData().OpenMenus.LoadingScreen { diff --git a/internal/pather/path.go b/internal/pather/path.go index 2c77d2ed..261c7e1f 100644 --- a/internal/pather/path.go +++ b/internal/pather/path.go @@ -13,22 +13,25 @@ import ( type Path []data.Position +// PathCacheEntry stores path data and metadata in the global cache +type PathCacheEntry struct { + Path Path + LastUsed int64 // Unix nano timestamp + PreviousPosition data.Position + LastCheck time.Time + LastRun time.Time +} + 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 + pathCache = make(map[string]PathCacheEntry) cacheLock sync.RWMutex // Protects concurrent access to cache - maxCacheSize = 500 // Maximum number of paths to cache (increased from 100 for adaptive eviction) + maxCacheSize = 500 // Maximum number of paths to cache ) -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 @@ -44,7 +47,7 @@ func cacheKey(from, to data.Position, area area.ID, teleport bool) string { } // Retrieves cached path with reverse path check -func getCachedPath(from, to data.Position, area area.ID, teleport bool) (Path, bool) { +func GetCachedPath(from, to data.Position, area area.ID, teleport bool) (Path, bool) { key := cacheKey(from, to, area, teleport) cacheLock.RLock() @@ -52,16 +55,16 @@ func getCachedPath(from, to data.Position, area area.ID, teleport bool) (Path, b // Check direct path if entry, exists := pathCache[key]; exists { - return entry.path, true + 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 + reversed := make(Path, len(entry.Path)) + for i, p := range entry.Path { + reversed[len(entry.Path)-1-i] = p } return reversed, true } @@ -69,19 +72,38 @@ func getCachedPath(from, to data.Position, area area.ID, teleport bool) (Path, b return nil, false } -// Stores path with usage timestamp -func cachePath(from, to data.Position, area area.ID, teleport bool, path Path) { +// GetCacheEntry retrieves the full cache entry for a path +func GetCacheEntry(from, to data.Position, area area.ID, teleport bool) (*PathCacheEntry, bool) { + key := cacheKey(from, to, area, teleport) + + cacheLock.RLock() + defer cacheLock.RUnlock() + + if entry, exists := pathCache[key]; exists { + entryCopy := entry // Create a copy to safely return + return &entryCopy, true + } + + return nil, false +} + +// StorePath stores a path in the global cache +func StorePath(from, to data.Position, area area.ID, teleport bool, path Path, currentPos data.Position) { 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(), + entry := PathCacheEntry{ + Path: path, + LastUsed: time.Now().UnixNano(), + PreviousPosition: currentPos, + LastCheck: time.Now(), + LastRun: time.Time{}, } + pathCache[key] = entry + // Adaptive eviction when exceeding capacity if len(pathCache) >= maxCacheSize { // LRU eviction logic var oldestKey string @@ -89,8 +111,8 @@ func cachePath(from, to data.Position, area area.ID, teleport bool, path Path) { // Find least recently used entry for k, v := range pathCache { - if v.lastUsed < oldestTime { - oldestTime = v.lastUsed + if v.LastUsed < oldestTime { + oldestTime = v.LastUsed oldestKey = k } } @@ -98,6 +120,56 @@ func cachePath(from, to data.Position, area area.ID, teleport bool, path Path) { } } +// UpdatePathLastRun updates the lastRun time for a path +func UpdatePathLastRun(from, to data.Position, area area.ID, teleport bool) { + key := cacheKey(from, to, area, teleport) + + cacheLock.Lock() + defer cacheLock.Unlock() + + if entry, exists := pathCache[key]; exists { + entry.LastRun = time.Now() + entry.LastUsed = time.Now().UnixNano() + pathCache[key] = entry + } +} + +// UpdatePathLastCheck updates the lastCheck time and previous position for a path +func UpdatePathLastCheck(from, to data.Position, area area.ID, teleport bool, currentPos data.Position) { + key := cacheKey(from, to, area, teleport) + + cacheLock.Lock() + defer cacheLock.Unlock() + + if entry, exists := pathCache[key]; exists { + entry.LastCheck = time.Now() + entry.PreviousPosition = currentPos + entry.LastUsed = time.Now().UnixNano() + pathCache[key] = entry + } +} + +// IsPathValid checks if a path is still valid based on current position +func IsPathValid(currentPos data.Position, startPos data.Position, destPos data.Position, path Path) bool { + // Valid if we're close to start, destination, or current path + if DistanceFromPoint(currentPos, startPos) < 20 || + DistanceFromPoint(currentPos, destPos) < 20 { + return true + } + + // Check if we're near any point on the path + minDistance := 20 + for _, pathPoint := range path { + dist := DistanceFromPoint(currentPos, pathPoint) + if dist < minDistance { + minDistance = dist + break + } + } + + return minDistance < 20 +} + // Return the ending position of the path func (p Path) To() data.Position { if len(p) == 0 { @@ -122,7 +194,6 @@ func (p Path) From() data.Position { // Intersects checks if the given position intersects with the path func (p Path) Intersects(d game.Data, position data.Position, padding int) bool { - for _, point := range p { xMatch := false yMatch := false diff --git a/internal/pather/path_finder.go b/internal/pather/path_finder.go index f381afbd..cab5a0e5 100644 --- a/internal/pather/path_finder.go +++ b/internal/pather/path_finder.go @@ -38,18 +38,17 @@ func (pf *PathFinder) GetPath(to data.Position) (Path, int, bool) { teleportEnabled := pf.data.CanTeleport() // Check cache with teleport status - if path, found := getCachedPath(currentPos, to, currentArea, teleportEnabled); found { + if path, found := GetCachedPath(currentPos, to, currentArea, teleportEnabled); found { return path, len(path), true } // Calculate and cache new path path, distance, found := pf.GetPathFrom(currentPos, to) if found { - cachePath(currentPos, to, currentArea, teleportEnabled, path) + StorePath(currentPos, to, currentArea, teleportEnabled, path, currentPos) } return path, distance, found } - func (pf *PathFinder) GetPathFrom(from, to data.Position) (Path, int, bool) { a := pf.data.AreaData