Rewrite to Go: engine, plugin system, D2R plugin, API, loot filter

This commit is contained in:
Hoid 2026-02-14 09:43:39 +00:00
parent e0282a7111
commit 3b363192f2
60 changed files with 1576 additions and 3407 deletions

93
pkg/api/api.go Normal file
View file

@ -0,0 +1,93 @@
// Package api provides the REST + WebSocket API for the bot dashboard.
//
// Endpoints:
// GET /api/status — bot status, current state, routine, stats
// GET /api/config — current configuration
// PUT /api/config — update configuration
// POST /api/start — start bot with routine
// POST /api/stop — stop bot
// POST /api/pause — pause/resume
// GET /api/routines — list available routines
// GET /api/loot/rules — get loot filter rules
// PUT /api/loot/rules — update loot filter rules
// GET /api/stats — run statistics, items found, etc.
// WS /api/ws — real-time status stream
//
// The API is served by the bot process itself (single binary).
package api
import (
"encoding/json"
"net/http"
"sync"
)
// Status represents the current bot status.
type Status struct {
Running bool `json:"running"`
Paused bool `json:"paused"`
GameState string `json:"gameState"`
Routine string `json:"routine,omitempty"`
Phase string `json:"phase,omitempty"`
RunCount int `json:"runCount"`
ItemsFound int `json:"itemsFound"`
Uptime string `json:"uptime"`
CaptureFPS float64 `json:"captureFps"`
HealthPct float64 `json:"healthPct"`
ManaPct float64 `json:"manaPct"`
}
// Server provides the HTTP API and WebSocket endpoint.
type Server struct {
mu sync.RWMutex
status Status
addr string
mux *http.ServeMux
}
// NewServer creates an API server on the given address.
func NewServer(addr string) *Server {
s := &Server{
addr: addr,
mux: http.NewServeMux(),
}
s.registerRoutes()
return s
}
// Start begins serving the API.
func (s *Server) Start() error {
return http.ListenAndServe(s.addr, s.mux)
}
// UpdateStatus updates the bot status (called by the engine).
func (s *Server) UpdateStatus(status Status) {
s.mu.Lock()
defer s.mu.Unlock()
s.status = status
// TODO: Broadcast to WebSocket clients
}
func (s *Server) registerRoutes() {
s.mux.HandleFunc("GET /api/status", s.handleStatus)
s.mux.HandleFunc("POST /api/start", s.handleStart)
s.mux.HandleFunc("POST /api/stop", s.handleStop)
// TODO: Remaining routes
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(s.status)
}
func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) {
// TODO: Signal engine to start
w.WriteHeader(http.StatusAccepted)
}
func (s *Server) handleStop(w http.ResponseWriter, r *http.Request) {
// TODO: Signal engine to stop
w.WriteHeader(http.StatusAccepted)
}

View file

@ -0,0 +1,99 @@
// Package capture provides screen capture from various sources.
//
// Supports capturing from:
// - Local window (by title or handle)
// - VM display (VNC, Spice, or VM window on host)
// - Full screen / monitor region
//
// The capture interface is source-agnostic — the engine doesn't care
// where the frames come from.
package capture
import (
"image"
"time"
)
// Region defines a rectangular area to capture.
type Region struct {
X, Y, Width, Height int
}
// Source represents a capture source (window, VM, screen, etc.)
type Source interface {
// Name returns a human-readable description of the source.
Name() string
// Capture grabs a single frame.
Capture() (image.Image, error)
// CaptureRegion grabs a sub-region of the source.
CaptureRegion(r Region) (image.Image, error)
// Size returns the source dimensions.
Size() (width, height int)
// Close releases resources.
Close() error
}
// Stats tracks capture performance metrics.
type Stats struct {
FrameCount uint64
AvgCaptureMs float64
FPS float64
LastCapture time.Time
}
// Manager handles screen capture with performance tracking.
type Manager struct {
source Source
stats Stats
}
// NewManager creates a capture manager with the given source.
func NewManager(source Source) *Manager {
return &Manager{source: source}
}
// Capture grabs a frame and updates performance stats.
func (m *Manager) Capture() (image.Image, error) {
start := time.Now()
frame, err := m.source.Capture()
if err != nil {
return nil, err
}
elapsed := time.Since(start)
m.stats.FrameCount++
m.stats.LastCapture = start
// Rolling average
alpha := 0.1
ms := float64(elapsed.Microseconds()) / 1000.0
if m.stats.FrameCount == 1 {
m.stats.AvgCaptureMs = ms
} else {
m.stats.AvgCaptureMs = m.stats.AvgCaptureMs*(1-alpha) + ms*alpha
}
if m.stats.AvgCaptureMs > 0 {
m.stats.FPS = 1000.0 / m.stats.AvgCaptureMs
}
return frame, nil
}
// CaptureRegion grabs a sub-region.
func (m *Manager) CaptureRegion(r Region) (image.Image, error) {
return m.source.CaptureRegion(r)
}
// Stats returns current capture performance stats.
func (m *Manager) Stats() Stats {
return m.stats
}
// Close releases the capture source.
func (m *Manager) Close() error {
return m.source.Close()
}

