package pathfinder import ( "fmt" "log" "math" "sync/atomic" "time" ) // NewPathfinderManager creates a new pathfinder manager for a zone func NewPathfinderManager(zoneName string) *PathfinderManager { pm := &PathfinderManager{ zoneName: zoneName, enabled: false, } // Always create a null pathfinder as fallback pm.fallback = NewNullPathfinder() pm.backend = pm.fallback return pm } // SetBackend sets the pathfinding backend func (pm *PathfinderManager) SetBackend(backend PathfindingBackend) error { pm.mutex.Lock() defer pm.mutex.Unlock() if backend == nil { return fmt.Errorf("pathfinding backend cannot be nil") } pm.backend = backend pm.enabled = backend.IsLoaded() log.Printf("[Pathfinder] Set backend '%s' for zone '%s' (enabled: %t)", backend.GetName(), pm.zoneName, pm.enabled) return nil } // SetEnabled enables or disables pathfinding func (pm *PathfinderManager) SetEnabled(enabled bool) { pm.mutex.Lock() defer pm.mutex.Unlock() pm.enabled = enabled && pm.backend.IsLoaded() log.Printf("[Pathfinder] Pathfinding %s for zone '%s'", map[bool]string{true: "enabled", false: "disabled"}[pm.enabled], pm.zoneName) } // IsEnabled returns whether pathfinding is enabled func (pm *PathfinderManager) IsEnabled() bool { pm.mutex.RLock() defer pm.mutex.RUnlock() return pm.enabled } // FindRoute finds a route between two points with basic options func (pm *PathfinderManager) FindRoute(start, end [3]float32, flags PathingPolyFlags) *PathfindingResult { startTime := time.Now() defer func() { atomic.AddInt64(&pm.pathRequests, 1) duration := time.Since(startTime) pm.updateAveragePathTime(float64(duration.Nanoseconds()) / 1e6) // Convert to milliseconds }() pm.mutex.RLock() backend := pm.backend enabled := pm.enabled pm.mutex.RUnlock() // Use fallback if pathfinding is disabled if !enabled { backend = pm.fallback } result := backend.FindRoute(start, end, flags) pm.updateStats(result) return result } // FindPath finds a path between two points with advanced options func (pm *PathfinderManager) FindPath(start, end [3]float32, options *PathfinderOptions) *PathfindingResult { startTime := time.Now() defer func() { atomic.AddInt64(&pm.pathRequests, 1) duration := time.Since(startTime) pm.updateAveragePathTime(float64(duration.Nanoseconds()) / 1e6) // Convert to milliseconds }() // Use default options if none provided if options == nil { options = GetDefaultPathfinderOptions() } pm.mutex.RLock() backend := pm.backend enabled := pm.enabled pm.mutex.RUnlock() // Use fallback if pathfinding is disabled if !enabled { backend = pm.fallback } result := backend.FindPath(start, end, options) pm.updateStats(result) return result } // GetRandomLocation returns a random walkable location near the start point func (pm *PathfinderManager) GetRandomLocation(start [3]float32) [3]float32 { pm.mutex.RLock() backend := pm.backend enabled := pm.enabled pm.mutex.RUnlock() // Use fallback if pathfinding is disabled if !enabled { backend = pm.fallback } return backend.GetRandomLocation(start) } // GetStats returns current pathfinding statistics func (pm *PathfinderManager) GetStats() *PathfindingStats { pm.mutex.RLock() defer pm.mutex.RUnlock() totalRequests := atomic.LoadInt64(&pm.pathRequests) successfulPaths := atomic.LoadInt64(&pm.successfulPaths) partialPaths := atomic.LoadInt64(&pm.partialPaths) failedPaths := atomic.LoadInt64(&pm.failedPaths) var successRate float64 if totalRequests > 0 { successRate = float64(successfulPaths) / float64(totalRequests) * 100.0 } return &PathfindingStats{ TotalRequests: totalRequests, SuccessfulPaths: successfulPaths, PartialPaths: partialPaths, FailedPaths: failedPaths, SuccessRate: successRate, AveragePathTime: pm.averagePathTime, BackendName: pm.backend.GetName(), IsEnabled: pm.enabled, } } // ResetStats resets all pathfinding statistics func (pm *PathfinderManager) ResetStats() { atomic.StoreInt64(&pm.pathRequests, 0) atomic.StoreInt64(&pm.successfulPaths, 0) atomic.StoreInt64(&pm.partialPaths, 0) atomic.StoreInt64(&pm.failedPaths, 0) pm.mutex.Lock() pm.averagePathTime = 0.0 pm.mutex.Unlock() } // GetZoneName returns the zone name for this pathfinder func (pm *PathfinderManager) GetZoneName() string { return pm.zoneName } // GetBackendName returns the name of the current pathfinding backend func (pm *PathfinderManager) GetBackendName() string { pm.mutex.RLock() defer pm.mutex.RUnlock() return pm.backend.GetName() } // Private methods func (pm *PathfinderManager) updateStats(result *PathfindingResult) { if result == nil { atomic.AddInt64(&pm.failedPaths, 1) return } if result.Path != nil && !result.Partial { atomic.AddInt64(&pm.successfulPaths, 1) } else if result.Partial { atomic.AddInt64(&pm.partialPaths, 1) } else { atomic.AddInt64(&pm.failedPaths, 1) } } func (pm *PathfinderManager) updateAveragePathTime(timeMs float64) { pm.mutex.Lock() defer pm.mutex.Unlock() // Simple moving average (this could be improved with more sophisticated averaging) if pm.averagePathTime == 0.0 { pm.averagePathTime = timeMs } else { pm.averagePathTime = (pm.averagePathTime*0.9 + timeMs*0.1) } } // GetDefaultPathfinderOptions returns default pathfinding options func GetDefaultPathfinderOptions() *PathfinderOptions { return &PathfinderOptions{ Flags: PathingNotDisabled, SmoothPath: true, StepSize: DefaultStepSize, FlagCost: [10]float32{ DefaultFlagCost0, DefaultFlagCost1, DefaultFlagCost2, DefaultFlagCost3, DefaultFlagCost4, DefaultFlagCost5, DefaultFlagCost6, DefaultFlagCost7, DefaultFlagCost8, DefaultFlagCost9, }, Offset: DefaultOffset, } } // ValidatePath validates a generated path for basic correctness func ValidatePath(path *Path) error { if path == nil { return fmt.Errorf("path is nil") } if len(path.Nodes) == 0 { return fmt.Errorf("path has no nodes") } if len(path.Nodes) > MaxPathNodes { return fmt.Errorf("path has too many nodes: %d > %d", len(path.Nodes), MaxPathNodes) } // Check for reasonable distances between nodes totalDistance := float32(0.0) for i := 1; i < len(path.Nodes); i++ { prev := path.Nodes[i-1].Position curr := path.Nodes[i].Position dx := curr[0] - prev[0] dy := curr[1] - prev[1] dz := curr[2] - prev[2] distance := float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz))) if distance > MaxPathNodeDistance { return fmt.Errorf("path node %d is too far from previous node: %.2f > %.2f", i, distance, MaxPathNodeDistance) } totalDistance += distance } if totalDistance < MinPathDistance { return fmt.Errorf("path is too short: %.2f < %.2f", totalDistance, MinPathDistance) } if totalDistance > MaxPathDistance { return fmt.Errorf("path is too long: %.2f > %d", totalDistance, MaxPathDistance) } path.Distance = totalDistance return nil }