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

107
plugins/d2r/config.go Normal file
View file

@ -0,0 +1,107 @@
// D2R-specific configuration: screen regions, colors, timings.
package d2r
import "image"
// ScreenRegions defines UI element positions at 1920x1080.
type ScreenRegions struct {
HealthOrb image.Rectangle
ManaOrb image.Rectangle
XPBar image.Rectangle
Belt image.Rectangle
Minimap image.Rectangle
Inventory image.Rectangle
Stash image.Rectangle
SkillLeft image.Rectangle
SkillRight image.Rectangle
}
// HSVRange defines a color range in HSV space.
type HSVRange struct {
LowerH, LowerS, LowerV int
UpperH, UpperS, UpperV int
}
// Colors defines HSV ranges for game elements.
type Colors struct {
HealthFilled HSVRange
ManaFilled HSVRange
ItemUnique HSVRange
ItemSet HSVRange
ItemRare HSVRange
ItemRuneword HSVRange
PortalBlue HSVRange
}
// Timings defines game-specific delay constants.
type Timings struct {
LoadingScreenMaxMs int
TownPortalCastMs int
TeleportDelayMs int
PotionCooldownMs int
PickupDelayMs int
}
// Config holds all D2R-specific configuration.
type Config struct {
Resolution image.Point
Regions ScreenRegions
Colors Colors
Timings Timings
// Loot settings
PickupUniques bool
PickupSets bool
PickupRares bool
PickupRunes bool
MinRuneTier int
PickupGems bool
// Safety thresholds (0.0 - 1.0)
HealthPotionThreshold float64
ManaPotionThreshold float64
ChickenThreshold float64 // exit game if health below this
}
// DefaultConfig returns the default D2R config for 1920x1080.
func DefaultConfig() Config {
return Config{
Resolution: image.Point{X: 1920, Y: 1080},
Regions: ScreenRegions{
HealthOrb: image.Rect(28, 545, 198, 715),
ManaOrb: image.Rect(1722, 545, 1892, 715),
XPBar: image.Rect(0, 1058, 1920, 1080),
Belt: image.Rect(838, 1010, 1082, 1058),
Minimap: image.Rect(1600, 0, 1920, 320),
Inventory: image.Rect(960, 330, 1490, 770),
Stash: image.Rect(430, 330, 960, 770),
SkillLeft: image.Rect(194, 1036, 246, 1088),
SkillRight: image.Rect(1674, 1036, 1726, 1088),
},
Colors: Colors{
HealthFilled: HSVRange{0, 100, 100, 10, 255, 255},
ManaFilled: HSVRange{100, 100, 100, 130, 255, 255},
ItemUnique: HSVRange{15, 100, 180, 30, 255, 255},
ItemSet: HSVRange{35, 100, 150, 55, 255, 255},
ItemRare: HSVRange{15, 50, 200, 25, 150, 255},
ItemRuneword: HSVRange{15, 100, 180, 30, 255, 255},
PortalBlue: HSVRange{90, 150, 150, 120, 255, 255},
},
Timings: Timings{
LoadingScreenMaxMs: 15000,
TownPortalCastMs: 3500,
TeleportDelayMs: 150,
PotionCooldownMs: 1000,
PickupDelayMs: 300,
},
PickupUniques: true,
PickupSets: true,
PickupRares: true,
PickupRunes: true,
MinRuneTier: 10,
PickupGems: false,
HealthPotionThreshold: 0.5,
ManaPotionThreshold: 0.3,
ChickenThreshold: 0.2,
}
}

77
plugins/d2r/detector.go Normal file
View file

