Add missing cmd/iso-bot files

This commit is contained in:
Hoid 2026-02-14 10:39:03 +00:00
parent 6a9562c406
commit 4e4acaa78d
2 changed files with 358 additions and 0 deletions

214
cmd/iso-bot/gen_testdata.go Normal file
View file

@ -0,0 +1,214 @@
//go:build ignore
// Generate test data for development.
// Run with: go run gen_testdata.go
package main
import (
"image"
"image/color"
"image/draw"
"image/png"
"log"
"os"
"path/filepath"
)
func main() {
// Create testdata directory
if err := os.MkdirAll("testdata", 0755); err != nil {
log.Fatalf("Failed to create testdata directory: %v", err)
}
// Generate synthetic D2R screenshot
img := generateD2RScreenshot()
// Save as PNG
filename := filepath.Join("testdata", "d2r_1080p.png")
file, err := os.Create(filename)
if err != nil {
log.Fatalf("Failed to create test image file: %v", err)
}
defer file.Close()
if err := png.Encode(file, img); err != nil {
log.Fatalf("Failed to encode test image: %v", err)
}
log.Printf("Generated test image: %s", filename)
}
func generateD2RScreenshot() image.Image {
// Create 1920x1080 black image
img := image.NewRGBA(image.Rect(0, 0, 1920, 1080))
// Fill with dark background (simulating game background)
darkGray := color.RGBA{20, 20, 25, 255}
draw.Draw(img, img.Bounds(), &image.Uniform{darkGray}, image.Point{}, draw.Src)
// Add some noise/texture to make it more realistic
for y := 0; y < 1080; y += 5 {
for x := 0; x < 1920; x += 5 {
noise := color.RGBA{
uint8(15 + (x+y)%20),
uint8(15 + (x+y)%25),
uint8(20 + (x+y)%30),
255,
}
fillRect(img, image.Rect(x, y, x+3, y+3), noise)
}
}
// Health orb (red circle) - Region: (28, 545, 198, 715)
healthRegion := image.Rect(28, 545, 198, 715)
healthColor := color.RGBA{220, 20, 20, 255} // Bright red for health
drawOrb(img, healthRegion, healthColor, 0.75) // 75% filled
// Mana orb (blue circle) - Region: (1722, 545, 1892, 715)
manaRegion := image.Rect(1722, 545, 1892, 715)
manaColor := color.RGBA{20, 20, 220, 255} // Bright blue for mana
drawOrb(img, manaRegion, manaColor, 0.60) // 60% filled
// XP bar - Region: (0, 1058, 1920, 1080)
xpRegion := image.Rect(0, 1058, 1920, 1080)
xpColor := color.RGBA{255, 215, 0, 255} // Gold color
fillRect(img, xpRegion, color.RGBA{40, 40, 40, 255}) // Dark background
// Partial XP fill
xpFillWidth := int(float64(xpRegion.Dx()) * 0.3) // 30% XP
xpFillRegion := image.Rect(xpRegion.Min.X, xpRegion.Min.Y, xpRegion.Min.X+xpFillWidth, xpRegion.Max.Y)
fillRect(img, xpFillRegion, xpColor)
// Belt - Region: (838, 1010, 1082, 1058)
beltRegion := image.Rect(838, 1010, 1082, 1058)
beltColor := color.RGBA{80, 60, 40, 255} // Brown belt background
fillRect(img, beltRegion, beltColor)
// Add some belt slots
slotWidth := beltRegion.Dx() / 4
for i := 0; i < 4; i++ {
slotX := beltRegion.Min.X + i*slotWidth + 5
slotRegion := image.Rect(slotX, beltRegion.Min.Y+5, slotX+slotWidth-10, beltRegion.Max.Y-5)
slotColor := color.RGBA{60, 45, 30, 255}
fillRect(img, slotRegion, slotColor)
// Add a potion in first slot
if i == 0 {
potionColor := color.RGBA{200, 50, 50, 255} // Red potion
potionRegion := image.Rect(slotX+5, beltRegion.Min.Y+10, slotX+slotWidth-15, beltRegion.Max.Y-10)
fillRect(img, potionRegion, potionColor)
}
}
// Minimap - Region: (1600, 0, 1920, 320)
minimapRegion := image.Rect(1600, 0, 1920, 320)
minimapColor := color.RGBA{40, 40, 50, 255} // Dark blue-gray
fillRect(img, minimapRegion, minimapColor)
// Add some minimap elements
// Player dot (center)
centerX := minimapRegion.Min.X + minimapRegion.Dx()/2
centerY := minimapRegion.Min.Y + minimapRegion.Dy()/2
playerDot := image.Rect(centerX-3, centerY-3, centerX+3, centerY+3)
fillRect(img, playerDot, color.RGBA{255, 255, 255, 255}) // White player dot
// Some terrain
for i := 0; i < 50; i++ {
x := minimapRegion.Min.X + (i*7)%minimapRegion.Dx()
y := minimapRegion.Min.Y + (i*11)%minimapRegion.Dy()
terrainColor := color.RGBA{60 + uint8(i%30), 80 + uint8(i%20), 40 + uint8(i%25), 255}
terrainDot := image.Rect(x, y, x+2, y+2)
fillRect(img, terrainDot, terrainColor)
}
// Skills - left and right
// Left skill: (194, 1036, 246, 1088)
leftSkillRegion := image.Rect(194, 1036, 246, 1088)
skillColor := color.RGBA{100, 100, 150, 255}
fillRect(img, leftSkillRegion, skillColor)
// Right skill: (1674, 1036, 1726, 1088)
rightSkillRegion := image.Rect(1674, 1036, 1726, 1088)
fillRect(img, rightSkillRegion, skillColor)
// Add some item drops on the ground (colored text rectangles)
items := []struct {
text string
color color.RGBA
x, y int
}{
{"Shako", color.RGBA{255, 165, 0, 255}, 400, 600}, // Orange unique
{"Rune", color.RGBA{255, 215, 0, 255}, 500, 650}, // Gold
{"Magic Item", color.RGBA{100, 100, 255, 255}, 600, 700}, // Blue magic
}
for _, item := range items {
textRegion := image.Rect(item.x, item.y, item.x+len(item.text)*8, item.y+15)
fillRect(img, textRegion, item.color)
}
return img
}
// drawOrb draws a circular orb with fill percentage
func drawOrb(img *image.RGBA, region image.Rectangle, fillColor color.RGBA, fillPct float64) {
centerX := region.Min.X + region.Dx()/2
centerY := region.Min.Y + region.Dy()/2
radius := region.Dx() / 2
// Draw orb background (dark)
darkColor := color.RGBA{40, 40, 40, 255}
fillCircle(img, centerX, centerY, radius, darkColor)
// Draw border
borderColor := color.RGBA{100, 100, 100, 255}
drawCircleBorder(img, centerX, centerY, radius, borderColor, 2)
// Draw filled portion (from bottom up)
if fillPct > 0 {
fillHeight := int(float64(region.Dy()) * fillPct)
fillStartY := region.Max.Y - fillHeight
// Fill the circle partially
for y := fillStartY; y < region.Max.Y; y++ {
for x := region.Min.X; x < region.Max.X; x++ {
dx := x - centerX
dy := y - centerY
if dx*dx + dy*dy <= radius*radius {
img.Set(x, y, fillColor)
}
}
}
}
}
// fillCircle fills a circle with the given color
func fillCircle(img *image.RGBA, centerX, centerY, radius int, col color.RGBA) {
for y := centerY - radius; y <= centerY + radius; y++ {
for x := centerX - radius; x <= centerX + radius; x++ {
dx := x - centerX
dy := y - centerY
if dx*dx + dy*dy <= radius*radius {
img.Set(x, y, col)
}
}
}
}
// drawCircleBorder draws a circle border
func drawCircleBorder(img *image.RGBA, centerX, centerY, radius int, col color.RGBA, thickness int) {
for y := centerY - radius - thickness; y <= centerY + radius + thickness; y++ {
for x := centerX - radius - thickness; x <= centerX + radius + thickness; x++ {
dx := x - centerX
dy := y - centerY
dist := dx*dx + dy*dy
if dist <= (radius+thickness)*(radius+thickness) && dist >= (radius-thickness)*(radius-thickness) {
img.Set(x, y, col)
}
}
}
}
// fillRect fills a rectangle with the given color
func fillRect(img *image.RGBA, rect image.Rectangle, col color.RGBA) {
draw.Draw(img, rect, &image.Uniform{col}, image.Point{}, draw.Src)
}

