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:
- 1. Old School's method
- 2. Leave 0 of the item in the bank slot
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.
Automated Tests - Core Production - Content
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:
- Removed code limiting a trial account to 300 items in stacks (this trial system hadn't been used since ~2012)
- Removed restrictions from old content (such as Bounty Hunter worlds)
- Reducing redundancy in the scripts
- Moved display-only calculations into the client (for example the current/total slots counter)
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:
- Removing an item:
- Does anything stop you removing the item? (Area, status effect, etc...)
- Run side-effects (e.g. ring of visibility deactivation)
- Equipping an item:
- Do you meet the requirements to equip the item?
- Does anything stop you wearing the item?
- Is this trying to equip to a valid slot?
- Does the item need to change type? (new to used, lightness items)
- Run side-effects
- Beast of Burden
- Is it summoned?
- Is this familiar able to hold enough items?
- Is it too valuable for it to store?
- Is it a prevented item
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:
- A lot of player's skilling presets will have the appropriate skilling outfit saved, only really expecting to change the inventory each time it loads. But each time the worn outfit would be deposited and withdrawn again.
- The area base restrictions run for every slot, but they only apply once.
- It's very unlikely you don't meet the requirements as you needed to be wearing an item to save it, BUT the requirements can change across game-updates.
The changes I made included:
- When trying to empty inventories to load a preset, it now skips the slot if it's already the item in the preset.
- After the first load of a preset per login the requirements are skipped (unless cleared e.g. via pure resets).
- Area based restrictions are only checked once per load.
- A lot of tweaks on how side-effects work.
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!):
- Old School & RS
- Click inventory to deposit
- Drag from Bank to Bank (swap slots)
- Drag from Bank to tab (create a tab)
- Drag from Bank to tab (move to tab)
- Click Bank to withdraw to inventory
- OS Only
- Drag from Bank to insert (move before item
- RS Only
- Drag from Bank to insert (move before item)
- Drag from inventory / worn / Beast of Burden to insert (deposit before item)
- Drag from Bank to end of tab spacer (move to end of Bank)
- Drag from inventory / worn / Beast of Burden to end of tab spacer (deposit to end of Bank)
- Drag from inventory / worn / Beast of Burden to empty slot (deposit)
- Drag from inventory / worn / Beast of Burden to occupied slot (swap with non-Bank inventory)
- Drag from inventory / worn / Beast of Burden to specific tab (deposit to end of tab)
- Click bank to withdraw to worn / Beast of Burden / money pouch
- Right click Bank to withdraw to worn
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:
- Cyan is an 'insert' space, e.g. dragging something to cyan 2 would insert before the item in slot 2
- Orange is the item slot
- Red is the end of tab 'drop space' to deposit to the end of the tab
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:
- %bank_tab_2 = 1
- %bank_tab_3 = 1
- %bank_tab_4 and higher = 0
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:
||%bank_tab_2 + %bank_tab_3
||%bank_tab_2 + %bank_tab_3
||%bank_tab_2 + %bank_tab_3 + %bank_tab_4
||%bank_tab_2 + %bank_tab_3 + %bank_tab_4
||%bank_tab_2 + %bank_tab_3 + %bank_tab_4 + %bank_tab_5
||Follow the same pattern
||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:
- Dragging from slot 1 to insert (cyan) 2 is trying to move the item to its current position in the Bank - this actually just needs to remove 1 from %bank_tab_3.
- Dragging from slot 1 to spacer (red) for tab 2 would similarly need to add 1 to %bank_tab_2 and remove 1 from %bank_tab_.
- Dragging from slot 2 to insert (cyan) 3 changes nothing and needs to be ignored.
- Dragging from slot 2 to insert (cyan) 4 is actually just swapping with slot 3.
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:
- Fail - Isn't a wearable item.
- Fail - Needs a confirmation before equipping.
- Already wearing the item?
- Success - It's stackable so can add more.
- Fail - It's the same item and doesn't have augmented data.
- Fail - Item isn't allowed to be worn (requirements, specific areas, etc).
- Is the item a weapon or shield and does that conflict with currently equipped items (e.g. 2h sword needs to deposit a shield)?
- Fail - We're not withdrawing the last item or we're leaving placeholders and there's not enough space to deposit the extra item and/or the weapon being replaced.
- Success - No items equipped, or can safely swap, withdraw as normal.
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:
- Was it dragged to a tab, insert spacer, drop space, or specific slot?
- Ignore the free slot - unless this would push the bank beyond max size in which case compress and offset the target slot for spaces removed.
- Is it a single slot required?
- Are multiple slots required (eg deposit 5 of the same augmented item)?
- Compress to guarantee all 5 get positioned together and make 5 spaces at the position of the first free slot.
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.
To 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!
Principal Technical Developer