@ -0,0 +1,77 @@
// Game state detection for D2R.
package d2r
import (
"image"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Detector implements plugin.GameDetector for D2R.
type Detector struct {
config Config
}
// NewDetector creates a D2R state detector.
func NewDetector(config Config) *Detector {
return &Detector{config: config}
}
// DetectState analyzes a screenshot and returns the current game state.
func (d *Detector) DetectState(frame image.Image) plugin.GameState {
// Priority-based detection:
// 1. Check for loading screen
// 2. Check for main menu
// 3. Check for character select
// 4. Check for in-game (health orb visible)
// 5. Check for death screen
if d.isLoading(frame) {
return plugin.StateLoading
}
if d.isMainMenu(frame) {
return plugin.StateMainMenu
}
if d.isCharacterSelect(frame) {
return plugin.StateCharacterSelect
}
if d.isInGame(frame) {
vitals := d.ReadVitals(frame)
if vitals.HealthPct == 0 {
return plugin.StateDead
}
return plugin.StateInGame
}
return plugin.StateUnknown
}
// ReadVitals reads health and mana from the orbs.
func (d *Detector) ReadVitals(frame image.Image) plugin.VitalStats {
// TODO: Analyze health/mana orb regions using color detection
return plugin.VitalStats{}
}
// IsInGame returns true if health orb is visible.
func (d *Detector) IsInGame(frame image.Image) bool {
return d.isInGame(frame)
}
func (d *Detector) isLoading(frame image.Image) bool {
// TODO: Check for loading screen (mostly black with loading bar)
return false
}
func (d *Detector) isMainMenu(frame image.Image) bool {
// TODO: Template match main menu elements
return false
}
func (d *Detector) isCharacterSelect(frame image.Image) bool {
// TODO: Template match character select screen
return false
}
func (d *Detector) isInGame(frame image.Image) bool {
// TODO: Check if health orb region contains red pixels
return false
}

View file

@ -0,0 +1,56 @@
# Default D2R loot filter rules.
# Rules are evaluated top-to-bottom; first match wins.
rules:
# High-value uniques — always alert
- name: "GG Uniques"
match:
type: unique
name_contains: "Shako"
action: alert
priority: 10
- name: "High Runes"
match:
type: rune
min_rarity: 20 # Vex+
action: alert
priority: 10
# Standard pickups
- name: "All Uniques"
match:
type: unique
action: pickup
priority: 8
- name: "All Sets"
match:
type: set
action: pickup
priority: 7
- name: "Mid Runes"
match:
type: rune
min_rarity: 10 # Lem+
action: pickup
priority: 6
- name: "GG Rare Bases"
match:
type: rare
base_type: "Diadem"
action: pickup
priority: 5
- name: "GG Rare Bases"
match:
type: rare
base_type: "Circlet"
action: pickup
priority: 5
# Ignore everything else
- name: "Default Ignore"
match: {}
action: ignore

65
plugins/d2r/plugin.go Normal file
View file

@ -0,0 +1,65 @@
// Package d2r implements the Diablo II: Resurrected game plugin.
package d2r
import (
"image"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Plugin implements plugin.Plugin for D2R.
type Plugin struct {
config Config
services plugin.EngineServices
detector *Detector
reader *Reader
}
// New creates a new D2R plugin with default config.
func New() *Plugin {
return &Plugin{
config: DefaultConfig(),
}
}
// Info returns plugin metadata.
func (p *Plugin) Info() plugin.PluginInfo {
return plugin.PluginInfo{
ID: "d2r",
Name: "Diablo II: Resurrected",
Version: "0.1.0",
Description: "Bot plugin for Diablo II: Resurrected — MF runs, rune farming, and more",
Resolution: image.Point{X: 1920, Y: 1080},
}
}
// Init initializes the plugin with engine services.
func (p *Plugin) Init(services plugin.EngineServices) error {
p.services = services
p.detector = NewDetector(p.config)
p.reader = NewReader(p.config)
return nil
}
// Detector returns the game state detector.
func (p *Plugin) Detector() plugin.GameDetector {
return p.detector
}
// Reader returns the screen reader.
func (p *Plugin) Reader() plugin.ScreenReader {
return p.reader
}
// Routines returns available farming routines.
func (p *Plugin) Routines() []plugin.Routine {
return []plugin.Routine{
// TODO: Initialize routines with plugin services
}
}
// DefaultLootFilter returns the default D2R loot filter.
func (p *Plugin) DefaultLootFilter() plugin.LootFilter {
// TODO: Return default rule engine
return nil
}

47
plugins/d2r/reader.go Normal file
View file

@ -0,0 +1,47 @@
// Screen reader for D2R — extracts game information from screenshots.
package d2r
import (
"image"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Reader implements plugin.ScreenReader for D2R.
type Reader struct {
config Config
}
// NewReader creates a D2R screen reader.
func NewReader(config Config) *Reader {
return &Reader{config: config}
}
// FindItems detects item labels on the ground.
func (r *Reader) FindItems(frame image.Image) []plugin.DetectedItem {
// TODO: Detect colored item text labels
// - Gold text = unique
// - Green text = set
// - Yellow text = rare
// - Orange text = runeword / crafted
// - White/grey = normal/magic
return nil
}
// FindPortal locates a town portal on screen.
func (r *Reader) FindPortal(frame image.Image) (image.Point, bool) {
// TODO: Detect blue portal glow
return image.Point{}, false
}
// FindEnemies detects enemy positions.
func (r *Reader) FindEnemies(frame image.Image) []image.Point {
// TODO: Enemy health bar detection
return nil
}
// ReadText extracts text from a screen region.
func (r *Reader) ReadText(frame image.Image, region image.Rectangle) string {
// TODO: OCR on the given region
return ""
}

View file

@ -0,0 +1,135 @@
// Mephisto farming routine for D2R.
//
// Classic MF run: Create game → WP to Durance 2 → Teleport to Durance 3 →
// Moat trick Mephisto → Loot → TP to town → Stash → Exit → Repeat
package mephisto
import (
"context"
"fmt"
"sync"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
)
// Phase represents the current phase of a Mephisto run.
type Phase string
const (
PhaseCreateGame Phase = "create_game"
PhaseTeleport Phase = "teleport_to_durance"
PhaseFindBoss Phase = "find_mephisto"
PhaseKill Phase = "kill"
PhaseLoot Phase = "loot"
PhaseTownPortal Phase = "town_portal"
PhaseStash Phase = "stash"
PhaseExitGame Phase = "exit_game"
)
// Routine implements plugin.Routine for Mephisto runs.
type Routine struct {
mu sync.RWMutex
services plugin.EngineServices
phase Phase
runCount int
}
// New creates a Mephisto routine.
func New(services plugin.EngineServices) *Routine {
return &Routine{services: services}
}
// Name returns the routine name.
func (r *Routine) Name() string { return "Mephisto" }
// Phase returns current phase for status display.
func (r *Routine) Phase() string {
r.mu.RLock()
defer r.mu.RUnlock()
return string(r.phase)
}
// Run executes one Mephisto run.
func (r *Routine) Run(ctx context.Context) error {
phases := []struct {
phase Phase
handler func(ctx context.Context) error
}{
{PhaseCreateGame, r.createGame},
{PhaseTeleport, r.teleportToDurance},
{PhaseFindBoss, r.findMephisto},
{PhaseKill, r.killMephisto},
{PhaseLoot, r.lootItems},
{PhaseTownPortal, r.townPortal},
{PhaseStash, r.stashItems},
{PhaseExitGame, r.exitGame},
}
for _, p := range phases {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
r.mu.Lock()
r.phase = p.phase
r.mu.Unlock()
r.services.Log("info", fmt.Sprintf("Phase: %s", p.phase))
if err := p.handler(ctx); err != nil {
return fmt.Errorf("phase %s failed: %w", p.phase, err)
}
r.services.Wait()
}
r.mu.Lock()
r.runCount++
r.mu.Unlock()
return nil
}
func (r *Routine) createGame(ctx context.Context) error {
// TODO: Navigate lobby → create game with randomized name
return nil
}
func (r *Routine) teleportToDurance(ctx context.Context) error {
// TODO: Open WP → Durance of Hate 2 → teleport to level 3 entrance
return nil
}
func (r *Routine) findMephisto(ctx context.Context) error {
// TODO: Teleport around Durance 3 to find Mephisto
// Position for moat trick (safe spot across the moat)
return nil
}
func (r *Routine) killMephisto(ctx context.Context) error {
// TODO: Cast offensive spells from moat trick position
// Monitor health, use potions if needed
return nil
}
func (r *Routine) lootItems(ctx context.Context) error {
// TODO: Teleport to body, detect items, pick up per loot filter
return nil
}
func (r *Routine) townPortal(ctx context.Context) error {
// TODO: Cast TP, click portal to go to town
return nil
}
func (r *Routine) stashItems(ctx context.Context) error {
// TODO: If inventory has items worth stashing, open stash and transfer
return nil
}
func (r *Routine) exitGame(ctx context.Context) error {
// TODO: ESC → Save & Exit
return nil
}