Insight into WebWalking with doors

Deep going explanation of the walker

Author: Loading...

Insight: WebWalking with doors

Introduction

Thanks to recent developments in the library, it was possible to create a system that can handle doors in their full generality. This is mainly thanks to the TRSMap system that @Torwent was able to create, and the awesome collision maps that came with it. For a tutorial on how to use this system, I refer you to the base level tutorial I wrote about it. In this tutorial, I will already assume you know the main gist of how the TRSWalker and/or TRSWalkerV2 work, as well as have an idea of what WebGraphs are and how the walker uses it to find paths.

This tutorial is split up into multiple chapters that cover different stages of the door handling process. The first chapter will start with the very backend stuff of how to find doors and add them to the WebGraph, afterwards we will transition into opening doors and the routine behind that. Lastly we will talk about how the walker has been modified to accommodate doors. These are also the three natural steps and represent the order in which I developed the project.

Chapter 1: Identifying doors

It all starts with identifying doors on the map. Thanks to the collision maps that @Torwent created, we have a map of where walls are, where doors are and what areas are walkable and which not. These maps are not 100% perfect, which also means that there are some doors it will not perfectly handle. To view the collision map of your loaded map, you can call:

Map.Loader.Collision.Debug();

and you will see whether the map is correct or not. On this map, white space is walkable and black space is not walkable, and doors are represented as red lines, just like on the minimap. An example is given below of the chunk of the West-Varrock bank. Example of collision map

If you look very closely you will see some open and some closed doors on the map. These represent the standard state of the door. To identify the doors on a map, it is as simple as getting all the red pixels and putting them in clusters. In the code, this is done in the TRSChunkLoader.BuildGraph function, which is where all the WebGraph magic happens. Side note: I recommend taking a Simba window beside this tutorial because I will be referring to a lot of functions and i will not post them all here so as to not make this text excruciatingly long.

So in this function, we start out by collecting clusters of white on the map as well as clusters of red. The whitespace clusters are particularly interesting and useful for a whole range of things, as it represents clusers of walkable space. An example of such a clustered collision map is below.

Clusters example

As you can likely imagine, the majority of the BuildGraph function is based on creating a WebGraph on the walkable space, by laying out nodes in a clever way and connecting using another cool technique called skeletonization. But as you can imagine, you wouldn’t immediately want to connect nodes that are in different clusters. The nice thing is, any place you want to get to, but is in another walkable cluster, is always going to be separated by a door. And therefore unlocking doors is essentially unlocking a way to step between clusters, which otherwise is impossible.

So how do we actually integrate doors into the graph? Recall that we got a clustering of red pixels. As you can imagine, these are almost always just doors (not always, for example the GE booths are also colored red) and so clustering red is a very effective way to find all doors on a map. On these clusters, we then call the internal function _FindDoors, which identifies the door centers and the direction the door is facing, so e.g. a door that appears vertical on the map, I consider to be pointing towards the right side, which would be a direction vector of [1,0]. This choice of making the direction be perpendicular to the plane of the door might seem weird, but it is the direction in which you have to look to make the door appear on your screen, so that’s the idea behind it. This info is then stored in a record TRSDoor for each door, along with its type. We consider normal doors, which are doors of size 4 and wide doors, which are of size 8. To deal with wide doors, we just act like they are normal doors and shift their center value two up or to the side (depending on door orientation), which will then make it be on the same point as if the door was a single tile door, and opening a double door is simply the same as if there was only a single tile on one of the doors. Lastly we also collect the coordinates of the tile before and after the door, although this is of course a matter of perspective depending on which side you come from.

An important step to do after this, is to integrate these doors into the WebGraph with connections that go through the door. To do this, we create nodes in the graph at two pixels away on both sides of the door and connect them to each other, so the line that connects them goes straight through the door. Then we connect the two nodes to the nearest nodes in that are in the same cluster of walkable space as them, or to no node if there are no other nodes in that cluster. A last check to consider is that we only want to have doors that separate the walkable clusters. Doors that do not have this property on our map are usually the ones rendered open rendered open, or ones that have another path around it, so the door is ‘optional’ to open.

This is still all a bit vague maybe so let’s take a quick excursion to explain tiles on the map and how they work and where doors are placed on this map.

Excursion: Collision map and tiles

The first thing to note is that a tile is always 4x4 pixels, so 16 pixels in total make one tile in the game. You can for example see this reflected in the fact that the walker always gives coordinates in steps of 4 per tile. An important aspect of using TRSMap is that the collision maps do not have weird cut-offs at the edges in the middle of the tiles. This allows us to accurately play with coordinates within a single tile and know that in game we are remaining on that particular tile. For example, with this knowledge, we can always normalize any tile so that the walker always spits out a specific pixel of the 16 options to represent a tile. This idea has been implemented into the position system, and now any position that the walker finds is always normalized to [0,2] within the 16 tile grid, so the left column on the third row. This choice is because the minimap center naturally corresponds to this displacement. However, these also exists another normalization that will later be used, which is the door normalization, found by calling TRSTranslator.NormalizeDoor and corresponds to a [1,1] displacement from the top-left pixel.

Now consider the following image of the pixel-grid on which we find our doors. Wall

In this case we are dealing with a door on the left hand side of the tile. Note that a door can be on any of the 4 sides on a tile. And in this case, the door is at standard shut, i.e. there is a wall around it. Now consider the tiles before and behind the door. Tiles are represented by their standard normalization. Below you see the two pixels that correspond to these as blue crosses.

Normalized tiles You can see that in this case the blue cross is inside the door, which is important to remember for a number of things later on.

Now we also draw in the door center as well as the two nodes of the graph we made around it (always 2 pixels away on each side of the center). door crosses

Lastly, we also draw in the pixels that correspond to the tiles normalized to the NormalizeDoor normalization. The handy thing about this particular normalization is that these pixels are always in the same line or column as the center of the door, as the TPA.center() always picks the second row for the center pixels as the center if the door is vertical, and it always picks the second column if the door is horizontal. This will also be important later on! Another important property of the door normalization is that the pixel is never inside a wall or door, and it is also always in the right walkable space cluster that the in-game tile belongs to. This categorizes the two normalizations to their practical uses: stuff to do with tile detection on the Mainscreen, that’s where you want to use the regular normalization, and things to do with walkable space and door related math, you in general want the door normalization. all crosses

With this information in mind, we can go onto the next step, which is how to open doors.

Chapter 2: Opening Doors

There are two kinds of door solving routines in the walker. The hard solve routine and the general routine. The hard solve is one that works all the time for every door but is very strict in conditions, whereas the general routine fails kinda often, but is way more flexible. The first one I made was the hard solver, because it was a proof of concept that every door is can be opened under the right conditions. This opened the door towards allowing more flexible routines, since we now always have a fallback. I will start by discussing the hard solve routine as it is the easiest to explain and the other routine follows naturally from it.

Hard Solving Doors

The hard solving routine is very simple in essence.

  1. Step onto tile in front of the door
  2. Rotate camera to face the door exactly
  3. Detect the tile behind the door and move the mouse to the center and then a bit below that (because some doors like gates aren’t high enough that they come to the center in certain zoom levels)
  4. If you see in the UpText that there is a door to be opened, open it, if you don’t see any door to be opened in the UpText, it must already be open.
  5. Now there shouldn’t be an ‘open’ related UpText anymore, so click on the same spot again.

Congratulations, you can now walk through every door in the game! Also, see the picture below for a visual aid to understand why this works. Imagine clicking exactly on my character’s head. That will open any door. hardsolve

There are three remarks to make here that made this possible. It used to not be possible to be tile perfect with the old walker, due to regular inaccuracies with the position finding, and being unable to normalize the position on it properly. Now with the normalization and consistent maps, it is a lot more feasible to do this. There are still very rare cases in which being tile perfect wouldn’t be possible, but in general it is. However, the ability to walk in one tile steps has been disabled since the old walker. There was a hardcoded check on the walker that made it so that if your destination tile is within 5 pixels distance of your start, it wouldn’t move. For reference, this check is in the TRSWalkerV2.WalkStep and TRSWalkerV2.WalkFinalStep functions. Now the check has been modified to allow tile perfectness. For reference, you can find the same functions for the old walker and you will see the difference in the check.

