World of Warcraft Discord
│ ▲
│ Estado do personagem │ Rich Presence
▼ │
┌──────────────────┐ Pixels ┌──────────────────┐
│ Addon Dwow │ ─────────> │ Companion Python │
│ Lua │ RGB RGB │ Captura + Decode │
└──────────────────┘ └──────────────────┘
Comunicação Sem Socket :3
[0x1] Como Surgiu O Dwow?
[1x0] O Problema Da Sandbox Do WoW
[2x0] O Protocolo De Pixels
├──[2x1] Geometria Da Faixa
├──[2x2] Header E Payload
├──[2x3] Checksum Adler-24
└──[2x4] Heartbeat E Magic Bytes
[3x0] Do Lua Até O Discord
├──[3x1] Coletando O Estado Do Personagem
├──[3x2] Transformando Bytes Em Cores
├──[3x3] Capturando E Decodificando
└──[3x4] Criando O Rich Presence
[4x0] Compatibilidade E Proteções
[5x0] Finalização
───────────────── Como Surgiu O Dwow? [ 0x1 ] ![]()
Ola gente! hoje eu vim mostrar para vocês um projeto que junta duas coisas que eu gosto muito,
World of Warcraft e ficar criando ferramentas diferentes que talvez apenas eu pensei em fazer kk
![]()
O nome dele é Dwow, um addon junto com um programa em Python que mostra no Rich Presence do Discord oque meu personagem esta fazendo dentro do WoW Classic.
Ele consegue mostrar coisas como:
- Nome, raça, classe, level e reino do personagem
- Zona, subzona, masmorra ou raid atual
- Combate, alvo e porcentagem de vida
- Montaria, voo, forma de druida e viagem de táxi
- Pesca, mineração, leilão, correio e outras atividades
- Fila de masmorra, battleground e até quando a fila popa
- Morte, fantasma, wipe, ressurreição e luta contra boss
De primeira isso parece simples, pegar as informações e mandar para o Discord, certo? Porem o problema começa justamente no mandar. Um addon do WoW não pode simplesmente abrir um socket, fazer uma requisição HTTP ou escrever toda hora em algum arquivo do computador.
Então como fazer o Lua conversar com nosso Python se ele não pode usar rede? bom, eu fiz a propia tela do jogo virar o cabo que passa os dados :P
───────────────── O Problema Da Sandbox Do WoW [ 1x0 ]
Os addons do WoW rodam em Lua dentro de uma sandbox, eles conseguem pegar varias informações usando a API oficial e desenhar coisas na interface, porem não conseguem sair conversando livremente com outro programa fora do jogo.
E isso é uma proteção importante, eu não queria fazer o Dwow lendo memoria, injetando DLL ou mandando input para o personagem pois alem de ser perigoso isso ja começa a atravessar uma linha que meu projeto nem precisa chegar perto.
Bom, mesmo sem rede o addon ainda pode desenhar cores na tela, e uma cor RGB tem 3 canais indo de
0 ate 255. Logo um unico pixel pode carregar três bytes, Acho que ja deu para entender aonde eu quero
chegar né? ![]()
R = primeiro byte
G = segundo byte
B = terceiro byte
RGB(66, 101, 122) ──> 42 65 7A ──> "Bez"
Com isso nasceu o protocolo de pixels do Dwow, o addon coloca os dados do personagem em uma faixa pequena de cores e o Python tira uma captura, le os RGBs e monta tudo novamente do outro lado.
Não existe socket entre eles e nem arquivo sendo atualizado, a tela é literalmente o fio ligando o Lua ate o Python.
───────────────── O Protocolo De Pixels [ 2x0 ] ![]()
Para isso não ficar dependendo da sorte eu criei um protocolo de verdade, atualmente na versão 1. Ele tem uma assinatura para achar onde começa, versão, tamanho, contador, checksum e nosso payload. Pode parecer ate coisa demais para uma faixinha de cor, mas cada parte resolve algum problema que eu fui encontrando durante as capturas.
──── Geometria Da Faixa [ 2x1 ]
Cada célula é um quadrado de 3x3 pixels físicos que guarda uma cor RGB. No começo da para pensar “por que não usar só 1 pixel?”, porem usando 3x3 a leitura fica mais resistente a borrões e mudanças que podem acontecer nas bordas.
E para uma borda alterada não estragar o byte, o decoder pega apenas o pixel que esta no centro:
┌─────┐
│ · · │
│ · X │ X = Pixel central lido pelo companion
│ · · │
└─────┘
3x3
As células começam no canto superior esquerdo e vão da esquerda para direita. Depois de 128 células ela continua na linha de baixo. Como temos 3 bytes em cada uma, uma linha consegue carregar ate 384 bytes usando cores, oque ja é bastante para mostrar o estado do personagem.
Porem apareceu outro problema que foi a escala da UI. Nem todo mundo joga na mesma resolução e alguem
pode colocar a interface em 80%, 100% ou qualquer outro valor. Para nossas células continuarem com
exatamente 3 pixels físicos eu usei SetIgnoreParentScale e essa conta:
escala = 768 / altura física da tela
Assim a unidade da interface acompanha o pixel físico e a configuração de UI não bagunça toda a leitura.
──── Header E Payload [ 2x2 ]
As primeiras 5 células são nosso header, e os dados de verdade começam na célula 5 pois estamos contando a primeira como 0. Ficando assim:
┌────────┬────────────────────────────────────────────────────────────┐
│ Célula │ Conteúdo │
├────────┼────────────────────────────────────────────────────────────┤
│ 0 │ Magic A: (192, 255, 238) │
│ 1 │ Magic B: (13, 21, 234) │
│ 2 │ Versão + tamanho do payload em 16 bits │
│ 3 │ Contador de sequência + dois bytes reservados │
│ 4 │ Checksum Adler-24 │
│ 5+ │ Payload UTF-8, três bytes dentro de cada célula RGB │
└────────┴────────────────────────────────────────────────────────────┘
O payload é apenas uma string UTF-8 com os campos separados por |, algo simples de criar no Lua e
simples de separar no Python:
Grubento|Firemaw|WARRIOR|Guerreiro|Orc|47|Vale Estrangulacér...|...
A ordem é fixa, começando com nome, reino, classe, raça e level e depois vem zona, instancia, vida, XP, grupo e guilda. Mais para frente temos flags, alvo, ouro, facção, forma, atividade, dificuldade e montaria.
Hoje o protocolo tem 31 campos e um limite de 600 bytes, quando preciso colocar algum campo novo eu sempre adiciono no final. Assim versões antigas não viram uma bagunça e um decoder novo ainda consegue entender um payload que não tem as features mais recentes.
Para não fazer um campo separado para cada true ou false eu juntei varios estados em um unico numero
chamado flags:
1 = Táxi 32 = AFK
2 = Combate 64 = Fantasma
4 = Descansando 128 = Furtivo
8 = Montado 256 = Voando
16 = Nadando 512 = Caindo
1024 = Pescando
Se esse numero for 34, por exemplo, temos 32 + 2, então nosso personagem esta AFK e em combate.
Assim eu consigo misturar varios estados usando apenas um campo :D
As atividades mais específicas utilizam tokens compactos:
boss:Onyxia ──> Lutando contra Onyxia
hearth:Orgrimmar ──> Usando a Pedra de Regresso para Orgrimmar
breath:37 ──> Restam 37% de fôlego
bgqueue:Warsong ──> Na fila para Warsong
mine ──> Minerando
ah ──> Na Casa de Leilões
O addon escolhe uma atividade especial por tick usando uma prioridade. Estar carregando bandeira, apanhando de um boss ou quase sem ar obviamente é mais importante do que falar que abrimos um vendedor.
──── Checksum Adler-24 [ 2x3 ]
Capturar uma cor não é tão perfeito como ler um byte de um arquivo, coisas como HDR, filtros ou outro overlay podem mudar apenas um canal do RGB e pronto, nossa letra pode virar outra.
Para descobrir quando isso acontece eu usei o Adler-32 cortado para os 24 bits menores, que chamei no projeto de Adler-24. E olha que conveniente, 24 bits cabem perfeitamente em um RGB:
Checksum = Adler32(payload) & 0xFFFFFF
R = bits 23 até 16
G = bits 15 até 8
B = bits 7 até 0
O Lua calcula antes de desenhar e o Python calcula novamente depois de ler. Caso os resultados sejam diferentes eu descarto aquele frame, pois alguma coisa mudou no meio do caminho.
É melhor perder uma atualização do que colocar no Discord um nome ou lugar todo corrompido kk
──── Heartbeat E Magic Bytes [ 2x4 ] ![]()
Os dois primeiros RGBs são valores fixos que eu chamei de Magic A e Magic B, eles são como uma placa falando para o companion “Opa! nosso protocolo começa aqui”.
Somente nesses magics existe uma tolerância de ±4 para ajudar a encontrar a faixa. Depois disso os bytes precisam ser exatos e quem valida nosso conteudo é o checksum.
A sequência é um contador de 0 ate 255 que muda em todo desenho. Mesmo com meu personagem completamente parado e sem nada novo no payload, esse numero continua andando uma vez por segundo.
... 253 ──> 254 ──> 255 ──> 0 ──> 1 ...
Isso virou o heartbeat do protocolo, se o Python continua tirando print mas o contador fica congelado ele sabe que esta olhando para uma imagem velha ou que o jogo parou de atualizar.
───────────────── Do Lua Até O Discord [ 3x0 ]
Bom, agora ja sabemos como a mensagem é montada, então vamos seguir ela do WoW ate aparecer no Discord.
──── Coletando O Estado Do Personagem [ 3x1 ]
Dentro do jogo nosso Core.lua utiliza apenas a API oficial do WoW. A cada segundo ele pega informações
usando coisas como UnitName, UnitClass, UnitRace, UnitHealth, GetInstanceInfo e GetRealZoneText.
Alguns estados chegam por eventos, como começar uma luta, abrir o banco, receber convite ou aparecer uma ressurreição. Outros eu verifico no tick como montaria, movimento, buffs, alvo, pesca e vida.
Depois disso ele limpa os textos, troca algum | que apareceu no meio por /, coloca os limites para
ninguem criar um payload gigante e junta tudo usando nosso separador.
API do WoW ──> Campos ──> Sanitize ──> table.concat(fields, "|")
──── Transformando Bytes Em Cores [ 3x2 ]
O Encoder.lua pega essa string, calcula tamanho e Adler-24, aumenta a sequência e vai separando nosso
payload de 3 em 3 bytes.
"B" = 66
"e" = 101
"z" = 122
SetColorTexture(66/255, 101/255, 122/255, 1)
Como o WoW usa cores entre 0.0 e 1.0 temos que dividir os canais por 255. Para quem esta jogando isso parece apenas uma pequena faixa estranha no canto, porem para nosso Python aquilo é o personagem inteiro.
──── Capturando E Decodificando [ 3x3 ]
Do lado de fora temos nosso companion em Python, ele procura a janela real do WoW pela classe
GxWindowClass e captura apenas a area do jogo usando PrintWindow do Windows.
Ele não le memoria e não manda input algum, é apenas uma captura de tela automatizada.
O caminho do decoder é:
Captura Da Janela
│
▼
Procura Magic A + Magic B
│
▼
Lê Versão E Tamanho
│
▼
Reconstrói Os Bytes RGB
│
▼
Valida Adler-24
│
▼
UTF-8 + split("|")
│
▼
CharacterState
No final isso vira um CharacterState, assim a parte do Discord não precisa saber como ler pixel e a
parte que le os pixels não precisa entender nada de Rich Presence. Cada um com seus problemas :P
──── Criando O Rich Presence [ 3x4 ]
Por fim chegamos na presence, aqui eu tambem coloquei prioridades para não criar frases estranhas. Estar morto ou lutando contra um boss tem que aparecer antes de algo comum como estar descansando né.
Além do texto, o card pode mostrar retrato da raça, classe, forma atual, montaria, olho verdadeiro do LFG, tamanho do grupo, guilda, ouro e o tempo da sessão.
O companion manda tudo pelo IPC local do Discord usando pypresence. Ele só atualiza quando algo
importante muda e respeita um tempo minimo de 15 segundos, pois o Discord tambem não gosta que fiquem
tentando trocar o RPC a cada segundo.
───────────────── Compatibilidade E Proteções [ 4x0 ]
Fazer isso funcionar uma vez foi facil comparado a fazer funcionar com resoluções, escalas, addons e versões diferentes do WoW Classic. Então fui colocando algumas proteções pelo caminho:
- Busca pela faixa: normalmente ela está em
(0,0), mas addons de viewport podem mover oWorldFrame. Se o magic some, o decoder procura na região superior esquerda e salva a nova origem. - Tolerância nos magics: uma pequena diferença de cor ainda deixa encontrar a assinatura, enquanto o checksum continua exigindo que o conteúdo esteja perfeito.
- Detecção de congelamento: se o contador de sequência não muda, o frame não está vivo.
- DPI awareness: impede a escala do Windows de deslocar as coordenadas em monitores acima de 100%.
- Compatibilidade de payload: campos opcionais entram somente no final e sua ausência não é um erro.
- Compatibilidade entre clientes: chamadas diferentes do Classic Era, Anniversary e MoP ficam isoladas e são verificadas antes de usar.
- Teste round-trip: o teste cria uma imagem igual à faixa desenhada pelo Lua e confirma que o Python reconstrói exatamente o mesmo personagem, incluindo UTF-8, múltiplas linhas e corrupção de pixels.
Também existe o comando /dwow para esconder ou mostrar a faixa. Quando ela é desativada, o companion
perde os magics e depois limpa o Rich Presence automaticamente.
O Dwow é apenas leitura, o addon usa a API oficial e o companion olha os pixels. Ele não injeta codigo,
não le memoria e não automatiza o personagem, pois isso não é necessario para o projeto e poderia colocar
a conta de alguem em risco.
───────────────── Finalização [ 5x0 ] ![]()
Bom esse foi um dos projetos que eu mais gostei de criar, todo mundo olha o Rich Presence bonitinho no Discord porem por baixo tem um protocolo carregando bytes usando quadradinhos coloridos huew huew.
Lua ──> UTF-8 ──> RGB ──> Captura ──> Bytes ──> Python ──> Discord
A sandbox que parecia ser o maior problema acabou criando a parte mais interessante. Ao invés de tentar burlar ela eu usei algo que o addon ja podia fazer, desenhar na interface, e fiz isso virar nossa ponte sem precisar tocar na memoria do WoW.
Enfim aquela faixinha estranha no canto não é apenas um monte de cor aleatoria, é meu personagem
fofocando para o Discord oque esta acontecendo em Azeroth ![]()
