Can we interact with everything we see in devtools dumps?

For example here is a devtools snippet of a window I would like to interact with.

image

There is my droneGroupInBay with 2 additional subgroup.
Hobgoblin (5)
Mining II (5)

How could I modify this function to interact with droneGroupInBay.Subgroup.Hobgoblin instead?

Is there a way to backtrace from devtools session to figure out what to call for a specific UI part?

I don’t know if that makes a lot of sense. I have some knowledge but not much experience. I can modify tools but I have a hard time creating. I’d like to develop and add some easy features to practice.

I’m attempting to follow Sanderling/implement/alternate-ui at main · Arcitectus/Sanderling · GitHub.

Here are my results so far.

image

image

image

Also, whenever I download an archived session I end up with JSONL files and not JSON like required to read from files.

image

1 Like

Actually got live process to work now.

I had falsely assumed that droneGroups.0 and droneGroups.1 were my custom group.

droneGroups.0.header.uiNode

is essentialy the same as

droneGroupInBay.header.uiNode

I cannot find any ways to interact with custom groups in the bay. They only show up as
"pythonObjectTypeName":"DroneSubGroup"
in the memory dump.

I actually found it.

   ᐯ0 descendants, EveLabelMedium [leftclick]  [rightclick]
    pythonObjectAddress = "2487598919976"
    pythonObjectTypeName = "EveLabelMedium"
    totalDisplayRegion = {"x":2166,"y":828,"width":78,"height":17}
    ᐯ10 dictEntriesOfInterest
    _color = {"aPercent":"60","rPercent":"100","gPercent":"100","bPercent":"100"}
    _displayHeight = "17"
    _displayWidth = "78"
    _displayX = "56"
    _displayY = "3"
    _height = "17"
    _left = "56"
    _setText = "Hobgoblin (5)"
    _top = "0"
    _width = "78"

Would this be enough information to make launchDrone use this menu instead of droneGroupInBay.header.uiNode

dronesWindow.droneview.__maincountainer.main.dronescroll.maincontainer.__clipper.__content.entry_1.labelClipper.EveLabelMedium

For hobgoblin (5)

dronesWindow.droneview.__maincountainer.main.dronescroll.maincontainer.__clipper.__content.entry_2.labelClipper.EveLabelMedium

For Mining II (5)

1 Like

The fastest way would be to use a memory reading from the game client as a reference. The visualization screenshot is good as an overview, but to generate the code, we use the actual reading as the reference.
You can export the reading from the game client to a file using the button “Download reading as JSON file” in the devtools in the view for an individual event. This is a screenshot of that part of the devtools:
button to export reading from game client

To get the memory reading file that you can import in the alternate UI, use the “Download reading as JSON file” button in devtools as explained above.

I do not see where the entry_1 comes from, so I would take another bit of information to identify it.
The text you copied also contains this line:

_setText = "Hobgoblin (5)"

You can use that property to identify the node in the program code.
I do not have your memory reading, but based on the part that you copied, we could formulate a function to identify that element:
We could say it is any node under the Drones window with "Hobgoblin (5)" as display text.

The getDisplayText function gets us the string from the _setText property:

The framework already contains a function to get the Drones window from the whole UI tree. But in this case, we don’t need that, because the launchDrones function you referenced already shows how to get up to the dronesWindow.
What we can do now is:

  • List all nodes under the Drones window.
  • Filter that list to only elements with the display text we found, "Hobgoblin (5)"
  • Take the first element from the filtered list.

Since you want to filter for two different nodes ("Hobgoblin (5)" and "Mining II (5)"), I am going make a function that you can (re)use for both cases:

getDescendantWithDisplayText : String -> UIElement -> Maybe UIElement
getDescendantWithDisplayText displayText parent =
    parent
        |> EveOnline.ParseUserInterface.listDescendantsWithDisplayRegion
        |> List.filter (.uiNode >> EveOnline.ParseUserInterface.getDisplayText >> (==) (Just displayText))
        |> List.head

And this is how you can integrate that function in launchDrones:

launchDrones : ReadingFromGameClient -> Maybe DecisionPathNode
launchDrones readingFromGameClient =
    readingFromGameClient.dronesWindow
        |> Maybe.andThen
            (\dronesWindow ->
                case ( dronesWindow.droneGroupInBay, dronesWindow.droneGroupInLocalSpace ) of
                    ( Just droneGroupInBay, Just droneGroupInLocalSpace ) ->
                        let
                            dronesInBayQuantity =
                                droneGroupInBay.header.quantityFromTitle |> Maybe.withDefault 0

                            dronesInLocalSpaceQuantity =
                                droneGroupInLocalSpace.header.quantityFromTitle |> Maybe.withDefault 0

                            droneGroupExpectedDisplayText =
                                "Hobgoblin (5)"
                        in
                        if 0 < dronesInBayQuantity && dronesInLocalSpaceQuantity < 5 then
                            Just
                                (describeBranch "Launch drones"
                                    (case getDescendantWithDisplayText droneGroupExpectedDisplayText dronesWindow.uiNode of
                                        Nothing ->
                                            describeBranch
                                                ("Did not find the node with display text '" ++ droneGroupExpectedDisplayText ++ "'")
                                                askForHelpToGetUnstuck

                                        Just droneGroupUiNode ->
                                            useContextMenuCascade
                                                ( "drones group", droneGroupUiNode )
                                                (useMenuEntryWithTextContaining "Launch drone" menuCascadeCompleted)
                                                readingFromGameClient
                                    )
                                )

                        else
                            Nothing

                    _ ->
                        Nothing
            )