View file

@ -0,0 +1,103 @@
// Package input — humanize.go provides human-like behavior randomization.
package input
import (
"image"
"math/rand"
"sync/atomic"
"time"
)
// HumanProfile defines behavior parameters for human-like input.
type HumanProfile struct {
ReactionMin time.Duration // min reaction delay
ReactionMax time.Duration // max reaction delay
MouseSpeedMin float64 // min pixels per second
MouseSpeedMax float64 // max pixels per second
ClickJitter int // pixel jitter on clicks
HesitationChance float64 // chance of extra pause (0-1)
HesitationMin time.Duration
HesitationMax time.Duration
}
// DefaultProfile returns a realistic human behavior profile.
func DefaultProfile() HumanProfile {
return HumanProfile{
ReactionMin: 150 * time.Millisecond,
ReactionMax: 450 * time.Millisecond,
MouseSpeedMin: 400,
MouseSpeedMax: 1200,
ClickJitter: 3,
HesitationChance: 0.1,
HesitationMin: 300 * time.Millisecond,
HesitationMax: 1200 * time.Millisecond,
}
}
// Humanizer applies human-like randomization to bot actions.
type Humanizer struct {
Profile HumanProfile
actionCount atomic.Int64
}
// NewHumanizer creates a humanizer with the given profile.
func NewHumanizer(profile HumanProfile) *Humanizer {
return &Humanizer{Profile: profile}
}
// ReactionDelay returns a randomized human-like reaction delay.
func (h *Humanizer) ReactionDelay() time.Duration {
minMs := h.Profile.ReactionMin.Milliseconds()
maxMs := h.Profile.ReactionMax.Milliseconds()
base := time.Duration(minMs+rand.Int63n(maxMs-minMs)) * time.Millisecond
// Occasional hesitation
if rand.Float64() < h.Profile.HesitationChance {
hesMinMs := h.Profile.HesitationMin.Milliseconds()
hesMaxMs := h.Profile.HesitationMax.Milliseconds()
base += time.Duration(hesMinMs+rand.Int63n(hesMaxMs-hesMinMs)) * time.Millisecond
}
// Slight fatigue factor
actions := float64(h.actionCount.Load())
fatigue := min(actions/1000.0, 0.3)
base = time.Duration(float64(base) * (1 + fatigue*rand.Float64()))
return base
}
// Wait pauses for a human-like reaction delay.
func (h *Humanizer) Wait() {
time.Sleep(h.ReactionDelay())
h.actionCount.Add(1)
}
// WaitMs pauses for baseMs ± varianceMs with human randomization.
func (h *Humanizer) WaitMs(baseMs, varianceMs int) {
actual := baseMs + rand.Intn(2*varianceMs+1) - varianceMs
if actual < 0 {
actual = 0
}
time.Sleep(time.Duration(actual) * time.Millisecond)
}
// JitterPosition adds random offset to a click position.
func (h *Humanizer) JitterPosition(pos image.Point) image.Point {
j := h.Profile.ClickJitter
return image.Point{
X: pos.X + rand.Intn(2*j+1) - j,
Y: pos.Y + rand.Intn(2*j+1) - j,
}
}
// MouseSpeed returns a randomized mouse movement speed.
func (h *Humanizer) MouseSpeed() float64 {
return h.Profile.MouseSpeedMin + rand.Float64()*(h.Profile.MouseSpeedMax-h.Profile.MouseSpeedMin)
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}

162
pkg/engine/input/input.go Normal file
View file

@ -0,0 +1,162 @@
// Package input provides human-like mouse and keyboard simulation.
//
// Mouse movement uses Bézier curves with natural acceleration.
// All inputs include randomized timing to mimic human behavior.
// Platform-specific backends: SendInput (Windows), X11/uinput (Linux).
package input
import (
"image"
"math"
"math/rand"
"time"
)
// MouseController handles human-like mouse movement and clicks.
type MouseController struct {
humanizer *Humanizer
}
// NewMouseController creates a mouse controller with the given humanizer.
func NewMouseController(h *Humanizer) *MouseController {
return &MouseController{humanizer: h}
}
// MoveTo moves the mouse to target using a Bézier curve.
func (m *MouseController) MoveTo(target image.Point) {
// Get current position
current := m.GetPosition()
// Generate Bézier control points
points := m.bezierPath(current, target)
// Animate along the path
speed := m.humanizer.MouseSpeed()
totalDist := m.pathLength(points)
steps := int(totalDist / speed * 1000) // ms-based steps
if steps < 5 {
steps = 5
}
for i := 1; i <= steps; i++ {
t := float64(i) / float64(steps)
// Ease in-out for natural acceleration
t = m.easeInOut(t)
p := m.evalBezier(points, t)
m.setPosition(p)
time.Sleep(time.Millisecond)
}
}
// Click performs a mouse click at the current position.
func (m *MouseController) Click() {
m.humanizer.Wait()
// TODO: Platform-specific click (SendInput / X11)
// Randomize hold duration
holdMs := 50 + rand.Intn(80)
time.Sleep(time.Duration(holdMs) * time.Millisecond)
}
// ClickAt moves to position and clicks.
func (m *MouseController) ClickAt(pos image.Point) {
jittered := m.humanizer.JitterPosition(pos)
m.MoveTo(jittered)
m.Click()
}
// RightClick performs a right mouse click.
func (m *MouseController) RightClick() {
m.humanizer.Wait()
holdMs := 50 + rand.Intn(80)
time.Sleep(time.Duration(holdMs) * time.Millisecond)
}
// GetPosition returns current mouse position.
func (m *MouseController) GetPosition() image.Point {
// TODO: Platform-specific implementation
return image.Point{}
}
func (m *MouseController) setPosition(p image.Point) {
// TODO: Platform-specific implementation
}
// bezierPath generates a cubic Bézier curve with randomized control points.
func (m *MouseController) bezierPath(start, end image.Point) [4]image.Point {
dx := float64(end.X - start.X)
dy := float64(end.Y - start.Y)
// Randomize control points for natural curvature
cp1 := image.Point{
X: start.X + int(dx*0.25+rand.Float64()*50-25),
Y: start.Y + int(dy*0.25+rand.Float64()*50-25),
}
cp2 := image.Point{
X: start.X + int(dx*0.75+rand.Float64()*50-25),
Y: start.Y + int(dy*0.75+rand.Float64()*50-25),
}
return [4]image.Point{start, cp1, cp2, end}
}
// evalBezier evaluates a cubic Bézier curve at parameter t.
func (m *MouseController) evalBezier(pts [4]image.Point, t float64) image.Point {
u := 1 - t
return image.Point{
X: int(u*u*u*float64(pts[0].X) + 3*u*u*t*float64(pts[1].X) + 3*u*t*t*float64(pts[2].X) + t*t*t*float64(pts[3].X)),
Y: int(u*u*u*float64(pts[0].Y) + 3*u*u*t*float64(pts[1].Y) + 3*u*t*t*float64(pts[2].Y) + t*t*t*float64(pts[3].Y)),
}
}
// easeInOut applies ease-in-out for natural mouse acceleration.
func (m *MouseController) easeInOut(t float64) float64 {
return t * t * (3 - 2*t)
}
func (m *MouseController) pathLength(pts [4]image.Point) float64 {
length := 0.0
prev := pts[0]
for i := 1; i <= 20; i++ {
t := float64(i) / 20.0
p := m.evalBezier(pts, t)
dx := float64(p.X - prev.X)
dy := float64(p.Y - prev.Y)
length += math.Sqrt(dx*dx + dy*dy)
prev = p
}
return length
}
// KeyboardController handles human-like keyboard input.
type KeyboardController struct {
humanizer *Humanizer
}
// NewKeyboardController creates a keyboard controller.
func NewKeyboardController(h *Humanizer) *KeyboardController {
return &KeyboardController{humanizer: h}
}
// PressKey presses and releases a key with human-like timing.
func (k *KeyboardController) PressKey(key string) {
k.humanizer.Wait()
// TODO: Platform-specific key press
holdMs := 30 + rand.Intn(70)
time.Sleep(time.Duration(holdMs) * time.Millisecond)
}
// TypeText types text with randomized inter-key delays.
func (k *KeyboardController) TypeText(text string) {
for _, ch := range text {
k.PressKey(string(ch))
delay := 30 + rand.Intn(120) // 30-150ms between keys
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
// HoldKey holds a key down for a duration.
func (k *KeyboardController) HoldKey(key string, durationMs int) {
// TODO: Platform-specific key down/up
variance := rand.Intn(durationMs / 5)
time.Sleep(time.Duration(durationMs+variance) * time.Millisecond)
}

120
pkg/engine/loot/filter.go Normal file
View file

@ -0,0 +1,120 @@
// Package loot provides a declarative, rule-based loot filter engine.
//
// Loot rules are defined in YAML and evaluated against detected items.
// Supports complex matching on type, name, rarity, properties, and more.
package loot
import (
"strings"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Priority levels for pickup ordering.
const (
PriorityLow = 1
PriorityNormal = 5
PriorityHigh = 8
PriorityCritical = 10
)
// Condition defines a match condition for a loot rule.
type Condition struct {
Type *string `yaml:"type,omitempty"` // "unique", "set", "rare", etc.
NameContains *string `yaml:"name_contains,omitempty"` // substring match
NameExact *string `yaml:"name_exact,omitempty"` // exact match
MinRarity *int `yaml:"min_rarity,omitempty"` // minimum rarity tier
MaxRarity *int `yaml:"max_rarity,omitempty"`
BaseType *string `yaml:"base_type,omitempty"` // e.g., "Diadem", "Monarch"
HasProperty *string `yaml:"has_property,omitempty"` // item has this property key
PropertyGTE map[string]int `yaml:"property_gte,omitempty"` // property >= value
}
// Action defines what to do when a rule matches.
type Action string
const (
ActionPickup Action = "pickup"
ActionIgnore Action = "ignore"
ActionAlert Action = "alert" // pickup + send notification
)
// Rule is a single loot filter rule.
type Rule struct {
Name string `yaml:"name,omitempty"`
Match Condition `yaml:"match"`
Action Action `yaml:"action"`
Priority int `yaml:"priority,omitempty"`
}
// RuleEngine evaluates items against a list of rules.
type RuleEngine struct {
Rules []Rule
}
// NewRuleEngine creates a rule engine from a list of rules.
func NewRuleEngine(rules []Rule) *RuleEngine {
return &RuleEngine{Rules: rules}
}
// Evaluate checks an item against all rules.
// Returns the first matching rule's action and priority.
func (e *RuleEngine) Evaluate(item plugin.DetectedItem) (action Action, priority int, matched bool) {
for _, rule := range e.Rules {
if e.matches(rule.Match, item) {
p := rule.Priority
if p == 0 {
p = PriorityNormal
}
return rule.Action, p, true
}
}
return ActionIgnore, 0, false
}
// ShouldPickup implements plugin.LootFilter.
func (e *RuleEngine) ShouldPickup(item plugin.DetectedItem) (bool, int) {
action, priority, matched := e.Evaluate(item)
if !matched {
return false, 0
}
return action == ActionPickup || action == ActionAlert, priority
}
// ShouldAlert implements plugin.LootFilter.
func (e *RuleEngine) ShouldAlert(item plugin.DetectedItem) bool {
action, _, matched := e.Evaluate(item)
return matched && action == ActionAlert
}
func (e *RuleEngine) matches(cond Condition, item plugin.DetectedItem) bool {
if cond.Type != nil && !strings.EqualFold(item.Type, *cond.Type) {
return false
}
if cond.NameContains != nil && !strings.Contains(strings.ToLower(item.Name), strings.ToLower(*cond.NameContains)) {
return false
}
if cond.NameExact != nil && !strings.EqualFold(item.Name, *cond.NameExact) {
return false
}
if cond.MinRarity != nil && item.Rarity < *cond.MinRarity {
return false
}
if cond.MaxRarity != nil && item.Rarity > *cond.MaxRarity {
return false
}
if cond.BaseType != nil && !strings.EqualFold(item.Properties["base_type"], *cond.BaseType) {
return false
}
if cond.HasProperty != nil {
if _, ok := item.Properties[*cond.HasProperty]; !ok {
return false
}
}
for key, minVal := range cond.PropertyGTE {
// TODO: Parse property value as int and compare
_ = key
_ = minVal
}
return true
}

108
pkg/engine/safety/safety.go Normal file
View file

@ -0,0 +1,108 @@
// Package safety provides anti-detection measures: session timing,
// break scheduling, and behavioral pattern randomization.
package safety
import (
"math/rand"
"time"
)
// SessionConfig defines play session parameters.
type SessionConfig struct {
MinSessionHours float64
MaxSessionHours float64
MinBreakMinutes float64
MaxBreakMinutes float64
MaxDailyHours float64
MicroBreakMinSec int
MicroBreakMaxSec int
MicroBreakIntervalMinSec int
MicroBreakIntervalMaxSec int
}
// DefaultSessionConfig returns realistic session timing.
func DefaultSessionConfig() SessionConfig {
return SessionConfig{
MinSessionHours: 1.0,
MaxSessionHours: 4.0,
MinBreakMinutes: 10.0,
MaxBreakMinutes: 45.0,
MaxDailyHours: 12.0,
MicroBreakMinSec: 2,
MicroBreakMaxSec: 8,
MicroBreakIntervalMinSec: 120,
MicroBreakIntervalMaxSec: 300,
}
}
// SessionTimer manages play session duration and breaks.
type SessionTimer struct {
config SessionConfig
sessionStart time.Time
dailyPlaytime time.Duration
targetDuration time.Duration
nextMicroBreak time.Time
}
// NewSessionTimer creates a session timer.
func NewSessionTimer(config SessionConfig) *SessionTimer {
st := &SessionTimer{
config: config,
sessionStart: time.Now(),
}
st.targetDuration = st.rollSessionDuration()
st.nextMicroBreak = st.scheduleMicroBreak()
return st
}
// ShouldStopSession returns true if the current session should end.
func (s *SessionTimer) ShouldStopSession() bool {
elapsed := time.Since(s.sessionStart)
if elapsed >= s.targetDuration {
return true
}
if s.dailyPlaytime+elapsed >= time.Duration(s.config.MaxDailyHours*float64(time.Hour)) {
return true
}
return false
}
// ShouldMicroBreak returns the break duration if it's time, or 0.
func (s *SessionTimer) ShouldMicroBreak() time.Duration {
if time.Now().After(s.nextMicroBreak) {
duration := time.Duration(s.config.MicroBreakMinSec+rand.Intn(s.config.MicroBreakMaxSec-s.config.MicroBreakMinSec+1)) * time.Second
s.nextMicroBreak = s.scheduleMicroBreak()
return duration
}
return 0
}
// BreakDuration returns a randomized long break duration.
func (s *SessionTimer) BreakDuration() time.Duration {
minMs := int(s.config.MinBreakMinutes * 60 * 1000)
maxMs := int(s.config.MaxBreakMinutes * 60 * 1000)
return time.Duration(minMs+rand.Intn(maxMs-minMs)) * time.Millisecond
}
// StartNewSession resets for a new play session.
func (s *SessionTimer) StartNewSession() {
s.dailyPlaytime += time.Since(s.sessionStart)
s.sessionStart = time.Now()
s.targetDuration = s.rollSessionDuration()
}
// Elapsed returns time in current session.
func (s *SessionTimer) Elapsed() time.Duration {
return time.Since(s.sessionStart)
}
func (s *SessionTimer) rollSessionDuration() time.Duration {
minMs := int(s.config.MinSessionHours * 3600 * 1000)
maxMs := int(s.config.MaxSessionHours * 3600 * 1000)
return time.Duration(minMs+rand.Intn(maxMs-minMs)) * time.Millisecond
}
func (s *SessionTimer) scheduleMicroBreak() time.Time {
interval := s.config.MicroBreakIntervalMinSec + rand.Intn(s.config.MicroBreakIntervalMaxSec-s.config.MicroBreakIntervalMinSec+1)
return time.Now().Add(time.Duration(interval) * time.Second)
}

86
pkg/engine/state/state.go Normal file
View file

@ -0,0 +1,86 @@
// Package state provides game state machine management with event callbacks.
package state
import (
"image"
"sync"
"time"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Transition records a state change.
type Transition struct {
From plugin.GameState
To plugin.GameState
Timestamp time.Time
}
// Manager tracks game state and fires callbacks on transitions.
type Manager struct {
mu sync.RWMutex
current plugin.GameState
previous plugin.GameState
enterTime time.Time
history []Transition
callbacks map[plugin.GameState][]func(Transition)
detector plugin.GameDetector
}
// NewManager creates a state manager with the given detector.
func NewManager(detector plugin.GameDetector) *Manager {
return &Manager{
current: plugin.StateUnknown,
enterTime: time.Now(),
callbacks: make(map[plugin.GameState][]func(Transition)),
detector: detector,
}
}
// Update analyzes the current frame and updates state.
func (m *Manager) Update(frame image.Image) plugin.GameState {
newState := m.detector.DetectState(frame)
m.mu.Lock()
defer m.mu.Unlock()
if newState != m.current {
t := Transition{
From: m.current,
To: newState,
Timestamp: time.Now(),
}
m.history = append(m.history, t)
m.previous = m.current
m.current = newState
m.enterTime = time.Now()
// Fire callbacks outside lock? For simplicity, keep in lock for now.
for _, cb := range m.callbacks[newState] {
cb(t)
}
}
return m.current
}
// Current returns the current game state.
func (m *Manager) Current() plugin.GameState {
m.mu.RLock()
defer m.mu.RUnlock()
return m.current
}
// TimeInState returns how long we've been in the current state.
func (m *Manager) TimeInState() time.Duration {
m.mu.RLock()
defer m.mu.RUnlock()
return time.Since(m.enterTime)
}
// OnState registers a callback for entering a specific state.
func (m *Manager) OnState(state plugin.GameState, cb func(Transition)) {
m.mu.Lock()
defer m.mu.Unlock()
m.callbacks[state] = append(m.callbacks[state], cb)
}

View file

@ -0,0 +1,88 @@
// Package vision provides computer vision utilities for game screen analysis.
//
// Uses GoCV (OpenCV bindings for Go) for template matching, color detection,
// and contour analysis. Designed for high-throughput real-time analysis.
package vision
import (
"image"
"image/color"
)
// Match represents a detected element on screen.
type Match struct {
Position image.Point
BBox image.Rectangle
Confidence float64
Label string
}
// Template is a pre-loaded image template for matching.
type Template struct {
Name string
Image image.Image
Width int
Height int
}
// ColorRange defines an HSV color range for detection.
type ColorRange struct {
LowerH, LowerS, LowerV int
UpperH, UpperS, UpperV int
}
// Pipeline processes frames through a series of vision operations.
type Pipeline struct {
templates map[string]*Template
threshold float64
}
// NewPipeline creates a vision pipeline with the given confidence threshold.
func NewPipeline(threshold float64) *Pipeline {
return &Pipeline{
templates: make(map[string]*Template),
threshold: threshold,
}
}
// LoadTemplate loads a template image for matching.
func (p *Pipeline) LoadTemplate(name string, img image.Image) {
bounds := img.Bounds()
p.templates[name] = &Template{
Name: name,
Image: img,
Width: bounds.Dx(),
Height: bounds.Dy(),
}
}
// FindTemplate searches for a template in the frame.
// Returns the best match above threshold, or nil.
func (p *Pipeline) FindTemplate(frame image.Image, templateName string) *Match {
// TODO: Implement with GoCV matchTemplate
// This is a stub — actual implementation needs gocv.MatchTemplate
return nil
}
// FindAllTemplates finds all matches of a template above threshold.
func (p *Pipeline) FindAllTemplates(frame image.Image, templateName string) []Match {
// TODO: Implement with GoCV + NMS
return nil
}
// FindByColor detects regions matching an HSV color range.
func (p *Pipeline) FindByColor(frame image.Image, colorRange ColorRange, minArea int) []Match {
// TODO: Implement with GoCV inRange + findContours
return nil
}
// ReadBarPercentage reads a horizontal bar's fill level (health, mana, xp).
func (p *Pipeline) ReadBarPercentage(frame image.Image, barRegion image.Rectangle, filledColor ColorRange) float64 {
// TODO: Implement — scan columns for filled color ratio
return 0.0
}
// GetPixelColor returns the color at a specific pixel.
func (p *Pipeline) GetPixelColor(frame image.Image, x, y int) color.Color {
return frame.At(x, y)
}

148
pkg/plugin/plugin.go Normal file
View file

@ -0,0 +1,148 @@
// Package plugin defines the interfaces that game plugins must implement.
//
// The engine is game-agnostic. All game-specific logic lives in plugins
// that implement these interfaces. Adding a new game = a new plugin.
package plugin
import (
"context"
"image"
)
// GameState represents the current state of the game (menu, loading, in-game, etc.)
type GameState string
const (
StateUnknown GameState = "unknown"
StateLoading GameState = "loading"
StateMainMenu GameState = "main_menu"
StateCharacterSelect GameState = "character_select"
StateInGame GameState = "in_game"
StateInventory GameState = "inventory"
StateDead GameState = "dead"
StateDisconnected GameState = "disconnected"
)
// DetectedItem represents an item found on screen.
type DetectedItem struct {
Name string
Type string // "unique", "set", "rare", "rune", "normal"
Rarity int // game-specific rarity tier
Position image.Point
BBox image.Rectangle
Confidence float64
Properties map[string]string // parsed item properties
}
// VitalStats represents character health/mana/etc.
type VitalStats struct {
HealthPct float64 // 0.0 - 1.0
ManaPct float64
XPPct float64
}
// GameDetector detects the current game state from a screen capture.
type GameDetector interface {
// DetectState analyzes a screenshot and returns the current game state.
DetectState(frame image.Image) GameState
// ReadVitals reads health, mana, and other vital stats from the screen.
ReadVitals(frame image.Image) VitalStats
// IsInGame returns true if the player is in an active game session.
IsInGame(frame image.Image) bool
}
// ScreenReader extracts game information from screenshots.
type ScreenReader interface {
// FindItems detects item labels/drops on screen.
FindItems(frame image.Image) []DetectedItem
// FindPortal locates a town portal on screen.
FindPortal(frame image.Image) (image.Point, bool)
// FindEnemies detects enemy positions (optional, not all games need this).
FindEnemies(frame image.Image) []image.Point
// ReadText extracts text from a screen region (OCR).
ReadText(frame image.Image, region image.Rectangle) string
}
// Routine represents an automated game routine (e.g., a farming run).
type Routine interface {
// Name returns the routine's display name.
Name() string
// Run executes one iteration of the routine.
// Returns nil on success, error on failure (bot will handle recovery).
Run(ctx context.Context) error
// Phase returns the current phase name for status display.
Phase() string
}
// LootFilter decides which items to pick up.
type LootFilter interface {
// ShouldPickup evaluates an item against the filter rules.
ShouldPickup(item DetectedItem) (pickup bool, priority int)
// ShouldAlert returns true if this item warrants a notification.
ShouldAlert(item DetectedItem) bool
}
// Plugin is the main interface a game plugin must implement.
type Plugin interface {
// Info returns plugin metadata.
Info() PluginInfo
// Init initializes the plugin with engine services.
Init(services EngineServices) error
// Detector returns the game state detector.
Detector() GameDetector
// Reader returns the screen reader.
Reader() ScreenReader
// Routines returns available farming routines.
Routines() []Routine
// DefaultLootFilter returns the default loot filter.
DefaultLootFilter() LootFilter
}
// PluginInfo describes a game plugin.
type PluginInfo struct {
ID string // e.g., "d2r"
Name string // e.g., "Diablo II: Resurrected"
Version string
Description string
Resolution image.Point // target resolution, e.g., (1920, 1080)
}
// EngineServices provides access to engine capabilities for plugins.
type EngineServices interface {
// Capture returns the current screen frame.
Capture() image.Image
// Click sends a mouse click at the given position.
Click(pos image.Point)
// MoveMouse moves the mouse to the given position with human-like movement.
MoveMouse(pos image.Point)
// PressKey sends a key press.
PressKey(key string)
// TypeText types text with human-like delays.
TypeText(text string)
// Wait pauses for a human-like delay.
Wait()
// WaitMs pauses for a specific duration with randomization.
WaitMs(baseMs int, varianceMs int)
// Log logs a message associated with the plugin.
Log(level string, msg string, args ...any)
}