The second remark is that it is very important to stick to the tile grid. You can find rectangles that are not on the tile grid, but are sort of ‘in between’ tiles, but the trick to be always on the grid is to be in the same 4 tile grid as the minimap, starting from Minimap.Center. Given that we already normalize positions to the center of the minimap pixel, any destination tile we select that is also normalized to this grid will be perfectly on the tile grid. For reference, below we have the tile grid I am referring to. Tile grid

Thirdly, the thing that made rotating towards doors possible is that we found the direction of the door earlier on the collision map. If you have that, it is a simple mathematical trick to figure out the closest cardinal direction to the vector between the tile before the door, normalized to the door tile, and the center of the door. Recall that these line up properly from before. The function that finds this angle is TRSDoor.GetDoorAngle.

Now, the issue is: this routine is very bot-like and it would get you banned before you know. But of course, that’s why we have the general routine.

General Routine

The general routine is very similar to the hard solving routine, but it works from a distance and it works from nearly all angles (only not in the 15 degrees around the sharpest angle of viewing the door, so if you do your math, we support 300 out of the 360 degrees). How do we do this?

  1. Make sure we are close enough to door so that the tile behind it is visible
  2. Make sure we are in one of the supported angles
  3. Once again hover the tile behind the door in a spot that is not the center of the tile, but kinda close to the edge of the door
  4. Now there is the peculiarity that we support the door to be opened from the backwards angle so you need to make sure to hover the tile behind the door visually. In this case that is the tile in front of the door from the path’s perspective. See picture below!
  5. In this spot you should be able to determine the door state just like before. If it is open, click the tile, if not the click to open the door

Door from other angle

The last step is moving through the door. This is closely intertwined with the walker part, so we will talk about that later. But know that if you open the door from the behind angle, like in the picture, you won’t pass through the door by clicking on that tile anymore unlike in the hard solve routine. Also, we want to get rid of the clicking on the tile exactly behind the door anyways, its a way better idea to let the walker carry you further through the door. So without further ado, we will move to the final chapter.

Chapter 3: WebWalking through doors

Let’s first take a quick look at how the walker works without doors. The procedure is relatively simple:

  1. Establish the nearest node in the WebGraph to your position and destination coordinates
  2. Make a path between these on the WebGraph using a pathfinding algorithm on the graph
  3. We call TRSWalkerV2.WalkPath on this path
  4. Now it takes this path and splits it up into smaller chunks (some nodes are a bit far apart) by taking pixels on the lines that connect the nodes
  5. Find the point that is furthest away on the path and is visible on screen and find the nearest point on the path to your player. Usually you select the point that is furthest away as your target, but with some small chance you can also select any intermediate point.
  6. Click on the target and execute function assigned to OnWalkEvent
  7. Wait until you are waitUntilDistance away from the target with the TRSWalkerV2.WaitMoving function. If the step is the final step in the walk, you use the parameter supplied to the WebWalker as waitUntilDistance and else you select some random number. You can find this logic in the WalkStep and WalkFinalStep functions.

Now how does this change if we add doors into the mix? Take a look at the TRSWalkerV2.WebWalkEx function and find out. Firstly, calculate a path along the graph in much the same way as before. We take special care here that the nearest nodes we are looking for are not in another walkable cluster than our own, so as to not randomly cross walls. Another check we conduct is to be sure the current position pixel is walkable, and if it is not, we are likely next to a wall (much like in the figure above where the blue cross was in the door) so in this case we normalize to a doortile, to try and get a white pixel. Once we have the path, we identify if there are doors on the path with TRSWalkerV2.GetDoorsOnPath. To do this, you can check whether you find the TRSDoor.Before and TRSDoor.After tiles on the path, on after the other, and if you find them the other way around, then you reorder them, so the before tile is always the first one you come across on the path.