Your replies are so in-dept. I like the explanations provided, really makes it easier to understand and incorporate in other projects.

It worked flawlessly.

image

Is it possible you haven’t had a session with subgroup of drones? They show up starting with entry_1 in the ui tree.

I have another question that will show how clueless I am.

We have this function that calls the modified launchDrones in mining bot.

travelToMiningSiteAndLaunchDronesAndTargetAsteroid : BotDecisionContext -> DecisionPathNode
travelToMiningSiteAndLaunchDronesAndTargetAsteroid context =
    case context.readingFromGameClient |> topmostAsteroidFromOverviewWindow of
        Nothing ->
            describeBranch "I see no asteroid in the overview. Warp to mining site."
                (returnDronesToBay context.readingFromGameClient
                    |> Maybe.withDefault
                        (warpToMiningSite context.readingFromGameClient)
                )

        Just asteroidInOverview ->
            describeBranch ("Choosing asteroid '" ++ (asteroidInOverview.objectName |> Maybe.withDefault "Nothing") ++ "'")
                (warpToOverviewEntryIfFarEnough context asteroidInOverview
                    |> Maybe.withDefault
                        (launchDrones context.readingFromGameClient
                            |> Maybe.withDefault
                                (lockTargetFromOverviewEntryAndEnsureIsInRange
                                    context.readingFromGameClient
                                    (min context.eventContext.appSettings.targetingRange
                                        context.eventContext.appSettings.miningModuleRange
                                    )
                                    asteroidInOverview
                                )
                        )
                )

If I wanted to re-order the logic so that launchDrones is called after lockTargetFromOverviewEntryAndEnsureIsInRange, how would I figure it out?

A lot of types mismatchs whenever I try to reorder the functions.

Yes, I do not remember seeing any subgroup or a structure as in your screenshot. Looks new to me.

Looking at it on a superficial level, without looking into the contents of the functions: I think that we would need lockTargetFromOverviewEntryAndEnsureIsInRange to inform us when it is done so that we know we should branch into launchDrones.

