Rewrite to Go: engine, plugin system, D2R plugin, API, loot filter
This commit is contained in:
parent
e0282a7111
commit
3b363192f2
60 changed files with 1576 additions and 3407 deletions
99
pkg/engine/capture/capture.go
Normal file
99
pkg/engine/capture/capture.go
Normal 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()
|
||||
}
|
||||
103
pkg/engine/input/humanize.go
Normal file
103
pkg/engine/input/humanize.go
Normal 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
162
pkg/engine/input/input.go
Normal 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
120
pkg/engine/loot/filter.go
Normal 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
108
pkg/engine/safety/safety.go
Normal 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
86
pkg/engine/state/state.go
Normal 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)
|
||||
}
|
||||
88
pkg/engine/vision/vision.go
Normal file
88
pkg/engine/vision/vision.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue