Skip to content

Performance upgrade, less CPU usage, code refactor, optimization of A* algorithm, an attempt of fixing character getting stuck on "Hidden Stash" object at Chaos Sanctuary. #650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8e077b8
Update utils.go
xVoidByte Jan 30, 2025
02740d1
Update path_finder.go
xVoidByte Jan 30, 2025
3f8688d
Update path.go
xVoidByte Jan 30, 2025
d1cd7b5
Update astar.go
xVoidByte Jan 30, 2025
7b88ab4
Update move.go
xVoidByte Jan 30, 2025
0e6e1e4
Update utils.go
xVoidByte Jan 30, 2025
9bb1cf8
Update move.go
xVoidByte Jan 30, 2025
fac71b9
Update path_finder.go
xVoidByte Jan 30, 2025
0bfa013
Update path_finder.go
xVoidByte Jan 31, 2025
0615288
Update interact_object.go
xVoidByte Jan 31, 2025
9a40ace
Update move.go
xVoidByte Jan 31, 2025
9f335ed
Update move.go
xVoidByte Feb 2, 2025
2e1dc29
Update interact_object.go
xVoidByte Feb 2, 2025
4ebd8ca
Update astar.go
xVoidByte Feb 6, 2025
203191d
Update path.go
xVoidByte Feb 6, 2025
437c643
Update path_finder.go
xVoidByte Feb 6, 2025
f5d407c
Update utils.go
xVoidByte Feb 6, 2025
49d08ea
Merge branch 'hectorgimenez:main' into main
xVoidByte Feb 8, 2025
6195038
Update move.go
xVoidByte Feb 8, 2025
9d38906
Merge branch 'hectorgimenez:main' into main
xVoidByte Feb 12, 2025
a307010
Update move.go
xVoidByte Feb 13, 2025
9abe5be
Update memory_reader.go
xVoidByte Feb 13, 2025
1f8c4af
Update astar.go
xVoidByte Feb 13, 2025
66344a8
Update path.go
xVoidByte Feb 13, 2025
0b53e6a
Update path_finder.go
xVoidByte Feb 13, 2025
63dda43
Update move.go
xVoidByte Feb 13, 2025
0a21886
Merge branch 'main' into main
xVoidByte Feb 17, 2025
d0beef6
Update astar.go
xVoidByte Feb 17, 2025
73c0a82
Update path_finder.go
xVoidByte Feb 17, 2025
d508359
Merge branch 'main' into main
xVoidByte Feb 17, 2025
f84dffa
Update astar.go
xVoidByte Feb 17, 2025
4c6ffd4
Update pindleskin.go
xVoidByte Feb 18, 2025
cba1b84
Update path_finder.go
xVoidByte Feb 18, 2025
9bcc557
Update move.go
xVoidByte Feb 18, 2025
244cee9
Update astar.go
xVoidByte Feb 18, 2025
c5a9263
Update utils.go
xVoidByte Feb 18, 2025
839a52f
Update path.go
xVoidByte Feb 18, 2025
f59b7e7
Merge branch 'main' into main
xVoidByte Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion internal/action/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package action

