Add missing cmd/iso-bot files
This commit is contained in:
parent
6a9562c406
commit
4e4acaa78d
2 changed files with 358 additions and 0 deletions
214
cmd/iso-bot/gen_testdata.go
Normal file
214
cmd/iso-bot/gen_testdata.go
Normal 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
144
cmd/iso-bot/main.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue