- Created debug tool (cmd/debug/main.go) to analyze real D2R screenshots and calibrate HSV ranges - Fixed HSV color ranges for health/mana orbs based on real screenshot analysis (99.5% and 82% detection rates) - Replaced ReadBarPercentage with ReadOrbPercentage for circular orbs (not horizontal bars) - Added SetSource() method to capture Manager for hot-swapping capture sources - Fixed dashboard JavaScript null reference errors with proper array checks - Improved dashboard refresh rate from 100ms to 1000ms for better performance - Added proper error handling for empty/null API responses - Successfully detecting game state, health (99.5%), and mana (82%) from real D2R screenshot
365 lines
No EOL
9.2 KiB
Go
365 lines
No EOL
9.2 KiB
Go
// 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(),
|
|
}
|
|
|
|
// Register game-specific resolution profiles with the engine's registry.
|
|
// Plugins that support resolution profiles implement this optional interface.
|
|
type profileRegistrar interface {
|
|
RegisterResolutionProfiles(*resolution.Registry) error
|
|
}
|
|
if registrar, ok := gamePlugin.(profileRegistrar); ok {
|
|
if err := registrar.RegisterResolutionProfiles(engine.resolutionRegistry); err != nil {
|
|
return nil, fmt.Errorf("failed to register resolution profiles: %w", err)
|
|
}
|
|
log.Printf("Registered resolution profiles for %s", gamePlugin.Info().ID)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SetCaptureSource swaps the capture source (for development/testing).
|
|
func (e *Engine) SetCaptureSource(newSource capture.Source) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
return e.captureManager.SetSource(newSource)
|
|
}
|
|
|
|
// 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
|
|
},
|
|
} |