268 lines
6.8 KiB
Go
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 > %d", totalDistance, MaxPathDistance)
|
|
}
|
|
|
|
path.Distance = totalDistance
|
|
return nil
|
|
} |