268 lines
6.8 KiB
Go

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 > %.2f", totalDistance, MaxPathDistance)
}
path.Distance = totalDistance
return nil
}