This post describes how I updated our item code on Leaguepedia to support a case where an item name no longer refers to a unique item. (Items here refer to items in the game League of Legends.) This situation posed a problem because our scoreboards reference items by name, and we needed some solution to have unique inputs refer to unique outputs.
The problem we encountered was that a few items - Trinity Force, Duskblade of Draktharr, Locket of the Iron Solari, and Stealth Ward - were re-released or changed so substantially in patch 10.23, the season 11 preseason patch, that we needed to create physically separate pages on the wiki for the two versions of the items before and after this patch update. As a result, any mention of one of these items in a pre-patch scoreboard (i.e. any of the scoreboards from the past 10 years) would then link to the wrong location unless we did something about it.
(Note: Unless otherwise stated, all code in this post was originally written by me for Leaguepedia and is licensed under CC BY-SA 3.0.)
Solution 1: Change all the old names
The original solution was to just change all of the names in every single old scoreboard to use “Legacy” versions of the items. At first, we didn’t realize that there was a naming conflict with
Stealth Ward, and so we only updated three items:
Trinity Force (Legacy)
Duskblade of Draktharr->
Duskblade of Draktharr (Legacy)
Locket of the Iron Solari->
Locket of the Iron Solari (Legacy)
We ran a brief Python script written by one of my admins to convert all instances:
This script iterates through all parameters of the template in question, sees if they’re set to any of the items in question, and if so then updates them. (Note that I may have made a couple changes to this at runtime but I lost the most recent version of the code.)
There were a couple problems with this approach:
- If one of these three items (four, including the Stealth Ward) is changed again in the future, our naming scheme leaves much to be desired - this is only good for a one-time name swap. Now, this is perhaps ok considering we’re currently eleven years into League of Legends' existence, and this is the first time we’ve had this problem arise; however, I’d rather avoid this problem completely.
- This script took five days to run. Admittedly some of the slowness was because it was the first large-scale script I was running on Fandom’s new Unified Community Platform (UCP) and I had to write an entire new error-handling framework in
river_mwclientto deal with the platform differences compared to Hydra, but even without the delays caused by irrecoverable timeouts as I was developing my new error handling, it would have still been multiple days to run. I wanted a solution that didn’t require any input changing in previous scoreboards.
- I ran this script once the new patch was on live servers, but there could still be some games still played on the older patch afterwards (since the tournament realm lags behind live play), needing significant manual effort to track and update these items. Also, games from a year after the fact could be edited into the system later - these would need to have the correct
Legacyversions of items entered. There has to be some kind of automated patch-item association at some level to at least errorcheck this, and removing manual work altogether would be ideal.
Solution 2: Create a go-between in Lua that understands patch
While item by itself is no longer well-defined, of course there still is a pair of inputs that together are: item plus patch. Therefore, an alternative solution to the problem is to update my item processor so that it’s capable of reading both a patch and an item name from the scoreboard. Scoreboards already have patches, so it’s just a matter of changing scoreboard module code, creating some new framework to translate patch and non-unique item into unique item, and loading the necessary data into this framework.
My item framework (Item / ItemList) is part of the same set of code as my CompoundRole code that I previously wrote about - notably, this framework has no way of dealing with two inputs (though it does have an
opts parameter in the
init constructor - see Module:EntityAbstract). Therefore I decided to create a new go-between module which I called, extremely creatively,
ItemPatch, backed by data in a module called
An entry in
ItemPatchLookup looks like this:
There’s a list of versions to iterate through, each one consisting of a start “timestamp” and an end “timestamp” along with a name. The name is the unique key to return to the caller to send to the Item framework; the “timestamps” govern the range of patches in which this name is valid. Patches are formatted like
10.23 so to see if a patch lies within
versionEnd I just split on the literal
. character and see if it’s within this range (inclusive) - though actually the easier, and equivalent, task is to see if it’s not outside of this range (exclusive).
ItemPatch accepts an
item and a
patchString and looks up the item in its data module. If it finds nothing, great, there’s no “ambiguity.” Otherwise it looks up in between the versions as described above and returns the name it finds. Here’s the code:
And the full text of
Module:ItemPatchLookup before (spoiler!) I bailed on this approach:
Almost immediately after writing this I realized this was not what I wanted. What I actually wanted was to be able to send a patch as part of the
opts param directly into
ItemList and have it “just know” what to do - i.e. move all of the code from
ItemPatchLookup directly into
Solution 3: Build a patch lookup directly into my item framework
So let’s build a patch lookup directly into my item framework. But could I actually do this? The format of my variables modules has never been anything like this before - it’s always just been:
Or to give a concrete example from
Here I am just providing key-value pairs of display variables associated to the items. I wasn’t sure how tightly my code was coupled towards this extremely rigid structure that up until now every single one of my lookup modules shares.
Turns out, not at all coupled - I wrote extremely flexible code that actually didn’t care in the slightest how the
vars table was formatted. I could do whatever I wanted with that format. So I did!
First, I’ll show you the new format of
Module:Itemnames, when there’s patch-specific versions:
hasPatchSpecificHandling flag - I could probably have made do without and instead done some
ifs to detect format, but this feels a lot cleaner, plus it clues the reader into wtf the point of all of these tables is, so that self-documentation is pretty great.
Now let’s look at
Module:Item. Mostly I just copied the contents of what was previously
PatchItem in, but I also added a bit of extra functionality:
- If patch is not specified, default to returning the vars of the last interval, since this code is now mandatory to run every time an item is displayed in any context anywhere on the wiki; while scoreboards will (almost) always have patch (though see next section), the rest of the wiki won’t
- Since we’re built directly into
Itemnames, now we have to return a full set of vars, not just a name
- And also we just modify the class object instead of returning a value
Before the new code was added, here’s how the full code of
Module:Item looked (note
Module:ItemList was not modified at all):
What I skipped
There’s a few additional things I skipped over that I want to come back to - first, note that I also changed our naming convention along the way,
Stealth Ward has specific patch numbers in the name, not just
Legacy, because I wanted something that will scale even if we have to move pages again later. The other three items are also renamed similarly in their “Legacy” versions.
Another slight complication is that I slightly lied when I said what the format of patches are. Actually if you look at our patch list navbox you can see that prior to season 3, the format varies widely. However, two points make this okay:
- There are almost no scoreboards in the system prior to season 3
- I added support to scoreboards for a parameter
patch_sortthat we can set to a fake number that obeys the desired formatting, for example,
-1.0619as necessary. It’s the order that matters here, not the actual number!
I do still need to run a Python script to rename all of the items back to
Duskblade of Draktharr, and
Locket of the Iron Solari, and yes, that will take 3 days. But better this one-time thing than having an open-ended number of times to run this script in the future.
The automatic “show last version if patch isn’t specified” may be a pretty big detriment to error-checking missing patches, and I may need to add a flag in
opts to turn this behavior off, and enable this flag from
Module:Scoreboard. That said it might be better to have a check in
Module:Scoreboard itself that one of
patch_sort is defined, so I might leave it as-is.
Finally, Scoreboards aren’t the only place I need patches sent to items - this has to happen in match histories too! So I will need to add the
patch_sort param to the
ScoreboardGames Cargo table for sure, though after that it’s just a one-line change in
MatchHistoryPlayer to send patch to
ItemList - which was the entire goal of this approach. (Update - actually, the
MatchHistoryPlayer edit isn’t needed because the unique version of the item is stored, though I did add the
patch_sort parameter to Cargo for errorchecking purposes.) So while this project is still a bit away from done, it’s definitely on the right track, with the hard part over.
My EntityAbstract code continues to be pretty cool, and I’m really happy with the solution that I arrived at here. It was not the easiest problem to solve - I spent a while refusing to write any code for this to make sure that once I did write code, it would be the “right” solution, and even still, I changed approach almost immediately after writing anything. But that made it seem like a nice blog post to conclude the year on! Even if this isn’t quite as applicable as a gadget, hopefully you found this interesting to read. Happy 2021!