import (
"fmt"
"log"
"log/slog"
"sort"
"time"
Expand All @@ -25,6 +26,7 @@ const (
)

func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error {
log.Printf("DEBUG: Starting area sync (expected: %d, current: %d)", expectedArea, ctx.Data.PlayerUnit.Area)
// Skip sync check if we're already in the expected area and have valid area data
if ctx.Data.PlayerUnit.Area == expectedArea {
if areaData, ok := ctx.Data.Areas[expectedArea]; ok && areaData.IsInside(ctx.Data.PlayerUnit.Position) {
Expand All @@ -34,6 +36,7 @@ func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error {

// Wait for area data to sync
for attempts := 0; attempts < maxAreaSyncAttempts; attempts++ {
log.Printf("DEBUG: Area sync attempt %d/%d", attempts+1, maxAreaSyncAttempts)
ctx.RefreshGameData()

if ctx.Data.PlayerUnit.Area == expectedArea {
Expand All @@ -52,6 +55,7 @@ func ensureAreaSync(ctx *context.Status, expectedArea area.ID) error {

func MoveToArea(dst area.ID) error {
ctx := context.Get()
log.Printf("INFO: Moving to area %d from %d", dst, ctx.Data.PlayerUnit.Area)
ctx.SetLastAction("MoveToArea")

if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil {
Expand Down Expand Up @@ -133,6 +137,7 @@ func MoveToArea(dst area.ID) error {

err := MoveTo(toFun)
if err != nil {
log.Printf("WARN: Movement error to area %d: %v", dst, err)
ctx.Logger.Warn("error moving to area, will try to continue", slog.String("error", err.Error()))
}

Expand Down Expand Up @@ -185,6 +190,7 @@ func MoveToArea(dst area.ID) error {

func MoveToCoords(to data.Position) error {
ctx := context.Get()
log.Printf("DEBUG: Moving to coordinates %d:%d in area %d", to.X, to.Y, ctx.Data.PlayerUnit.Area)

if err := ensureAreaSync(ctx, ctx.Data.PlayerUnit.Area); err != nil {
return err
Expand Down Expand Up @@ -227,6 +233,7 @@ func MoveTo(toFunc func() (data.Position, bool)) error {

// If we can teleport, don't bother with the rest
if ctx.Data.CanTeleport() {
log.Printf("DEBUG: Executing final movement step")
return step.MoveTo(to)
}

Expand All @@ -249,6 +256,7 @@ func MoveTo(toFunc func() (data.Position, bool)) error {
// Check if there is any object blocking our path
for _, o := range ctx.Data.Objects {
if o.Name == object.Barrel && ctx.PathFinder.DistanceFromMe(o.Position) < 3 {
log.Printf("DEBUG: Found barrel at %d:%d", o.Position.X, o.Position.Y)
err := step.InteractObject(o, func() bool {
obj, found := ctx.Data.Objects.FindByID(o.ID)
//additional click on barrel to avoid getting stuck
Expand All @@ -271,7 +279,14 @@ func MoveTo(toFunc func() (data.Position, bool)) error {
minDistanceForElites := 20 // This will make the character to kill elites even if they are far away, ONLY during leveling
stuck := ctx.PathFinder.DistanceFromMe(previousIterationPosition) < 5 // Detect if character was not able to move from last iteration

log.Printf("DEBUG: Checking %d nearby monsters", len(ctx.Data.Monsters.Enemies()))
for _, m := range ctx.Data.Monsters.Enemies() {
log.Printf("DEBUG: Monster %d at %d:%d (life: %d%%)",
m.Name,
m.Position.X,
m.Position.Y,
m.Stats[stat.Life],
)
// Skip if monster is already dead
if m.Stats[stat.Life] <= 0 {
continue
Expand Down Expand Up @@ -321,7 +336,7 @@ func MoveTo(toFunc func() (data.Position, bool)) error {
}

// Continue moving
WaitForAllMembersWhenLeveling()
WaitForAllMembersWhenLeveling() // function being called from leveling_tools.go
previousIterationPosition = ctx.Data.PlayerUnit.Position

if lastMovement {
Expand Down
302 changes: 158 additions & 144 deletions internal/action/step/interact_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,148 +22,162 @@ const (
)

func InteractObject(obj data.Object, isCompletedFn func() bool) error {
interactionAttempts := 0
mouseOverAttempts := 0
waitingForInteraction := false
currentMouseCoords := data.Position{}
lastRun := time.Time{}

ctx := context.Get()
ctx.SetLastStep("InteractObject")

// If there is no completion check, just assume the interaction is completed after clicking
if isCompletedFn == nil {
isCompletedFn = func() bool {
return waitingForInteraction
}
}

// For portals, we need to ensure proper area sync
expectedArea := area.ID(0)
if obj.IsRedPortal() {
// For red portals, we need to determine the expected destination
switch {
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.StonyField:
expectedArea = area.Tristram
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.RogueEncampment:
expectedArea = area.MooMooFarm
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.Harrogath:
expectedArea = area.NihlathaksTemple
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.ArcaneSanctuary:
expectedArea = area.CanyonOfTheMagi
case obj.Name == object.BaalsPortal && ctx.Data.PlayerUnit.Area == area.ThroneOfDestruction:
expectedArea = area.TheWorldstoneChamber
case obj.Name == object.DurielsLairPortal && (ctx.Data.PlayerUnit.Area >= area.TalRashasTomb1 && ctx.Data.PlayerUnit.Area <= area.TalRashasTomb7):
expectedArea = area.DurielsLair
}
} else if obj.IsPortal() {
// For blue town portals, determine the town area based on current area
fromArea := ctx.Data.PlayerUnit.Area
if !fromArea.IsTown() {
expectedArea = town.GetTownByArea(fromArea).TownArea()
} else {
// When using portal from town, we need to wait for any non-town area
isCompletedFn = func() bool {
return !ctx.Data.PlayerUnit.Area.IsTown() &&
ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) &&
len(ctx.Data.Objects) > 0
}
}
}

for !isCompletedFn() {
ctx.PauseIfNotPriority()

if interactionAttempts >= maxInteractionAttempts || mouseOverAttempts >= 20 {
return fmt.Errorf("[%s] failed interacting with object [%v] in Area: [%s]", ctx.Name, obj.Name, ctx.Data.PlayerUnit.Area.Area().Name)
}

ctx.RefreshGameData()

// Give some time before retrying the interaction
if waitingForInteraction && time.Since(lastRun) < time.Millisecond*200 {
continue
}

var o data.Object
var found bool
if obj.ID != 0 {
o, found = ctx.Data.Objects.FindByID(obj.ID)
if !found {
return fmt.Errorf("object %v not found", obj)
}
} else {
o, found = ctx.Data.Objects.FindOne(obj.Name)
if !found {
return fmt.Errorf("object %v not found", obj)
}
}

lastRun = time.Now()

// Check portal states
if o.IsPortal() || o.IsRedPortal() {
// If portal is still being created, wait
if o.Mode == mode.ObjectModeOperating {
utils.Sleep(100)
continue
}

// Only interact when portal is fully opened
if o.Mode != mode.ObjectModeOpened {
utils.Sleep(100)
continue
}
}

if o.IsHovered {
ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y)
waitingForInteraction = true
interactionAttempts++

// For portals with expected area, we need to wait for proper area sync
if expectedArea != 0 {
utils.Sleep(500) // Initial delay for area transition
for attempts := 0; attempts < maxPortalSyncAttempts; attempts++ {
ctx.RefreshGameData()
if ctx.Data.PlayerUnit.Area == expectedArea {
if areaData, ok := ctx.Data.Areas[expectedArea]; ok {
if areaData.IsInside(ctx.Data.PlayerUnit.Position) {
if expectedArea.IsTown() {
return nil // For town areas, we can return immediately
}
// For special areas, ensure we have proper object data loaded
if len(ctx.Data.Objects) > 0 {
return nil
}
}
}
}
utils.Sleep(portalSyncDelay)
}
return fmt.Errorf("portal sync timeout - expected area: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area)
}
continue
} else {
objectX := o.Position.X - 2
objectY := o.Position.Y - 2
distance := ctx.PathFinder.DistanceFromMe(o.Position)
if distance > 15 {
return fmt.Errorf("object is too far away: %d. Current distance: %d", o.Name, distance)
}

mX, mY := ui.GameCoordsToScreenCords(objectX, objectY)
// In order to avoid the spiral (super slow and shitty) let's try to point the mouse to the top of the portal directly
if mouseOverAttempts == 2 && o.IsPortal() {
mX, mY = ui.GameCoordsToScreenCords(objectX-4, objectY-4)
}

x, y := utils.Spiral(mouseOverAttempts)
currentMouseCoords = data.Position{X: mX + x, Y: mY + y}
ctx.HID.MovePointer(mX+x, mY+y)
mouseOverAttempts++
}
}

return nil
// Enhanced hidden stash check
if string(obj.Name) == "hidden stash" || obj.ID == 125 || obj.ID == 127 || obj.ID == 128 {
return fmt.Errorf("interaction blocked for hidden stash")
}

// Add position validation before interaction
ctx := context.Get()
if !ctx.Data.AreaData.IsWalkable(obj.Position) {
if walkablePos, found := ctx.PathFinder.FindNearbyWalkablePosition(obj.Position); found {
if err := MoveTo(walkablePos); err != nil {
return fmt.Errorf("failed to move to safe interaction position: %v", err)
}
}
}

interactionAttempts := 0
mouseOverAttempts := 0
waitingForInteraction := false
currentMouseCoords := data.Position{}
lastRun := time.Time{}

ctx.SetLastStep("InteractObject")

// If there is no completion check, just assume the interaction is completed after clicking
if isCompletedFn == nil {
isCompletedFn = func() bool {
return waitingForInteraction
}
}

// For portals, we need to ensure proper area sync
expectedArea := area.ID(0)
if obj.IsRedPortal() {
// For red portals, we need to determine the expected destination
switch {
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.StonyField:
expectedArea = area.Tristram
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.RogueEncampment:
expectedArea = area.MooMooFarm
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.Harrogath:
expectedArea = area.NihlathaksTemple
case obj.Name == object.PermanentTownPortal && ctx.Data.PlayerUnit.Area == area.ArcaneSanctuary:
expectedArea = area.CanyonOfTheMagi
case obj.Name == object.BaalsPortal && ctx.Data.PlayerUnit.Area == area.ThroneOfDestruction:
expectedArea = area.TheWorldstoneChamber
case obj.Name == object.DurielsLairPortal && (ctx.Data.PlayerUnit.Area >= area.TalRashasTomb1 && ctx.Data.PlayerUnit.Area <= area.TalRashasTomb7):
expectedArea = area.DurielsLair
}
} else if obj.IsPortal() {
// For blue town portals, determine the town area based on current area
fromArea := ctx.Data.PlayerUnit.Area
if !fromArea.IsTown() {
expectedArea = town.GetTownByArea(fromArea).TownArea()
} else {
// When using portal from town, we need to wait for any non-town area
isCompletedFn = func() bool {
return !ctx.Data.PlayerUnit.Area.IsTown() &&
ctx.Data.AreaData.IsInside(ctx.Data.PlayerUnit.Position) &&
len(ctx.Data.Objects) > 0
}
}
}

for !isCompletedFn() {
ctx.PauseIfNotPriority()

if interactionAttempts >= maxInteractionAttempts || mouseOverAttempts >= 20 {
return fmt.Errorf("[%s] failed interacting with object [%v] in Area: [%s]", ctx.Name, obj.Name, ctx.Data.PlayerUnit.Area.Area().Name)
}

ctx.RefreshGameData()

// Give some time before retrying the interaction
if waitingForInteraction && time.Since(lastRun) < time.Millisecond*200 {
continue
}

var o data.Object
var found bool
if obj.ID != 0 {
o, found = ctx.Data.Objects.FindByID(obj.ID)
if !found {
return fmt.Errorf("object %v not found", obj)
}
} else {
o, found = ctx.Data.Objects.FindOne(obj.Name)
if !found {
return fmt.Errorf("object %v not found", obj)
}
}

lastRun = time.Now()

// Check portal states
if o.IsPortal() || o.IsRedPortal() {
// If portal is still being created, wait
if o.Mode == mode.ObjectModeOperating {
utils.Sleep(100)
continue
}

// Only interact when portal is fully opened
if o.Mode != mode.ObjectModeOpened {
utils.Sleep(100)
continue
}
}

if o.IsHovered {
ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y)
waitingForInteraction = true
interactionAttempts++

// For portals with expected area, we need to wait for proper area sync
if expectedArea != 0 {
utils.Sleep(500) // Initial delay for area transition
for attempts := 0; attempts < maxPortalSyncAttempts; attempts++ {
ctx.RefreshGameData()
if ctx.Data.PlayerUnit.Area == expectedArea {
if areaData, ok := ctx.Data.Areas[expectedArea]; ok {
if areaData.IsInside(ctx.Data.PlayerUnit.Position) {
if expectedArea.IsTown() {
return nil // For town areas, we can return immediately
}
// For special areas, ensure we have proper object data loaded
if len(ctx.Data.Objects) > 0 {
return nil
}
}
}
}
utils.Sleep(portalSyncDelay)
}
return fmt.Errorf("portal sync timeout - expected area: %v, current: %v", expectedArea, ctx.Data.PlayerUnit.Area)
}
continue
} else {
objectX := o.Position.X - 2
objectY := o.Position.Y - 2
distance := ctx.PathFinder.DistanceFromMe(o.Position)
if distance > 15 {
return fmt.Errorf("object is too far away: %d. Current distance: %d", o.Name, distance)
}

mX, mY := ui.GameCoordsToScreenCords(objectX, objectY)
// In order to avoid the spiral (super slow and shitty) let's try to point the mouse to the top of the portal directly
if mouseOverAttempts == 2 && o.IsPortal() {
mX, mY = ui.GameCoordsToScreenCords(objectX-4, objectY-4)
}

x, y := utils.Spiral(mouseOverAttempts)
currentMouseCoords = data.Position{X: mX + x, Y: mY + y}
ctx.HID.MovePointer(mX+x, mY+y)
mouseOverAttempts++
}
}

return nil
}
Loading