08/02/2019
As we announced in May - Bank Placeholders are coming and they are now live on our Beta servers! I'd like to take this developer blog to give some insight into the technical changes we've made.
This is what I like to call an Iceberg Update. You've got placeholders, usability and UI changes visible above the surface, while the bulk of the action lurks below - for several months at the start of the project you wouldn't have noticed any of the changes that had taken place!
Here's a look at steps this project went through on the road to release.
Pre-production is a step in the development process that happens before the team starts coding anything. It's intended for research, validation and gaining understanding of the problem.
The first step to prototyping is to look at various ways of implementing the feature. The engine developers and I had a meeting where we went through the pros and cons of each approach, eliminating those which had critical flaws. Once we arrived at a short list, it became evident that we had two options:
Leaving 0 method was preferable as it didn't require creating a whole load of new item IDs (although that would be automated), and meant you only needed to check the inventory once when depositing an item. This sounds simple in theory, but the complicating factor was that the engine was set up with a hard rule that a slot in a player's inventory with 0 items is empty.
The engine developers had an idea of how much would need to be changed, but there was of course potential that a lot more work would be required than we realised. So they set to making a prototype - a build where only core functionality is implemented.
While the engine developers were prototyping, I also had the enviable task of picking apart the Runescript - with the intention that it would be optimised, made more maintainable and ready for placeholders to be plugged in once the engine work was ready.
Unusually, for this project there was no specific release date - luckily this worked really well, as it would be an extremely difficult project to estimate time for.
My journey through the bank code mostly followed through these steps:
Optimising a script includes anything from looking at how quickly it runs, how easy the code is to read and how easily it can be expanded. My core focus was to make the code faster without changing the behaviour, while also improving readability.
For the initial stage of optimisation I went through the list created in pre-production, made the changes I had highlighted and ensured the test script were still passing.
Much of this was focused on reducing unnecessary script, with some examples being:
One of the biggest improvements came from looking at how engine commands were being used.
For example: When withdrawing an item, we need to limit the amount you can attempt to withdraw to whatever is actually in your bank. This originally used the inv_total (inventory total) command, which seems entirely sensible to use for this purpose.
However, in the Bank everything stacks, and we only ever allow 1 stack of an item. This means that we only actually need to look at the slot the player clicked - with the exception of selecting to withdraw more than 1 of items with data (e.g. augmented items) as they can appear in multiple slots. Similarly, a lot of the original code was written before local variables were supported by Runescript, so that inv_total lookup may have appeared in multiple places where it would run in full each time. I therefore ensured it was only looked up once.
Bank presets are where the biggest change occurred. Currently, when you load a preset it will deposit all items in the selected inventories before withdrawing the preset.
On top of that, there are extra considerations:
This isn't a complete list, but is more than enough to explain the changes here! This brought with it a lot of performance concerns:
The changes I made included:
All of this should significantly reduce the impact on the server, though as a player you generally wouldn't be aware anything had changed unless something went wrong!
Following on from the initial pass my aim was to make the code easier to update and follow. And this meant re-writing a lot of it - particularly around how Bank tabs work in code.
The following are the core operations the Bank supports (I found myself envying Old School's limited drag functionality!):
All of these dragging operations add a lot of complexity to the code - particularly for worn and Beast of Burden where special rules apply. But before jumping into that let's look at how the interface and tabs are generated.
In this image:
It also highlights how tabs work - which can appear counter-intuitive. The first slots of the Bank are those in tabs, and anything untabbed is at the end but is rendered first. A tab is simply a variable counting how many slots each tab has. In this Bank:
This means that tabs start to act like a pyramid, if we pretend there are a few more tabs it will be like in the below table:
Tab | Start Slot | End Slot |
---|---|---|
2 | 0 | %bank_tab_2 |
3 | %bank_tab_2 | %bank_tab_2 + %bank_tab_3 |
4 | %bank_tab_2 + %bank_tab_3 | %bank_tab_2 + %bank_tab_3 + %bank_tab_4 |
5 | %bank_tab_2 + %bank_tab_3 + %bank_tab_4 | %bank_tab_2 + %bank_tab_3 + %bank_tab_4 + %bank_tab_5 |
6-15 | Follow the same pattern | |
Untabbed | Total of adding _2 to _15 | Last used slot of bank |
Even in this simple image there's a few special cases to handle, for example:
Most other combinations would mean moving about the order of items. For example, dragging slot 1 to spacer (red) for 'all' view would move the item to slot 5 and will now leave a blank slot in tab 3 whereas in currently in live it would delete tab 3.
A different implication of this is how the deposit scripts handle the tabs. Currently it will deposit to the end of the Bank (ensuring there's space in the bank for the item) and if that succeeds it will attempt to move back to the end of the target tab. This feeds back into the optimisation section, where if you take a mostly full Bank you may be checking 1200 slots (to find the first available empty slot and ensure it's not already in the Bank) and then you may be shuffling the items along to move it back to slot 600.
A maintenance drawback to this is that anyone calling the "deposit" script would need to remember to check for success and call the "move to tab" script. My changes here were to shuffle the items to empty the target slot before deposit rather than after, eliminating the second pass entirely.
Other changes included making the withdraw checks more centralised - particularly for the worn inventory. To deposit an item (and similar for withdraw) to the Bank we would use the ~deposit_generic script, but for worn this had to be ~removeobj which in itself wasn't well named. The decision tree for this does become quite complex.
When withdrawing from Bank to worn these are some of the potential fail/success states:
This doesn't look too complicated simplified like this, but it's much more complicated in code! Beast of Burden has similar extra failure scenarios, like the amount that can be withdrawn being limited by value.
Following the core improvements it was time to look into making changes to functionality.
The big one here is removing compress or shuffling on withdraw - this is what causes a lot of people issues when they're spam clicking to withdraw items and accidentally move tabs around, or withdrawing the wrong item because it empties a row and shifts the Bank up. We've previously tried to mitigate the impact of that, but it's always going to be an issue as the client and server needs to be synchronised as the Bank changes. The changes here means when fully withdrawing a slot it will remain as a blank slot until you close your Bank or the space needs using up, effectively eliminating the shuffling unless in a filter or search view.
This isn't as simple a change as it first appears - any external code still needs to shuffle the slots down, and the tab size changes need to be calculated all at once rather than happening in real-time.
Factoring in to which slot to deposit to also became more complex. Assuming the item isn't in the bank:
The bulk of placeholder support was game-engine work, so for me this plugging in the functionality seemed strangely easy. However, that was because of months of optimisation and maintenance improvements beforehand!
Most of the complexity came from external scripts. Let's take fermenting wine as a strangely niche example - we have a completely full bank, with unfermented wine and no regular wine. Currently this would delete the unfermented wine, and re-add the proper wine. This is safe, as the Bank slot is guaranteed to be empty. With placeholders that's no longer the case as it could leave a 0-stack unfermented wine behind. We could ignore the placeholder setting here to ensure it remains blank, but the same function to remove the items is also used in locations you would expect placeholders to work - like a butler getting planks from your Bank.
The only solution therefore was to go through the references to the delete function and have each situation decide if they should honour placeholders or not. I also took this opportunity to change how many of these delete-then-add functions were structured. The reason behind delete-then-add was that if you simply changed the slot to the new type of item you would potentially have duplicate slots, and I wrote a new function to handle this while changing the slot type.
This was the case with a handful of other commands.
Adding TabsTo add additional tabs I also re-wrote quite a lot of the tabbing system. They were all using static components (set in the interface editor) and now use dynamic components (created in script).
In the interface editor the live version would look like:
Whereas it now looks like:
When you don't know the tool this can look a little scary - for a simple outline the green sections (Layers) are containers that hold other things (including script-generated components), orange (Rect) are used as simple dividing lines (moving the order of these has a big impact on the selected tab visuals!), and blue (Graphic) is the tab icon.
This means that to add new tabs we don't need to edit the interface file (which is also difficult to merge) and copy-paste all of the layers shown for each tab. The ~200 line file with ID to component remapping is also removed so it's much easier to update - whether it's additional tabs, graphics, icon changes or something else. The "tab spacer" components mentioned earlier were also changed in the same way.
So that's a look at just how complicated these things can be and why they take a while!
Enjoy the beta!