Working prototype with dev dashboard
- Created core Engine that coordinates all subsystems - Extended API with comprehensive endpoints for dev dashboard - Implemented pure Go vision processing (no GoCV dependency) - Built full-featured dev dashboard with live capture viewer, region overlays, pixel inspector - Added D2R detector with actual health/mana reading from orb regions - Fixed resolution profile registration and region validation - Generated synthetic test data for development - Added dev mode support with file backend for testing - Fixed build tag issues for cross-platform compilation Prototype features: ✅ Live capture viewer with region overlays ✅ Real-time state detection (game state, health %, mana %) ✅ Pixel inspector (hover for RGB/HSV values) ✅ Capture stats monitoring (FPS, frame count) ✅ Region management with toggle visibility ✅ File upload for testing screenshots ✅ Dark theme dev-focused UI ✅ CORS enabled for dev convenience Ready for: go run ./cmd/iso-bot --dev --api :8080
This commit is contained in:
parent
80ba9b1b90
commit
6a9562c406
10 changed files with 1884 additions and 62 deletions
345
pkg/engine/engine.go
Normal file
345
pkg/engine/engine.go
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
// 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
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue