/ blog

#Games15 MIN DE LEITURA

A odisseia de migrar código sem atualização por mais de 16 anos

Há um tempo, em outro artigo sobre o WODBO, mencionei que estava migrando o servidor de 8.60 para Canary 1525, e que entre uma versão e outra havia algo como dezesseis anos de diferença. Naquele momento escrevi isso bem por cima, principalmente porque o artigo tratava de outra coisa, mas também porque eu ainda não entendia direito o que aqueles dezesseis anos significavam.

Pensei, talvez ingenuamente, que o problema ia estar principalmente no código. O WODBO continuava rodando sobre um stack velho, algumas coisas tinham ficado limitadas pelo cliente 8.60 e fazia tempo que eu queria modernizá-lo, sobretudo porque o ecossistema de OpenTibia continuou avançando enquanto nós seguíamos ali, usando praticamente a mesma base de uma quantidade considerável de anos atrás. O servidor funcionava, os jogadores podiam entrar, estavam seus personagens, casas, guilds, rankings, NPCs, spells e todo o resto, mas cada vez que eu queria adicionar algo novo começava a aparecer aquela sensação de estar tentando continuar construindo em cima de algo que já tinha cumprido bastante bem o seu trabalho.

Além disso, o feedback ia um pouco para esse lado. Ninguém me dizia especificamente “vamos migrar para Canary 1525”, claro, mas apareciam pedidos de coisas que em servidores modernos já eram bastante normais, e que no 8.60 começavam a precisar de criatividade demais para justificar continuar ali. Então tomei a decisão de migrar o WODBO para Canary junto com o OTClient Redemption, habilitar coisas novas do cliente moderno, manter o que já funcionava, e na minha cabeça tudo soava difícil, mas razoável.

Erro.

A primeira coisa que decidi foi não tocar em produção. O servidor 860 continuou rodando na Hostinger com gente jogando, enquanto o Canary vivia no CT160 dentro do meu homelab. Isso acabou sendo bastante útil porque, enquanto eu quebrava o servidor novo de formas que não sabia que eram possíveis, sempre tinha o velho ao lado para comparar. Se uma spell batia estranho, eu testava no 860. Se um efeito aparecia diferente, eu testava no 860. Se um monstro fazia algo que eu não entendia, de novo, 860. Em algum momento ele deixou de ser simplesmente o servidor velho e se transformou numa espécie de oráculo ao qual eu voltava cada vez que o Canary me fazia duvidar da realidade.

Antes do corte público até deixamos o cliente novo atrás de uma allowlist de e-mails. O marketplace, a API e o mundo principal continuavam trabalhando sobre o stack anterior, enquanto eu podia fazer testes em casa sem que cada crash-loop significasse explicar no Discord por que ninguém conseguia entrar. Isso me deu uma certa margem para errar com calma, o que num projeto desse estilo acaba sendo bem mais importante do que parece.

Minha primeira ideia era começar pelo mapa, porque, bem, é o mapa. Pensei que ia ser um daqueles problemas em que você abre algo velho, converte, corrige alguns IDs e segue. O que eu não estava levando muito em conta é que o mapa do WODBO não era só um .otbm. Ao redor tinha spawn.xml, house.xml, DAT, SPR, items.otb, IDs que significavam uma coisa para o servidor e outra para o cliente, além de decisões tomadas há muitos anos que ninguém tinha motivo para documentar porque simplesmente funcionavam.

O Canary representa várias dessas coisas de outra forma. Appearances em protobuf, outro catálogo de sprites, items.xml, monsters, NPCs e zones separados, e sobretudo outra relação entre o ID que existe dentro do mapa e o que o cliente acaba vendo. Foi aí que começamos a montar uma tabela de remap e congelamos uma regra que, escrita, parece trivial: o ID usado no OTBM, o de items.xml e o appearance ID tinham que representar exatamente a mesma coisa.

O conversor acabou processando mais de três milhões e oitocentos mil IDs.

Ainda me parece um número ridículo para um jogo de Dragon Ball que comecei a reviver por nostalgia.

Depois disso abri o mapa convertido no editor do Canary e, obviamente, não estava tudo bem. Havia spawns duplicados, NPCs que não carregavam, formatos com os quais o editor não queria saber de nada e milhares de invalid item id for border. Passei um bom tempo tentando entender como eu podia ter tantos IDs inválidos em zonas onde eu nem via esses items, até descobrir que uma parte vinha direto da paleta stock do editor. Ou seja, eu estava procurando lixo no meu mapa que na verdade nem estava no meu mapa.

Claro, durante essa parte também apareceu a Soul Wars Coin. No 860 ela reutilizava o sprite da gold coin e isso nunca tinha representado muito problema, até que no Canary a relação entre o item e sua appearance mudou o suficiente para que alguns monstros pudessem começar a dropar Soul Wars Coins como loot normal. Uma decisão que durante anos foi basicamente “vamos usar este sprite e pronto” agora estava tocando a economia do jogo. Acabamos dando a ela o ID 12573 e deixando o 6526 para a gold coin.

E esse tipo de coisa começou a se repetir. Não necessariamente grandes problemas de arquitetura, mas decisões minúsculas que funcionavam perfeitamente dentro do contexto do 8.60, e que dezesseis anos depois significavam outra coisa.

Quando o mapa começou a ficar razoavelmente parecido com o original, passei para os scripts. Foi aí que tive outra das minhas ideias brilhantes: portar uma boa quantidade de coisas juntas e resolver a compatibilidade de uma vez só. O Canary respondeu com crash-loops, que tecnicamente também é uma forma de feedback.

Instalar toda a camada de compatibilidade durante o boot podia derrubar o servidor. Tentar carregar umas duzentas e quarenta e quatro spells também. Alguns monsters Lua estavam mal gerados, várias APIs já não existiam da mesma forma e num desses dias acabei debugando arquivos que nem deveriam estar ali porque um docker cp tinha mergeado conteúdo velho com o novo. Não lembro exatamente quantas horas perdi com isso e acho que prefiro manter assim.

Então apareceu a ponte legacy. Wrappers que faziam dofile de scripts velhos, libs separadas, partes de compatibilidade que carregavam quando realmente eram necessárias e não na inicialização do servidor. A ideia já não era migrar tudo de uma vez, mas conseguir que o Canary pudesse conviver com coisas do 860 enquanto íamos substituindo.

Isso fez o servidor ligar de forma bem mais consistente, embora “ligar” e “funcionar” tenham se tornado rapidamente dois conceitos completamente distintos.

Um dos primeiros exemplos foram os efeitos das spells. Alguns beams tinham as partes deslocadas; um Kamehameha podia mostrar uma ponta no meio do feixe ou usar sprites que claramente não correspondiam. Eu estava bastante convencido de que o problema vinha dos assets, então acabei comparando sprites entre os dois clientes, revisando atlas, olhando cada parte separadamente e perdendo uma boa quantidade de tempo antes de encontrar algo muito mais bobo.

O TFS 0.4 numerava os magic effects a partir do zero.

O Canary, a partir de um.

Um offset.

Os sprites estavam perfeitamente bem. O script pedia um efeito e o cliente moderno mostrava o seguinte. Depois de entender isso fizemos a correção, montamos um probe in-game e acabei tendo até um renderer offline do atlas de spells, principalmente porque depois de comparar Kamehamehas suficientes na mão a gente começa a reconsiderar como está usando o tempo livre.

Enquanto revisávamos os efeitos apareceu algo bem pior: o dano também não era equivalente.

Um Boo Kamehameha que no 860 podia bater em torno de 108.000 acabava chegando perto de 700.000 no Canary. A fórmula tinha o mesmo nome, LEVELMAGIC, então durante um tempo assumi que teria um comportamento parecido, até olhar o que cada motor de fato fazia. No velho o fator era level dividido por cinco mais magic level; o Canary usava level vezes dois mais magic level vezes três. O nome era igual. Mais nada.

Tentamos primeiro por callbacks, mas o Canary não encontrava corretamente o global, deixava o erro nos logs e depois acabava usando a fórmula dele mesmo assim. A saída foi calcular o range durante o cast e aplicar o dano diretamente, evitando depender de uma interpretação diferente do motor.

Acho que por aí comecei a entender melhor qual era realmente o problema da migração. Eu não precisava que o Canary pudesse executar uma spell chamada Kamehameha. Precisava que alguém que jogava WODBO desde antes entrasse, usasse o personagem e sentisse que ainda era o mesmo Kamehameha. Eu podia mudar praticamente tudo que havia por baixo, mas se o resultado era seis vezes mais dano, tecnicamente eu tinha migrado o código e ao mesmo tempo quebrado o jogo.

A camada de compatibilidade também não ajudava muito a minha saúde mental. Alguns globals velhos acabavam convertidos em ponteiros crus dentro do Canary, então havia scripts que passavam um integer onde o motor novo esperava userdata e o processo simplesmente se suicidava. Até o sweep que usávamos para ir testando spells podia morrer em uma, reiniciar, percorrer tudo de novo e morrer exatamente no mesmo lugar. Tivemos que fazer com que guardasse o progresso, colocasse o culpado numa blocklist e continuasse com o próximo arquivo. Me causava bastante graça que a ferramenta desenhada para encontrar crashes precisasse de resiliência contra os próprios crashes.

No final de julho chegaram finalmente as primeiras spells completamente nativas. Aura, Renzoku Energy Dan e Kamehameha já não dependiam de dofile nem de wrappers do 860. Montamos libs próprias para dano, caps e exhaustion, e usamos o servidor velho como fonte para gerar perfis sem copiar na mão vinte e nove configurações e umas oitocentas e trinta e cinco vocações. Em algum momento a suíte de paridade ficou verde e os erros Lua dentro do LXC passaram de 105 para zero.

Acho que naquele dia realmente pensei que já tínhamos passado da parte feia.

Enquanto tirávamos wrappers descobrimos que alguns IDs de spells dependiam da ordem em que eram registrados, então retirar um podia renumerar tudo o que vinha depois e quebrar hotkeys que os jogadores já tinham salvas. Depois apareceram monsters registrados duas vezes; o Canary avisava already registered, conservava a primeira definição e descartava a segunda, sendo casualmente a segunda a que tinha as nossas mudanças. O installer teve que começar a apartar a árvore velha para evitar que as duas versões convivessem.

Cada vez ficava mais claro que o código legacy não ia desaparecer numa sexta à noite depois de um rewrite heroico. O Canary podia ser o mundo novo enquanto ainda existia uma quantidade importante de código velho fazendo de ponte, e sinceramente, depois de algumas semanas, isso já não me parecia tão grave. Preferia saber exatamente qual parte ainda era legacy a reescrever mil scripts juntos e passar os meses seguintes tentando descobrir qual deles tinha mudado o comportamento.

No começo de agosto fizemos o cutover.

Migramos personagens, mudamos a API e o portal para trabalhar com a nova MariaDB, adaptamos rankings, guilds e online ao schema do Canary, publicamos o cliente e paramos de aceitar conexões do 860 no mundo principal. Tínhamos passado semanas testando o servidor novo no homelab e havia um certo grupo de usuários que já vinha usando, então naquele ponto parecia razoável abrir.

E aí entraram os jogadores de verdade.

O ETL tinha migrado quase todos os depots, mas os lockers ficaram aninhados sob caixas com a estrutura que o TFS usava. Os items estavam lá, a informação não tinha se perdido, mas quando alguém abria o depot podia se deparar com um locker vazio porque as coisas reais continuavam guardadas um nível abaixo. Tivemos que achatá-los e, quando isso ficou resolvido, descobrimos que a UI de depot do Canary também não se dava muito bem com o nosso OTC. Acabamos espelhando o conteúdo para uma backpack marcada que o cliente realmente entendia.

Uma mochila resolveu parte de um problema que vinha depois de semanas mexendo em ETLs, bases, protocolos e milhões de IDs. Não tenho uma conclusão inteligente sobre isso, simplesmente achei engraçado.

Os NPCs também começaram a mostrar problemas que no lab não tínhamos encontrado. Alguns cumprimentavam perfeitamente e depois não vendiam nada porque havia stubs de trader sem shop pisando a configuração correta. Outros tinham diferenças entre o nome usado no spawn e o script. O Emporio entregava coisas erradas por como o Canary interpretava os chests. Em combate apareceram spells de área que usavam setArea em runtime, algo que o motor novo só permite durante o load time, então uma boa quantidade de AOEs deixou de se comportar como esperávamos.

Em outro momento, os efeitos de alguns beams começaram a vazar para o melee. Você podia dar um golpe normal e ver parte de um Kamehameha, o que certamente teria sido uma feature excelente se o objetivo fosse fazer exatamente o contrário de manter paridade com o 860. Foi preciso tirar os effects de Combat, aplicá-los à parte, limpar os parâmetros depois e purgar caches de armas.

E enquanto consertávamos isso aparecia outra coisa. O radar de Dragon Ball não pintava bem, o online do portal não batia, o OTServerList respondia estranho, o feed de mortes precisava de outro schema, em algum ponto os personagens andavam rápido demais, depois lento demais, Yellow Ki não se sentia igual, a experiência não respeitava corretamente os stages do servidor anterior e chegamos a encontrar um clawback que podia resetar níveis depois de um reborn.

A diferença era que agora tudo isso acontecia com gente dentro.

Até então eu podia passar horas testando uma spell com determinado personagem e me convencer de que estava bem. Depois entrava alguém com outra vocação, outra transformação, um inventário que carregava há anos, usava uma spell que mal tínhamos olhado, ia até um NPC ao qual ninguém durante os testes tinha prestado atenção e encontrava um bug em cinco minutos.

Até apareceu um senzu perto de uma zona VIP que podia acabar provocando um crash.

Não lembro de ter adicionado “testar senzu perto do VIP” a nenhum test plan, sinceramente.

Aí o feedback começou a pesar muito mais do que antes. Havia reports bem explicados, outros que eram praticamente “isso não funciona”, gente paciente e obviamente gente bem menos paciente, mas todos acabavam mostrando partes do servidor que nós não tínhamos percorrido. O lab tinha sido excelente para quebrar coisas sem consequências, levantar duas versões, comparar, automatizar testes e revisar logs. O que ele não podia fazer era reproduzir anos de hábitos acumulados de jogadores reais.

Em paralelo, boa parte de tudo isso eu fiz com o Cursor. Menciono porque esconder isso faria a história ficar bastante incompleta. No ciclo que revisei enquanto preparava este artigo, entre o final de julho e o final de agosto, o custo de modelos tinha passado de 700 dólares. O Grok levou uma parte importante, o Opus apareceu em vários problemas e o Auto acabou fazendo bastante do dia a dia. Agent foi muito mais usado do que o chat e cheguei ao limite do ciclo.

Eu já tinha escrito antes sobre Jevons e como, quando testar algo custa menos, a gente simplesmente acaba fazendo mais testes. Esta migração foi basicamente a versão prática disso. Havia centenas de scripts para revisar, conversões que eu podia gerar, rodar e descartar, comparações que de outra forma teria feito à mão e tooling que provavelmente nunca teria escrito se cada tentativa me levasse uma tarde inteira.

A IA acelerou uma quantidade enorme de coisas.

Também me permitiu errar mais rápido, o que nem sempre é exatamente a mesma coisa.

Eu podia gerar um script para migrar lockers, rodá-lo e revisar o resultado. Podia procurar padrões incompatíveis em centenas de spells ou gerar uma base para levar alguma delas para código nativo. O que eu não podia fazer era decidir que o resultado estava correto simplesmente porque o processo tinha terminado com exit code zero. Se no dia seguinte alguém entrasse e não encontrasse os items dentro do depot, o responsável continuava sendo eu, independentemente de quem tivesse escrito o script.

Enquanto tudo isso acontecia também fui usando muito trabalho que não era meu: Canary, OTClient Redemption, Remere e em geral boa parte do OpenTibiaBR existem porque há gente que leva anos mantendo ferramentas públicas para este ecossistema. Uma parte da minha intenção ao documentar esta migração também vem daí. Ainda preciso ser bem mais concreto devolvendo coisas upstream, mas depois de passar tantas horas me apoiando em trabalho comunitário, me limitar a usar e seguir em frente também não me fecha.

Hoje o mundo principal já roda em Canary 1525.

O 860 ficou de fora, os personagens estão migrados, o portal e a API trabalham contra o schema novo e há jogadores usando Canary todos os dias. O datapack, no entanto, ainda conserva partes dessa ponte legacy. Há wrappers que faltam retirar, coisas que ainda não estão portadas de forma nativa e certamente bugs que simplesmente não conhecemos porque ninguém executou ainda a combinação suficientemente estranha de ações necessária para encontrá-los.

E acho que está bem dizer assim.

Quando comecei pensava em dezesseis anos como uma distância entre duas versões, como se o 8.60 estivesse de um lado e o Canary 1525 do outro, com uma quantidade considerável de commits no meio. Depois de fazer a migração me parece que os dezesseis anos estavam escondidos em outro lugar: num ID reutilizado porque naquele momento não importava, numa fórmula que conservou o nome mas mudou completamente por dentro, na forma de guardar um depot, num NPC, numa hotkey que alguém tinha configurado há anos, num script que ninguém tocava porque simplesmente continuava funcionando.

Era fácil olhar o código velho e pensar que tinha que modernizá-lo.

O difícil acabou sendo descobrir o que não havia que mudar.

O WODBO já está no Canary e ainda resta trabalho. Bastante, provavelmente. Mas durante semanas tive os dois mundos rodando um ao lado do outro, usando o 860 para perguntar constantemente como as coisas deveriam funcionar.

Agora aquele mundo velho já não está esperando do outro lado para me responder.

Então acho que, pelo menos em algum ponto, a migração terminou.

Se você chegou até aqui e ficou curioso para ver como ficou tudo isso do outro lado, o WODBO Wars já está rodando sobre Canary 1525 e o cliente novo está disponível para qualquer um que queira entrar.

Não prometo que não reste algum bug escondido esperando a combinação certa de coisas para aparecer, provavelmente haja, mas também há um mundo bem mais moderno, com anos de conteúdo atrás e uma quantidade absurda de trabalho recente para manter o que fazia o 860 especial sem ficarmos presos ali.

Você pode criar sua conta, baixar o cliente e jogar em:

https://wodbowars.com/

E se encontrar algo quebrado, bem… depois deste artigo você já sabe mais ou menos o que vem depois.

← Artigos