A jornada de um juninho construindo um assistente de League of Legends.
Eae pessoal, me chamo Igor Tiburcio, vulgarmente conhecido como DuduIsOnFire. Sou estudante de Análise e Desenvolvimento de Sistemas e venho estudando programação a pouco mais de um ano. Recentemente pude fazer uma mentoria com o Lucas Badico e comentei com ele sobre um projeto que fiz, ele acabou achando interessante e comentou que eu deveria documentar e compartilhar sobre esse projeto. Pois bem, resolvi criar coragem e falar um pouco sobre minha trajetória com esse projeto e compartilhar um pouco de informação que talvez possa ajudar algum outro iniciante assim como eu.
1. Qual problema eu queria resolver?
Falarei um pouquinho de mim e o motivo de eu ter começado esse projeto. Até meados do ano passado eu era advogado, me formei com 21 e advoguei até os 24. Eu sempre gostei bastante de tecnologia e depois que terminei a faculdade de direito acabei entrando mais fundo nesse mundo e me resolvi por migrar para o Linux.
A minha migração para o Linux foi bastante difícil, afinal, sou um gamer e amo jogar no PC, e a quantidade de configurações que precisam ser feitas para jogar numa distro Linux é intimidadora para alguém que nunca havia visto tecnologia de uma forma mais profunda. Esta no ambiente Linux foi o que me fez criar vontade de virar programador e foi aí que comecei a estudar.
Agora vamos aos finalmente, eu gosto muito de jogar League of Legends, e consegui fazer o jogo rodar no Linux com exito. Porém, quem joga LoL sabe que antes do início da partida o jogador precisa escolher um personagem e um conjunto de runas (uma espécie de habilidade passiva) para o campeão escolhido. No Windows existem uma série de aplicativos que automatizam essa escolha de runa já selecionando as melhores runas para um determinado campeão, porém no Linux não existia um aplicativo parecido e foi aí que resolvi escrever o meu próprio.
2. Quais tecnologias usar?
Na época em que comecei a pensar sobre desenvolver isso, eu havia estudado apenas JavaScript, TypeScript e estava estudando React. Então eu não tive muitas opções, não era a melhor combinação para escrever um app desktop, porém quem não têm cão caça com gato. Pesquisei como poderia criar um app desktop com essas tecnologias e achei o Electron, foi a primeira opção que encontrei e adotei sem pensar muito. Por fim, pra estilizar, como não gosto muito de escrever CSS peguei o Tailwind e segui feliz para montar meu ambiente de desenvolvimento.
3. O processo de desenvolvimento.
3.1. Montando o ambiente.
Eu nunca havia feito uma aplicação de ponta a ponta, ainda mais utilizando tantas tecnologias, sendo duas delas sem ter tido nenhum projeto, então vocês devem imaginar que tive muita dor de cabeça para fazer funcionar o Electron com o React e TypeScript, mas nada que algumas horas pensando e lendo documentação não resolvesse. No fim do dia achei o Electron Forge que facilita bastante essa integração.
3.2. Descobrindo como funcionava o Electron.
Quando comecei a pensar em desenvolver, a única coisa que eu sabia sobre o Electron é que ele funcionava como um navegador, então a primeira coisa que pensei foi: vou por o React e fazer toda a lógica da aplicação nele. Como eu estava enganado, por questões de segurança, o Chromium não acessa diretamente a maquina em que está rodando, impedindo assim que você possa coletar dados importantes para a aplicação.
Pesquisando mais sobre isso descobri que o Electron funciona com duas camadas de aplicações, uma é o Chromium rodando por cima e renderizando as coisas na tela e a outra é o NodeJS por baixo e ele, sim, tem acesso ao sistema operacional. E por meio de alguma espécie de gambiarra essas duas camadas se comunicam, e isso vai ser muito importante para os próximos passos.
3.3. O primeiro problema: como comunicar meu app com o client do LoL?
Se vocês acham que dominar apenas as tecnologias é o suficiente para desenvolver um software, é porque nunca precisaram lidar com fatores de tecnologias externas. Quando eu comecei a programar eu sabia que a aplicação era possível, pois já tinham feito para Windows, mas eu não fazia ideia de como. Então fiz o mais óbvio no momento, abri o jogo, entrei numa partida contra bots, deixei na tela de seleção de personagens e comecei a vasculhar os arquivos do jogo para ver se eu achava alguma alteração nos arquivos. Eureca, com alguns minutos procurando achei um arquivo JSON dentro da pasta do LoL onde o jogo deixava salvo as runas atuais configuradas na sua conta.
Abrir um sorriso maior do mundo e comecei a desenvolver, foi nesse momento que me deparei com o problema que citei um pouco acima sobre o Chromium não acessar arquivos locais. Basicamente você precisa escrever código para duas aplicações diferentes e você expões os dados de uma para outra através de uma API do próprio Electron, mais uma vez, um pouco de documentação e algumas horas batendo a cabeça e consegui resolver o problema.
Por fim, consegui fazer com que o meu programa lesse e escrevesse no arquivo JSON. Abri o jogo, rodei o programa com um código que mandava escrever novas informações no arquivo e como resultado eu tive um grandioso, NADA. Por mais que eu alterasse o arquivo JSON onde ficava configurado a página de runas, o comportamento dentro do client do jogo não mudava. Pois bem, hora de voltar para a internet. Passei muitas horas pesquisando sobre isso em forums e comunidades de League of Legends, melhor dizendo, algo em torno de três ou quatro dias procurando, até que achei uma comunidade de desenvolvedores de aplicativos para o LoL e lendo alguns artigos descobri algo muito interessante.
Quando o client do LoL abre ele solicita uma porta para o sistema operacional, gera uma password e sobe uma API Rest em localhost e você pode se comunicar pelos end-points dessa API, por sorte a comunidade já tinha documentado todos os end-points existentes. Depois dessa grande descoberta voltei para o VSCode, rodei um “npm install axios” e parti pra cima, a grande dúvida era, como descobrir qual porta o LoL subia essa API e qual password para usar na requisição HTTP? Voltei para o forum de devs que comentei e facilmente achei a resposta. Assim que o jogo abre ele cria um arquivo na pasta principal do jogo chamado “lockfile”, nesse arquivo tem a password e a porta.
Pronto, primeira parte concluída, refiz meu código para ler o arquivo “lockfile” ao invés do e escrevi uma lógica para pegar a porta e a password desse arquivo e estava feito, o coração da minha aplicação estava pulsando e vivo. O grosso já estava feito, era o que eu pensava. Mais uma vez, ledo engano…
3.4. Primeiras páginas React.
Agora com o coração da aplicação pronto, eu podia me preocupar apenas com as telas da aplicação e com a lógica da aplicação, então comecei, a página inicial da aplicação seria para o usuário selecionar o “.exe” do jogo e a partir disso o programa pegaria o “path” e vasculharia procurando o arquivo “lockfile”, mais uma vez enfrentei o problema do Chromium não pode acessar o sistema operacional, só que agora ao invés da camada de baixo passar a informação para a camada do Chromium eu tinha que fazer uma box de seleção de arquivo e passar a informação da box para a camada de baixo processar e vasculhar buscando o arquivo em questão. Mais uma vez bati a cabeça um pouco e com algumas horas vasculhando na internet descobri que o Electron expunha uma espécie de API para integrar o “select file” do navegador com a camada por baixo do Chromium.
Agora que eu tinha conseguido pegar o path correto, eu simplesmente salvei ele no Local Storage e coloquei uma verificação nessa página inicial. Se existisse um path salvo no Local Storage o programa pegaria e passaria para a próxima página. Se não houvesse renderizar a box para selecionar o arquivo. Próxima página.
Essa segunda página eu nomeei de isClosed, eu sei, péssimo nome, mas foi o que meu de alguns meses atrás pensou. Essa página funcionava de uma forma muito simples, ela fica fazendo um pooling verificando se o arquivo “lockfile” existe, se ele não existir a página exibe a mensagem “Client is Closed”, se existir ela pega as informações do arquivo “lockfile” e passa para a próxima página.
3.5. Exibindo informações do client.
Essa terceira página eu chamei obviamente de isOpen, como eu disse, péssimo nome. Essa página foi realmente um enigma para mim, para ela eu queria quatro coisas: exibir o nome de usuário logado no client do LoL, pegar as informações das partidas mais recentes, verificar se o client do jogo estava fechado e verificar se o jogar tinha entrado em alguma partida.
Exibir o nome de usuário e as informações das últimas partidas foi fácil, eu só precisei fazer a requisição para API do LoL puxar os dados e exibir na tela, claro que tive outras complicações no meio do caminho, mas que não vou expor aqui, pois o artigo já vai ficar bastante grande sem elas, porém quem quiser abrir o código e ver vai notar que não comentei de alguns passos aqui.
O meu problema real surgiu com uma dúvida, como verificar se o client do jogo continuava aberto e como verificar se o usuário tinha entrado numa partida. Bom, eu tinha em mão vários end-points e poderia usar eles para essas verificações. O primeiro end-point era o principal onde eu extraia o nome de usuário, era só fazer um pooling que ficava batendo nele a cada x milissegundos, enquanto ele retornasse o status code 200 o client estava aberto, senão, o client estava fechada e retornava para a página de client fechado, simples. O segundo end-point que usei era uma que pegava os dados da seleção de campeão (esse é muito importante, pois também é por onde você controla as runas do jogo), enquanto esse end-point retornasse o status code 400 o programa não fazia nada, mas se retornasse 200 significava que uma partida tinha começado e renderizava a próxima página.
Parecia simples e fazia sentido, então comecei a implementar esses dois poolings nessa página. Bom, aqui vai uma dica, implementar um pooling na mão no React COM TODA CERTEZA É UMA PÉSSIMA IDEIA. Descobri da pior forma que minha ideia funcionava na teoria, mas tinha um problema enorme, quando o React renderizava a próxima página ele não parava o pooling da página anterior, isso gerava um loop eterno de ir para pagina de seleção de personagem, client fechado e client aberto, e toda vez que renderizava essas paginas novos pooling eram executados e isso acontecia até a aplicação quebrar. Isso tem relação em como o LifeCycle do React funciona, não vou parar para explicar aqui como funciona, mas vale dar uma olhada na documentação. Esse problema me consumiu uma semana, passei dias tentando arranjar formas de entender e fazer o pooling funcionar, por fim eu consegui, manipulando estados e mexendo nos estados em diversas páginas diferentes consegui estabilizar a aplicação e de quebra aprendo MUITO sobre React. O engraçado foi que quando eu consegui resolver esse problema eu fiquei tão eufórico que fui comentar num grupo do Discord que eu participo e alguém só falou “por que tu não usou o React Query?”. Poisé, existia uma biblioteca que implementava essa funcionalidade e lidava com os problemas do React. Mas bom, não foi tempo perdido, aprendi como a biblioteca funcionava por experiência própria, voltei para o projeto, “npm install react-query”, refiz as páginas em questão e parti para a próxima.
3.6. Selecionando as runas.
A essa altura do campeonato eu já estava desenvolvendo esse programa há quase um mês. Eu já tinha uma boa noção de como o React funcionava, como lidar com o Electron e o ecossistema do client do LoL. A próxima página era a que lidava com a informação da partida e fazer as requisições para alterar as runas do jogo. Tinha tudo para ser fácil, eu literalmente já sabia fazer tudo que eu imaginava, mas a vida de dev nunca é fácil, vocês sabem…
Quando eu comecei a fazer essa página tudo fluía lindamente, no fim das contas eu precisava fazer uma série de requisições, uma para verificar o campeão selecionado, outra para selecionar a página de runas atual, outra para apagar essa página de runas e por fim a última para setar a nova página de runas. Claro, não podemos esqueça do pooling para verificar se a partida terminou e voltar para a página anterior. Parece simples certo? Errado.
O problema que se materializou na minha frente foi, como eu vou saber quais as melhores combinações de runas para um determinado personagem? A Riot não fornece essas informações, mas eu sabia que alguns sites compilam essas informações e você pode acessar a página para verificar. Para minha sorte, no mesmo grupo do Discord que comentei antes eu tinha visto alguém falar sobre um tal de WebScrap que tinha feito em Python, o conceito era um programa que acessava uma página web e extraia informações dela.
Pensei comigo, se dá para fazer em Python dá para fazer em TypeScript, comecei a pesquisar e achei um tutorial que ensinava a fazer um WebScrap com JS usando as bibliotecas Cheerio e Puppeteer. Em pouco tempo eu tinha o WebScrap funcional e pegando os dados que eu queria, porém, mais uma vez outro problema surge, o scraping demorava alguns segundos, para selecionar o campeão todo segundo importa, o jogador não poderia esperar até o programa fazer o scraping e alterar as runas dele. Esse problema me consumiu, chegou a hora de falar do nosso back-end.
3.7. Um back-end para salvar nossa lista de runas.
Nesse momento eu já estava desenvolvendo essa aplicação há alguns meses e sempre intercalando com estudar e fazer a faculdade, portanto, eu já sabia fazer algumas coisas e já dominava bem mais a arte de programar do que quando tinha começado. A minha primeira ideia para resolver o problema do tempo das runas foi: e se eu terceirizar esse WebScrap para uma API onde eu possa fazer requisição com o nome do campeão e pegar os dados prontos e já formatados como eu preciso?
A ideia era realmente ótima, fazia todo o sentido do mundo para mim. Fechei os códigos em TypeScript, abri o terminal do Linux, “dotnet new webapi”, vamos começar. Honestamente essa parte foi bem tranquila, não tive tantos problemas até porque, como eu disse, já estava bem mais experiente programando. Montei a API em C#, procurei um tutorial de como fazer WebScrap com Dotnet, achei rápido, apliquei a mesma lógica que eu já tinha feito no scrap em TS e em pouco tempo eu tinha uma API que cuspia as runas que eu queria. Porém, mesmo problema de antes, 3-4 segundos para fazer o scraping, isso na minha máquina que é uma Ryzen 5 3600 com 16gb de ram, na VPS que eu tenho, onde pago 25 reais e ganho 1gb de ram junto a um Xeon quase morto por dentro, isso ia demorar uns 12-16 segundos.
Felizmente dessa vez eu já sabia como resolver, primeiro eu precisaria de um banco de dados para salvar as runas toda vez que o programa fizesse um Scraping, configurei rapidamente um MongoDB, pela própria Atlas, eles oferecem um plano gratuito que era mais que o suficiente para mim (inclusive foi por isso que escolhi Mongo, poderia ter sido feito facilmente com SQL também). Fiz o programa verificar primeiro no banco de dados se existia um conjunto de runas salvas para o campeão solicitado na requisição, se tivesse ele pegava e devolvia, senão, ele fazia o scraping, salvava no banco de dados e devolvia em seguida. Mas isso não resolvia meu problema, na verdade, essa tinha de ser a última opção.
Então eu pensei: eu deveria fazer alguma forma de automatizar que o scraping de todas os conjuntos de runas possíveis para todos os campeões. Foi exatamente o que fiz, programei para quando o programa iniciasse ele verificasse no banco de dados se todas as runas estavam salvas (basicamente ele pegava quantos conjuntos de runas tinham salvas, cada campeão tem 5 possibilidades de conjunto de runas, uma para cada lane do jogo, se o número de conjuntos fosse 5 * o número de campeões então tudo certo), caso estivessem o programa iniciava sem fazer nada. Caso não estivessem o programa iniciava e numa thread por fora ele fazia o scraping de todas as runas e salvava no banco de dados.
Também, limitei que os dados só ficassem no banco de dados por 36 horas e configurei um CronJob para a cada 34 horas atualizar todos os conjuntos de runas e resetar o tempo limite dos dados no DB. Com isso minha API estava pronta, subi no GitHub, fiz um pipeline com GitHub Actions para fazer o Deploy na minha VPS e estava tudo pronto.
3.8. Voltando para a seleção de runas.
Agora com um back-end pronto me fornecendo as runas num tempo em torno de 120 a 200ms, com o app Electron praticamente todo montado eu só precisava voltar para o código em TypeScript, nova instancia do Axios para fazer requisições para a minha API e pronto, já podia enviar as runas para o client do LoL.
E foi simples assim, claro depois de praticamente tudo pronto esses últimos detalhes foram um passeio no parque, a maioria de vocês devem já imaginar como eu fiz dado tudo que escrevi.
Concluída essa parte da troca de runas eu só precisava fazer uma verificação se a partida tinha acabado e caso tivesse terminado voltar para a página principal do app. Mais um pooling feito e eu tinha terminado a aplicação.
4. Conclusão.
Esse projeto é a maior prova que prática em programação é aquilo que molda seu conhecimento, eu aprendi mais com esse projeto do que com qualquer curso que eu tinha feito até aquele momento. O projeto me testou, me estressou, muitas vezes fez eu pensar em desistir achando que seria incapaz, mas ele finalmente ficou pronto. Consegui terminar nele tudo o que eu queria, claro em questão de UI/UX ele tá bem feinho ainda, isso são planos para outros momentos.
Espero que esse artigo aqui sirva para passar conhecimento aos juninhos como eu, e que sirva também para motivar vocês a praticarem cada vez mais e a não desistirem quando se sentirem desmotivados. Quem se interessar pelo projeto segue os links dos repositórios no github: