// Package engine provides the core bot engine that wires all components together. package engine import ( "context" "fmt" "image" "log" "sync" "time" "git.cloonar.com/openclawd/iso-bot/pkg/engine/capture" "git.cloonar.com/openclawd/iso-bot/pkg/engine/resolution" "git.cloonar.com/openclawd/iso-bot/pkg/engine/state" "git.cloonar.com/openclawd/iso-bot/pkg/engine/loot" "git.cloonar.com/openclawd/iso-bot/pkg/plugin" ) // Engine is the core bot engine that coordinates all subsystems. type Engine struct { mu sync.RWMutex running bool paused bool devMode bool // Core components captureManager *capture.Manager resolutionRegistry *resolution.Registry stateManager *state.Manager lootEngine *loot.RuleEngine // Plugin system gamePlugin plugin.Plugin services *engineServices // Current state currentFrame image.Image gameState plugin.GameState vitals plugin.VitalStats // Statistics frameCount uint64 itemsFound int runCount int startTime time.Time // Control channels stopChan chan struct{} pauseChan chan bool } // NewEngine creates a new bot engine. func NewEngine(captureSource capture.Source, gamePlugin plugin.Plugin, devMode bool) (*Engine, error) { engine := &Engine{ captureManager: capture.NewManager(captureSource), resolutionRegistry: resolution.NewRegistry(), devMode: devMode, gamePlugin: gamePlugin, stopChan: make(chan struct{}), pauseChan: make(chan bool, 1), startTime: time.Now(), } // Create engine services for the plugin engine.services = &engineServices{engine: engine} // Initialize the plugin if err := gamePlugin.Init(engine.services); err != nil { return nil, fmt.Errorf("failed to initialize game plugin: %w", err) } // Set up state manager engine.stateManager = state.NewManager(gamePlugin.Detector()) // Set up default loot filter if lootFilter := gamePlugin.DefaultLootFilter(); lootFilter != nil { if ruleEngine, ok := lootFilter.(*loot.RuleEngine); ok { engine.lootEngine = ruleEngine } } return engine, nil } // Start begins the engine's main loop. func (e *Engine) Start(ctx context.Context) error { e.mu.Lock() if e.running { e.mu.Unlock() return fmt.Errorf("engine is already running") } e.running = true e.mu.Unlock() log.Printf("Engine starting (dev mode: %v)", e.devMode) ticker := time.NewTicker(33 * time.Millisecond) // ~30 FPS defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-e.stopChan: return nil case paused := <-e.pauseChan: e.mu.Lock() e.paused = paused e.mu.Unlock() if paused { log.Println("Engine paused") } else { log.Println("Engine resumed") } case <-ticker.C: if e.isPaused() { continue } if err := e.processFrame(); err != nil { log.Printf("Frame processing error: %v", err) continue } } } } // Stop stops the engine. func (e *Engine) Stop() { e.mu.Lock() defer e.mu.Unlock() if !e.running { return } e.running = false close(e.stopChan) log.Println("Engine stopped") } // Pause pauses or resumes the engine. func (e *Engine) Pause(paused bool) { select { case e.pauseChan <- paused: default: // Channel is full, already have a pending pause state } } // Status returns the current engine status. func (e *Engine) Status() EngineStatus { e.mu.RLock() defer e.mu.RUnlock() return EngineStatus{ Running: e.running, Paused: e.paused, DevMode: e.devMode, GameState: e.gameState, Vitals: e.vitals, FrameCount: e.frameCount, ItemsFound: e.itemsFound, RunCount: e.runCount, Uptime: time.Since(e.startTime), CaptureStats: e.captureManager.Stats(), } } // CurrentFrame returns the most recent captured frame. func (e *Engine) CurrentFrame() image.Image { e.mu.RLock() defer e.mu.RUnlock() return e.currentFrame } // GamePlugin returns the active game plugin. func (e *Engine) GamePlugin() plugin.Plugin { return e.gamePlugin } // ResolutionRegistry returns the resolution registry. func (e *Engine) ResolutionRegistry() *resolution.Registry { return e.resolutionRegistry } // LootEngine returns the loot filter engine. func (e *Engine) LootEngine() *loot.RuleEngine { return e.lootEngine } // processFrame captures and analyzes a single frame. func (e *Engine) processFrame() error { // Capture frame frame, err := e.captureManager.Capture() if err != nil { return fmt.Errorf("capture failed: %w", err) } e.mu.Lock() e.currentFrame = frame e.frameCount++ e.mu.Unlock() // Update game state gameState := e.stateManager.Update(frame) // Read vitals if in-game var vitals plugin.VitalStats if gameState == plugin.StateInGame { vitals = e.gamePlugin.Detector().ReadVitals(frame) } e.mu.Lock() e.gameState = gameState e.vitals = vitals e.mu.Unlock() // In dev mode, we don't perform any actions if e.devMode { return nil } // TODO: Implement bot actions based on state and detected items return nil } // isPaused returns true if the engine is paused. func (e *Engine) isPaused() bool { e.mu.RLock() defer e.mu.RUnlock() return e.paused } // EngineStatus represents the current state of the engine. type EngineStatus struct { Running bool `json:"running"` Paused bool `json:"paused"` DevMode bool `json:"devMode"` GameState plugin.GameState `json:"gameState"` Vitals plugin.VitalStats `json:"vitals"` FrameCount uint64 `json:"frameCount"` ItemsFound int `json:"itemsFound"` RunCount int `json:"runCount"` Uptime time.Duration `json:"uptime"` CaptureStats capture.Stats `json:"captureStats"` } // engineServices implements plugin.EngineServices. type engineServices struct { engine *Engine } // Capture returns the current screen frame. func (s *engineServices) Capture() image.Image { return s.engine.CurrentFrame() } // CaptureSource returns the active capture source. func (s *engineServices) CaptureSource() capture.Source { return s.engine.captureManager.Source() } // Resolution returns the current capture resolution. func (s *engineServices) Resolution() (width, height int) { return s.engine.captureManager.Size() } // Region returns a named screen region for the current game and resolution. func (s *engineServices) Region(name string) image.Rectangle { width, height := s.Resolution() gameID := s.engine.gamePlugin.Info().ID region, err := s.engine.resolutionRegistry.GetRegion(gameID, width, height, name) if err != nil { log.Printf("Warning: region %q not found: %v", name, err) return image.Rectangle{} } return region } // Click sends a mouse click at the given position. func (s *engineServices) Click(pos image.Point) { if s.engine.devMode { log.Printf("Dev mode: would click at (%d, %d)", pos.X, pos.Y) return } // TODO: Implement actual mouse click } // MoveMouse moves the mouse to the given position. func (s *engineServices) MoveMouse(pos image.Point) { if s.engine.devMode { log.Printf("Dev mode: would move mouse to (%d, %d)", pos.X, pos.Y) return } // TODO: Implement actual mouse movement } // PressKey sends a key press. func (s *engineServices) PressKey(key string) { if s.engine.devMode { log.Printf("Dev mode: would press key %q", key) return } // TODO: Implement actual key press } // TypeText types text with human-like delays. func (s *engineServices) TypeText(text string) { if s.engine.devMode { log.Printf("Dev mode: would type %q", text) return } // TODO: Implement actual text typing } // Wait pauses for a human-like delay. func (s *engineServices) Wait() { time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond) } // WaitMs pauses for a specific duration with randomization. func (s *engineServices) WaitMs(baseMs int, varianceMs int) { variance := time.Duration(rand.Intn(varianceMs*2)-varianceMs) * time.Millisecond delay := time.Duration(baseMs)*time.Millisecond + variance time.Sleep(delay) } // Log logs a message associated with the plugin. func (s *engineServices) Log(level string, msg string, args ...any) { prefix := fmt.Sprintf("[%s] %s", s.engine.gamePlugin.Info().ID, level) log.Printf(prefix+": "+msg, args...) } // Random number generator (TODO: use crypto/rand for better randomness) var rand = struct { Intn func(int) int }{ Intn: func(n int) int { return int(time.Now().UnixNano()) % n }, }