Bocek - League of Legends live commentary

I can (not) proudly say that I've been playing League of Legends since the end of season 1, which dates back to 2011. I had some breaks of course, even 1.5 years long but LoL comes back like an allergy, chronically. My another comeback to LoL just so happened not long after Bocek got his ability to speak. With the inspiration flowing I thought that it would be a great idea to integrate some LoL APIs into Bocek. If you have no idea what I'm talking about then I suggest you read the previous Bocek article.
Turns out that there is plenty of Riot Games APIs:
- Developer API - for players statistics, game history
- Data Dragon - for in game items and champions information
- Tournament API - for creating custom tournaments
- League Client API - allows to control the client with API
- Game Client API - reads the data of the live game
Having player statistics could be fun but then it would require a lot of work to analyze them and there is plenty of ready solutions to analyze your profile so that's a no go. Items and Champions are not really useful. I'm also not a tournament organizer. All that is left is League Client API and Game Client API. For the first one there is an API Explorer and so it happens that there is an absurd amount of endpoints, basically for every possible action inside the client. We're talking about sending messages, joining lobbies, picking champions, runes, adding friends, everything. Feeling pretty overwhelmed I glanced at the last possible API - Game Client API.
On the other hand the whole API documentation for Game Client API is on the webpage and you don't need any extra apps. The API exposes a plenty of data about the current game, most of the data is about the current player but you can also get the most important stuff about all the players in the game. In the sample response you can preview the whole data which is a lot. This API was the most promising one. What if Bocek could know what is happening during the game and comment on the fact that you've just killed someone or more importantly insult you when you died?
The endpoints are available automatically when the game starts so it's just a matter of connecting to them. By default the game data is available at https://127.0.0.1:2999/liveclientdata/allgamedata
. Getting it with Postman is pretty easy, same goes for Python with only exception being SSL errors. Thankfully they are explained in docs as well. There are 2 options, either download Riot root certificate or ignore SSL. Since I'm kinda lazy and Bocek runs on a Raspberry Pi in my local network I went for the latter one. The implementation is simple, I'm using an AsyncSession
from niquests
and setting the verification to false
from niquests import AsyncSession
from urllib3.util import Retry
retries = Retry(total=3, backoff_factor=0.1)
class Session(AsyncSession):
def __init__(self, base_url, headers=None):
self.base_url = base_url
self.headers = headers
super().__init__(retries=retries, base_url=base_url)
Then inside the Rito
cog class the session object is initialized with a base url. Similarly to Bocek random events, he needs to ask for the data constantly, thus rito_check
is a discord.py task than runs in a background.
class Rito(Cog, name="rito"):
def __init__(self, bot):
self.bot = bot
self.session = Session(f"https://{LOL_GAME_IP}:{LOL_GAME_PORT}/liveclientdata")
self.glossary = Glossary(self, "rito.json")
self.events = {}
self.rito_check.start() # start the loop
@tasks.loop(seconds=30)
async def rito_check(self):
async with self.session.get("/allgamedata", verify=False) as resp:
data = await resp.json()
print(data)
With that, every 30s we have a pretty big JSON printed to the console with everything that is happening during a game.
Everything works smoothly when run on the same PC that runs the game but I don't need a JSON to play, Bocek needs it and for that it's necessary to expose the 2999 port to the local network. Well, Windows has netsh
command that allows to do fair bit of networking through the cmd
. After some tinkering, it comes down to 3 lines that basically enable a port proxy on port 2999 and 127.0.0.1 becomes 0.0.0.0 available to the local network.
netsh
interface portproxy
add v4tov4 listenport=29999 listenaddress=0.0.0.0 connectport=2999 connectaddress=127.0.0.1
If it was Linux, we're done, but it's not :) Windows 11 hides the PC in a local network by default so it has to be turned off as well and finally it is mandatory to add an incoming firewall rule to allow accessing the port 2999.
Okay, we've got the port exposed, the data is accessible in a local network, so all that is left to do is for Bocek to recognize what is happening. Browsing through the response from /allgamedata
, you can see that one of the fields is called events
(also available at /eventdata
) and it contains only the events from the game, that is champion kills, deaths and objectives. Realistically that is all the information that is needed to know what is going on. The only problem is that the endpoint returns all the events that happened during the game but we're only interested in the most recent events that were unknown from x seconds ago until now. My solution to that is to calculate a difference between a dictionary from x seconds ago and the present one and we should get all the events that happened during these x seconds.
from deepdiff import DeepDiff
...
async def compare_stats(self):
events_prev = self.events.copy()
if not events_prev:
await self.get_all_events() # the first call
return None
events = await self.get_all_events()
if not events:
return None
to_ret = []
if not (
diff := DeepDiff(events_prev, events).get("iterable_item_added", None)
):
return None
for event in diff.values():
if not (processed := self.handle_event(event)):
continue
to_ret.append(processed)
Thanks to deepdiff
package we can easily get the difference between the two dictionaries that can be processed later. Unfortunately the events don't have an identical structure so there is a bit of dictionary processing left to do. Also it would be nice to only know about the events of certain players (me, friends) and not everyone. All of that is done inside the handle_events
function which I won't go into detail as it is just a bunch of if
s making sure that the resulting list of dictionaries has a specific set of fields.
In action packed game like LoL there can be a bunch of events, especially during multi kills and it would be annoying to have Bocek comment all of that. For that reason I introduced an event priority list that determines the likelihood of an event being commented on. The configuration is kept inside a configuration file and looks like so:
[tool.bocek.rito.event-possibility]
PentaKill = 1
QuadraKill = 0.8
TripleKill = 0.5
...
Bocek also needs a glossary, he cannot use the same "X killed Y" over and over, you have to get creative with the praises and insults of course. A full example of a glossary is available here but it goes like this:
{
"ChampionDeath": [
"{user}, you suck",
"{user} I'm starting to think it wasn't the last time"
],
"PentaKill": [
"{user} is a chad"
],
...
I've also included a player transcript just so Bocek knows how to pronounce some weird nicknames.
Finally, having the events filtered and having a bunch of lines to say we're coming to a point where we're just selecting one random event that happened during x seconds, creating a message based off of that, which can later be turned into an .mp3 by the TTS cog.
def create_msg(self, events):
for event_name, prio in EVENT_POSSIBILITY.items():
if event := next((e for e in events if e["EventName"] == event_name), None):
if random() < prio:
player = event["Who"]
event_name = event["EventName"]
user, _ = self.glossary.get_value("player_transcript", player)
msg, msg_placeholders = self.glossary.get_random(event_name)
scope = locals()
msg = replace_all(
msg, {f"{{{p}}}": eval(p, scope) for p in msg_placeholders}
)
return msg
return None
EVENT_PRIORITY
is loaded straight from the configuration file and if randomly chosen number is less than this possibility this event will get commented about. Because all events are processed into the same structure it's making it easier to insert the user's nickname and to find the correct category from the glossary.
In the later updates I decreased the time between subsequent calls to just 3 seconds what makes the commentary more lively and I must tell you that it keeps the games 10x more enjoyable or annoying if you keep dying and a bot keeps insulting you ;) If you want to see the whole code check out the repository.