The standard way to do this is to change the return type of the function from DecisionPathNode to Maybe DecisionPathNode. This way, the function tells us it is done by returning the Nothing case. You can see this approach already implemented, for example, in launchDrones and returnDronesToBay. (I talked about this also at https://youtu.be/dgV9Ce7f03I?t=230)

Since returnDronesToBay returns an instance of Maybe a, we can use it with the function Maybe.withDefault to declare what comes ‘after’ launching drones.

This week I was wondering what an effective topic would be for a next guide or video. Maybe that could be explaining how to connect nodes in the decision tree and how Maybe a return types help with that. :thinking:

Back to your current task, the next question is: Why is lockTargetFromOverviewEntryAndEnsureIsInRange not already returning an instance of Maybe a? Which branch of that function should we change to return Nothing? We did not need to integrate the function that way in the past. It defaults to wait and do nothing instead of explicitly telling the integrating function that it is done. This behavior was sufficient in the past and allowed us to use the simpler return type DecisionPathNode.
Let’s look at the whole function:

lockTargetFromOverviewEntryAndEnsureIsInRange : ReadingFromGameClient -> Int -> OverviewWindowEntry -> DecisionPathNode
lockTargetFromOverviewEntryAndEnsureIsInRange readingFromGameClient rangeInMeters overviewEntry =
    case overviewEntry.objectDistanceInMeters of
        Ok distanceInMeters ->
            if distanceInMeters <= rangeInMeters then
                if overviewEntry.commonIndications.targetedByMe || overviewEntry.commonIndications.targeting then
                    describeBranch "Locking target is in progress, wait for completion." waitForProgressInGame

                else
                    describeBranch "Object is in range. Lock target."
                        (lockTargetFromOverviewEntry overviewEntry readingFromGameClient)

            else
                describeBranch ("Object is not in range (" ++ (distanceInMeters |> String.fromInt) ++ " meters away). Approach.")
                    (if shipManeuverIsApproaching readingFromGameClient then
                        describeBranch "I see we already approach." waitForProgressInGame

                     else
                        useContextMenuCascadeOnOverviewEntry
                            (useMenuEntryWithTextContaining "approach" menuCascadeCompleted)
                            overviewEntry
                            readingFromGameClient
                    )

        Err error ->
            describeBranch ("Failed to read the distance: " ++ error) askForHelpToGetUnstuck

Here we find two leaves in the decision tree where it terminates with waiting:

  • “Locking target is in progress, wait for completion.”
  • “I see we already approach.”

I am looking into changing the part that currently adds the text “I see we already approach.” to make it return Nothing. To make the types match, we need to wrap the other cases in Just. This is the resulting function:

lockTargetFromOverviewEntryAndEnsureIsInRange : ReadingFromGameClient -> Int -> OverviewWindowEntry -> Maybe DecisionPathNode
lockTargetFromOverviewEntryAndEnsureIsInRange readingFromGameClient rangeInMeters overviewEntry =
    case overviewEntry.objectDistanceInMeters of
        Ok distanceInMeters ->
            if distanceInMeters <= rangeInMeters then
                Just
                    (if overviewEntry.commonIndications.targetedByMe || overviewEntry.commonIndications.targeting then
                        describeBranch "Locking target is in progress, wait for completion." waitForProgressInGame

                     else
                        describeBranch "Object is in range. Lock target."
                            (lockTargetFromOverviewEntry overviewEntry readingFromGameClient)
                    )

            else if shipManeuverIsApproaching readingFromGameClient then
                -- describeBranch "I see we already approach." waitForProgressInGame
                Nothing

            else
                Just
                    (describeBranch
                        ("Object is not in range (" ++ (distanceInMeters |> String.fromInt) ++ " meters away). Approach.")
                        (useContextMenuCascadeOnOverviewEntry
                            (useMenuEntryWithTextContaining "approach" menuCascadeCompleted)
                            overviewEntry
                            readingFromGameClient
                        )
                    )

        Err error ->
            Just (describeBranch ("Failed to read the distance: " ++ error) askForHelpToGetUnstuck)

As we can see in the display text, this approach of implementing it removes the "I see we already approach." message. If we wanted to keep that message to appear above the messages from launchDrones we could use another approach to implement the new lockTargetFromOverviewEntryAndEnsureIsInRange: Instead of changing the return type to an instance of Maybe a, we could add another parameter with type DecisionPathNode to use to continue in these cases. You can see that for example in branchDependingOnDockedOrInSpace

Choosing between those two approaches comes down to what messages you want to show to the user and what you find easier to read in the program code.

Here is how we resolve that by adding the new default leaf and also implement the reordering with launchDrones:

            describeBranch ("Choosing asteroid '" ++ (asteroidInOverview.objectName |> Maybe.withDefault "Nothing") ++ "'")
                (warpToOverviewEntryIfFarEnough context asteroidInOverview
                    |> Maybe.withDefault
                        (lockTargetFromOverviewEntryAndEnsureIsInRange
                            context.readingFromGameClient
                            (min context.eventContext.appSettings.targetingRange
                                context.eventContext.appSettings.miningModuleRange
                            )
                            asteroidInOverview
                            |> Maybe.withDefault
                                (launchDrones context.readingFromGameClient
                                    |> Maybe.withDefault
                                        (describeBranch "Locked asteroid and launched drones." waitForProgressInGame)
                                )
                        )
                )

EDIT: Better move that part to the following post below.

After today’s exploration, I am wondering if the overall readability would be better if we change the types of functions like launchDrones and returnDronesToBay to not use a plain DecisionPathNode as return type and add a parameter like { ifComplete : DecisionPathNode }.
:thinking:
I implemented that here to illustrate the differences:

Unifying the return types like this could make it easier to swap these functions and avoid problems like this:

I’ve watched your video but if there is a guide out there talking about how Botengine uses the different types that could be a nice read.

I’ll try to tinker some more with your provided explanation.

Thanks for the explanations.

I have successfully achieved what I wanted but , sadly, without the use of the provided code. I did learn some new logics and patterns from the information provided in your posts though.

I used part of the anomalie lauchdrone function to make a lunchAndEngageMiningDrones function that gets pushed through after locking an asteroid.

Once I get the combat drone parts to work whenever a NPC rat shows up on the overview I’ll make sure to share for education purposes.

1 Like

Thank you for sharing your experience.

The botengine uses Elm as the programming language for program code. That means rules for Elm types apply here, too, and there are already some guides out there. This one is a good start: Types · An Introduction to Elm
Besides guides, there are also tools to experiment and learn about those types. These tools are useful for beginners as they provide quick feedback with low (in some cases zero) effort to set up:

Related, copying from the guide on developing for EVE Online:

The guide on the Elm programming language has a chapter “Types”, and I recommend reading this if you want to learn what these syntaxes mean. This chapter is also worth a look if you encounter a confusing “TYPE MISMATCH” error in a program code. In the “Reading Types” part, you will also find an interactive playground where you can test Elm syntax to reveal types that are sometimes not visible in program syntax.