144
cmd/iso-bot/main.go Normal file
View file

@ -0,0 +1,144 @@
// iso-bot — isometric game bot engine.
//
// Usage:
//
// iso-bot --game d2r --routine mephisto --api :8080
// iso-bot --dev --api :8080 --capture-file testdata/d2r_1080p.png
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"git.cloonar.com/openclawd/iso-bot/pkg/api"
"git.cloonar.com/openclawd/iso-bot/pkg/engine"
"git.cloonar.com/openclawd/iso-bot/pkg/engine/capture/backends"
"git.cloonar.com/openclawd/iso-bot/pkg/plugin"
"git.cloonar.com/openclawd/iso-bot/plugins/d2r"
)
func main() {
// Command line flags
game := flag.String("game", "d2r", "game plugin to load")
routine := flag.String("routine", "mephisto", "farming routine to run")
apiAddr := flag.String("api", ":8080", "API server address")
configFile := flag.String("config", "", "path to config file (YAML)")
devMode := flag.Bool("dev", false, "enable development mode (no input simulation, serves dev dashboard)")
captureFile := flag.String("capture-file", "", "file to capture from (for dev/testing)")
captureDir := flag.String("capture-dir", "", "directory to capture from (for dev/testing)")
flag.Parse()
log.Printf("iso-bot starting: game=%s routine=%s api=%s dev=%t", *game, *routine, *apiAddr, *devMode)
if *configFile != "" {
log.Printf("Loading config from %s", *configFile)
// TODO: Load YAML config
}
// Set up capture source
var captureSource string
var captureConfig map[string]interface{}
if *devMode {
if *captureFile != "" {
captureSource = *captureFile
captureConfig = map[string]interface{}{
"path": *captureFile,
"type": "image",
"loop": true,
}
} else if *captureDir != "" {
captureSource = *captureDir
captureConfig = map[string]interface{}{
"path": *captureDir,
"type": "directory",
"frame_rate": 1.0, // 1 FPS for directory scanning
"loop": true,
}
} else {
// Default test image
testDataPath := filepath.Join("testdata", "d2r_1080p.png")
captureSource = testDataPath
captureConfig = map[string]interface{}{
"path": testDataPath,
"type": "image",
"loop": true,
}
log.Printf("No capture source specified, using default: %s", testDataPath)
}
} else {
// Production mode - would use window/monitor capture
log.Fatal("Production mode not implemented yet. Use --dev for development.")
}
// Create capture backend
registry := backends.GetDefault()
source, err := registry.Create(backends.BackendFile, captureConfig)
if err != nil {
log.Fatalf("Failed to create capture source %s: %v", captureSource, err)
}
defer source.Close()
// Load game plugin
var gamePlugin plugin.Plugin
switch *game {
case "d2r":
gamePlugin = d2r.New()
default:
log.Fatalf("Unknown game plugin: %s", *game)
}
// Create engine
eng, err := engine.NewEngine(source, gamePlugin, *devMode)
if err != nil {
log.Fatalf("Failed to create engine: %v", err)
}
// Set up web root for dev dashboard
webRoot := "web/dev"
if _, err := os.Stat(webRoot); os.IsNotExist(err) {
// Try relative to executable
execDir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
webRoot = filepath.Join(execDir, "web", "dev")
}
// Start API server
server := api.NewServer(*apiAddr, eng, webRoot)
go func() {
log.Printf("Starting API server on %s", *apiAddr)
if *devMode {
log.Printf("Dev dashboard will be available at http://localhost%s", *apiAddr)
}
if err := server.Start(); err != nil {
log.Fatalf("API server error: %v", err)
}
}()
// Start engine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := eng.Start(ctx); err != nil {
log.Printf("Engine error: %v", err)
}
}()
fmt.Printf("Game: %s, Routine: %s, Dev Mode: %t\n", *game, *routine, *devMode)
log.Printf("Capture source: %s", captureSource)
// Wait for interrupt
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
log.Println("Shutting down...")
cancel()
eng.Stop()
}