No doors on the path? Great, then we just call the normal path walking function and walk the path. If there are doors on the path, we get into a while loop that treats the doors one by one. We first give the internal record _DoorHandler the information about what door we are handling and what the upcoming doors on the path are. This record was made invisible to Simba function search bar, but you can find it defined inside the TRSWalkerV2 record. We have 4 booleans inside this record : FlagMoved, Handling, SettingUp, Skipped and each of these will play an important role in the handling of doors.

So now that we have a path an we know what doors are on it, we call the TRSWalkerV2.WalkDoorway function, which is an edited TRSWalkerV2.WalkPath that has the logic to open the doors on the path. It starts out by calling the same function as before that builds a more fragmented path, but now we let to go until the first door’s TRSDoor.Before tile. We also set the _Doorhandler.SettingUp to True. When we are walking along this path, we constantly check for visibility of the door and, as long as SettingUp is true, check whether we have a valid camera angle to open the door. If we have identified we have the right angle to open the door, we set _Doorhandler.SettingUp to False, and else we rotate to a suitable angle and then set it to True. We do this while walking, in the WalkStep function. Once we are set up to open the door, and we are far enough along the path so that it is visible, we can enact the TRSWalkerV2.SolveDoor function, which does the general routine described above. Out of this, we can have two outcomes: either the door was _DoorHandler.Skipped or not. Skipped means that the door was detected open, by lack of uptext related to opening doors, and not skipped means that we actively clicked on the door and are now walking towards it to open it.

We first consider the case hat we skipped the door. Recall that we are walking a normal path to the tile before the door. So now since we detected that we can move through it, we can extend this path further. Ideally towards the destination, but if there are more doors on the way, only until the next door. We now also set the Handling Boolean to True. If this is set to true, it will do two things. Firstly, whenever we click to move with the TRSWalkerV2.WalkStep or TRSWalkerV2.WalkFinalStep function, we need to check if the minimap flag moved. If it did, we need to check if it moved to a place outside the walkable cluster that we intended to click on. Mostly this happens because the door is still closed (due to it not finding the door on the spot it expected to most likely, or because someone else closed it). If it did move outside this cluster, we set FlagMoved to True, which will make it so that we immediately quit the WalkDoorway function, back to the WebWalkEx function, then increase the number of tries and restart from the beginning. Then it will recalculate the path, find the door to be the first door on the path again and call WalkDoorway again and restart the entire routine above. Up to three tries it will try with the general routine, and then it will attempt with the hard solve routine.

/---------------------

Quick sidenote:

The combination of walkable space clusters and flag movement is very powerful for many applications. Access the power of walkable clusters yourself with TRSMap.Walker.WebGraph^.walkableClusters and TRSWalkerV2.FindFlagMove! This is handy for all kinds of obstacles you may come across, like agility shortcuts, and determining the success of having moved past it. /---------------------

Now say the flag didn’t move, which is what usually happens, it generally means success and we are cruising towards the destination. However, we don’t call ourselves victorious until we acually find ourselves within the walkable cluster of the tile after the door. If we do find ourselves there, we exit on a success, reset the number of tries and recalculate the entire path, detect doors again and so forth… until it doesn’t find any doors on the path anymore and we can just do a normal walk towards the end!

The only case we have not considered is that we clicked on the door to open it. What happens then is really quite simple: you wait until you stop moving. If you find yourself to be right next to the door when that happens, the door should now be open, so you can act just like you skipped it, and extend the path and whatnot. If you do not find yourself near the door, most likely someone else opened it while you were walking towards it, causing you to stop mid path. If that happens, exit and report as failure, increasing the tries by 1 and then it will restart the door opening procedure again.

Acknowledgements

Thank for reading my thesis on doors. I would like to acknowledge @Torwent for figuring all this stuff out from the scuffed 2000 lines piece of code I sent him and then taking the time to implement it into the library. It quite a piece of work for him, and in doing so he also added valuable improvements, so major shoutout for his contributions! Also shoutout to @Kriptic for helping me test it.