package alchemy import ( "autobrew/action" "autobrew/container" "autobrew/ingredient" "autobrew/recipe" "autobrew/utils" "bytes" "fmt" "image" "image/png" "os" "path/filepath" "slices" "strings" "time" "github.com/MaxHalford/halfgone" "github.com/bendahl/uinput" "github.com/otiai10/gosseract/v2" "github.com/vova616/screenshot" "github.com/yassinebenaid/godump" ) type Alchemy struct { keyboard uinput.Keyboard mouse uinput.Mouse searchRect image.Rectangle save bool } func NewAlchemy(mouse uinput.Mouse, keyboard uinput.Keyboard, searchRect image.Rectangle, save bool) *Alchemy { return &Alchemy{ mouse: mouse, keyboard: keyboard, searchRect: searchRect, save: save, } } func (a Alchemy) findString(word string) (out gosseract.BoundingBox, err error) { img, err := screenshot.CaptureRect(a.searchRect) utils.Check(err) gray := halfgone.ImageToGray(img) gray = halfgone.ThresholdDitherer{Threshold: 85}.Apply(gray) buf := new(bytes.Buffer) err = png.Encode(buf, gray) utils.Check(err) client := gosseract.NewClient() defer client.Close() client.SetImageFromBytes(buf.Bytes()) // use from screen frame if a.save { path1 := filepath.Join(".", "output.png") err = os.WriteFile(path1, buf.Bytes(), 0644) utils.Check(err) } bounding_boxes, err := client.GetBoundingBoxes(2) utils.Check(err) index := slices.IndexFunc(bounding_boxes, func(e gosseract.BoundingBox) bool { a := strings.ToLower(e.Word) b := strings.ToLower(word) return strings.Contains(a, b) }) if index < 0 { text, err := client.Text() utils.Check(err) // godump.Dump(bounding_boxes) godump.Dump(text) return out, fmt.Errorf("%s not found\n", word) } out = bounding_boxes[index] return } func (a Alchemy) sleep(msec time.Duration) { time.Sleep(time.Duration(msec * time.Millisecond)) } func (a Alchemy) use() { fmt.Println("Pressing E") a.keyboard.KeyPress(uinput.KeyE) a.sleep(200) } func (a Alchemy) hold() { fmt.Println("Holding E") a.keyboard.KeyDown(18) a.sleep(1000) a.keyboard.KeyUp(18) a.sleep(200) } func (a Alchemy) lookAt(str string) { a.mouse.Move(-1200, -550) a.sleep(200) // fmt.Printf("Looking at %s\n", str) switch strings.ToLower(str) { case "water": a.mouse.Move(0, 150) case "wine": a.mouse.Move(100, 150) case "oil": a.mouse.Move(200, 150) case "spirits": a.mouse.Move(300, 150) case "phial": a.mouse.Move(105, 320) a.sleep(2500) // make sure we're actually looking at it case "still": a.mouse.Move(200, 460) case "dish": a.mouse.Move(600, 600) case "mortar": a.mouse.Move(850, 500) case "cauldron": a.mouse.Move(530, 300) case "topshelf_0": a.mouse.Move(1000, 150) case "topshelf_1": a.mouse.Move(900, 150) case "topshelf_2": a.mouse.Move(760, 150) case "bottomshelf_0": a.mouse.Move(980, 350) case "bottomshelf_1": a.mouse.Move(900, 350) case "bottomshelf_2": a.mouse.Move(780, 350) } a.sleep(100) } func (a Alchemy) OpenInventory() { fmt.Println("Opening Inventory") a.keyboard.KeyPress(uinput.KeyI) a.sleep(400) } func (a Alchemy) SelectIngredient(str ingredient.Ingredient) { rect, err := a.findString(str.Name()) utils.Check(err) x := rect.Box.Min.X + (rect.Box.Bounds().Size().X / 2) y := rect.Box.Min.Y + (rect.Box.Bounds().Size().Y / 2) a.mouse.Move(-2560, -1440) a.sleep(100) fmt.Printf("Selecting %s at %dx%d\n", str.Name(), x, y) a.mouse.Move(int32(x), int32(y)) a.sleep(50) a.mouse.LeftClick() a.mouse.LeftClick() a.sleep(50) } func (a Alchemy) CloseInventory() { fmt.Println("Closing Inventory") a.keyboard.KeyPress(uinput.KeyEsc) a.sleep(300) } func (a Alchemy) ClosePopup() { fmt.Println("Closing Popup") a.keyboard.KeyPress(uinput.KeyEsc) a.sleep(300) } func (a Alchemy) PourBase(i ingredient.Ingredient) { a.lookAt(i.Name()) a.sleep(100) a.use() a.sleep(7980) } func (a Alchemy) GrabIngredient(i ingredient.Ingredient, shelf []ingredient.Ingredient) { // fmt.Printf("Looking for %s\n", i.Name()) bottomShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool { return i.BottomShelf() }) topShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool { return !i.BottomShelf() }) topIndex := slices.Index(topShelf[:], i) if topIndex > -1 { // fmt.Printf("Found %s on the top shelf at index %d\n", i.Name(), topIndex) a.sleep(1000) switch topIndex { case 0: a.lookAt("topshelf_0") case 1: a.lookAt("topshelf_1") case 2: a.lookAt("topshelf_2") } } else { bottomIndex := slices.Index(bottomShelf[:], i) if bottomIndex == -1 { return } // fmt.Printf("Found %s on the bottom shelf at index %d\n", i.Name(), bottomIndex) switch bottomIndex { case 0: a.lookAt("bottomshelf_0") case 1: a.lookAt("bottomshelf_1") case 2: a.lookAt("bottomshelf_2") } } a.sleep(500) a.use() a.sleep(5000) } func (a Alchemy) DropIn(c container.Container) { switch c { case container.Cauldron: a.lookAt("cauldron") case container.Dish: a.lookAt("dish") case container.Mortar: a.lookAt("mortar") default: panic(fmt.Sprintf("unexpected container.Container: %#v", c)) } a.sleep(100) a.use() a.sleep(5500) } func (a Alchemy) Grind() { a.lookAt("mortar") a.use() a.sleep(10000) } func (a Alchemy) Boil(turns int) { a.lookAt("cauldron") a.sleep(100) a.keyboard.KeyPress(uinput.KeyX) a.sleep(5000) for range turns { // fmt.Printf("Boiling for %d turns\n", turns-i) a.sleep(9000) } a.keyboard.KeyPress(uinput.KeyX) a.sleep(5000) } func (a Alchemy) BoilBellows(turns int) { // var duration time.Duration = 10 a.lookAt("cauldron") a.sleep(100) a.keyboard.KeyPress(uinput.KeyX) a.sleep(5000) // 4+1 seconds to lower the cauldron and one for pre-boil for range turns { // fmt.Printf("Bellow Boiling for %d turns\n", turns-i) for range 18 * 2 { a.keyboard.KeyPress(uinput.KeyQ) // tap q every 500ms a.sleep(250) // boil time, 18*500 makes 9000 again } } a.sleep(2000) a.keyboard.KeyPress(uinput.KeyX) a.sleep(5000) // 5 seconds to pull it back up and stop animating } func (a Alchemy) PreparePhial() { a.lookAt("phial") a.use() a.sleep(2000) } func (a Alchemy) Distill() { a.lookAt("still") a.hold() a.sleep(16000) } func (a Alchemy) PourOut() { a.lookAt("cauldron") a.hold() a.sleep(8000) } func (a Alchemy) GrindOut() { a.lookAt("mortar") a.hold() a.sleep(15000) } func (a Alchemy) innerBrew(r recipe.Recipe) { for _, step := range r.Steps { switch step.Action { case action.Prepare: ing := utils.DedupeSlice(step.Ingredients) base := utils.Filter(ing, func(i ingredient.Ingredient) bool { return i.IngredientType == ingredient.Base }) if len(base) == 0 { panic("Recipe is missing a base") } a.OpenInventory() for _, i := range ing { if i.IngredientType != ingredient.Base { // r.Ingredients = append(r.Ingredients, i) a.SelectIngredient(i) } } a.CloseInventory() a.PourBase(base[0]) case action.AddToCauldron: for _, i := range step.Ingredients { a.GrabIngredient(i, r.Ingredients) a.DropIn(container.Cauldron) } case action.Boil: for _, i := range step.Ingredients { a.GrabIngredient(i, r.Ingredients) a.DropIn(container.Cauldron) } a.Boil(step.Turns) case action.BoilBellows: for _, i := range step.Ingredients { a.GrabIngredient(i, r.Ingredients) a.DropIn(container.Cauldron) } a.BoilBellows(step.Turns) case action.GrindIngredients: for _, i := range step.Ingredients { a.GrabIngredient(i, r.Ingredients) a.DropIn(container.Mortar) } a.Grind() a.DropIn(container.Cauldron) // finishers case action.PourPhial: a.PreparePhial() a.PourOut() case action.Distill: a.PreparePhial() a.Distill() case action.GrindPotion: a.GrindOut() default: panic(fmt.Sprintf("unexpected main.StepAction: %#v", step.Action)) } } } func (a Alchemy) Brew(book map[string][]recipe.Step, name string, amount int) { r := recipe.Recipe{ Steps: book[name], } r.LoadIngredients() ingredients := make(map[string]int) for _, i := range r.Ingredients { ingredients[i.Name()]++ } formatted := make([]string, 0) for k, v := range ingredients { formatted = append(formatted, fmt.Sprintf("%dx %s", v, k)) } fmt.Printf("%s\n\nIngredients:\n\t%s\n\n", name, strings.Join(formatted, "\n\t")) a.sleep(1000) totalStartTime := time.Now() if amount == -1 { i := 0 for { fmt.Printf("Start brewing %s in 3\n", name) a.sleep(1000) fmt.Printf("Start brewing %s in 2\n", name) a.sleep(1000) fmt.Printf("Start brewing %s in 1\n", name) a.sleep(1000) startTime := time.Now() a.innerBrew(r) fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime)) a.ClosePopup() i++ } } for i := range amount { fmt.Printf("Start brewing %s in 3\n", name) a.sleep(1000) fmt.Printf("Start brewing %s in 2\n", name) a.sleep(1000) fmt.Printf("Start brewing %s in 1\n", name) a.sleep(1000) startTime := time.Now() a.innerBrew(r) fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime)) a.ClosePopup() } }