From 4e4acaa78d43e7b753e695daa195351b1b0ace31 Mon Sep 17 00:00:00 2001 From: Hoid Date: Sat, 14 Feb 2026 10:39:03 +0000 Subject: [PATCH] Add missing cmd/iso-bot files --- cmd/iso-bot/gen_testdata.go | 214 ++++++++++++++++++++++++++++++++++++ cmd/iso-bot/main.go | 144 ++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 cmd/iso-bot/gen_testdata.go create mode 100644 cmd/iso-bot/main.go diff --git a/cmd/iso-bot/gen_testdata.go b/cmd/iso-bot/gen_testdata.go new file mode 100644 index 0000000..e769045 --- /dev/null +++ b/cmd/iso-bot/gen_testdata.go @@ -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) +} \ No newline at end of file diff --git a/cmd/iso-bot/main.go b/cmd/iso-bot/main.go new file mode 100644 index 0000000..f77aafd --- /dev/null +++ b/cmd/iso-bot/main.go @@ -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() +}