Skip to content
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

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

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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