https://pbs.twimg.com/media/EiAqhIyWAAAgf-A?format=jpg&name=small

Eu gastei horas fazendo algo praticamente inútil.

Eu estava pensando em implementar uma VM que executa jogos feitos com o GameMaker: Studio 1. (Sabia que o ".exe" de jogos feitos pelo GM:S é uma VM? Igual como Java funciona, ele executa bytecode. E é por isso que eu consegui fazer o Droidtale, um port não oficial do Undertale para o Android)

O objetivo era conseguir rodar Undertale (e outros jogos feitos pelo GM:S) em uma VM feita em Kotlin, sendo possível portar a VM para outras plataformas (e assim portando os jogos também).

Claro, é bem mais fácil apenas recompilar o jogo para a plataforma desejada, mas existem os seguintes problemas:

Caso você tenha o GameMaker: Studio com os exports que você deseja...

  • Você precisa compilar um jogo vazio para a plataforma desejada e alterar os dados do jogo pelos dados do jogo que você quer portar. Não é possível fazer isto com export para HTML5.
  • A sua versão do GameMaker: Studio precisa exportar a mesma versão de bytecode que o jogo que você deseja portar.

Caso você tenha (ou não tenha) o GameMaker: Studio sem ter os exports que você deseja...

  • Você precisa encontrar algum jogo feito no GameMaker: Studio para a plataforma que você deseja portar o jogo, e o jogo precisa ter sido exportado com a mesma versão de bytecode que o jogo que você deseja portar.

Por isso decidi fazer uma VM, porque não deve ser tão difícil... né?

Não, não é fácil. E sim MUITO difícil, já que você precisa implementar uma VM que interpreta o código gerado pelo GM:S para executar o código do jogo.

Eu usei as seguintes documentações para fazer a minha VM:

Enquanto estava fazendo o meu projeto, descobri o Luna, um projeto parecido com o meu, que tenta implementar uma VM para jogos do GameMaker: Studio, mas o Luna foi feito para jogos que usam o bytecode das novas versões do GameMaker: Studio 2, enquanto eu estou tentando implementar o bytecode utilizado no GameMaker: Studio 1 (já que o objetivo era emular o Undertale). O Luna é bastante interessante já que o dev consegiu rodar um jogo simples feito no GM:S2! Vale a pena conferir o blog do criador para ver o progresso.

Mas mesmo assim resolvi continuar o meu projeto, afinal, imagina o Undertale rodando no seu navegador? Então fui para a ação e programação, eu reutilizei partes do meu antigo projeto FriskEuphoria, que foi originalmente criado para fazer unpack e pack do data.win do Undertale e de outros jogos feitos no GameMaker: Studio.

Criei um projeto vazio no GameMaker: Studio 1 com apenas uma sala vazia e um objeto que no "Create" possuia o seguinte código:

show_message("owo whats this???");

show_message("uwu whats this???");

show_message(";w; whats this???");

Como reaproveitei o código do FriskEuphoria, a parte de extrair as strings já tinha sido feita, yay!

Todas as strings do data.win

Então parti para a parte de fazer unpack da parte do código... que foi difícil, já que era a primeira vez que eu implementei um programa que faz parse e executa bytecode.

foto do datawin aqui

E depois de ler MUITA a documentação e muito trial and error, você precisa fazer o seguinte para ler o chunk de "CODE"

  1. Você lê o nome do chunk (no caso, CODE)
  2. Você lê o tamanho do chunk
  3. Você lê a quantidade de addresses (address = posição no data.win aonde está o conteúdo)
  4. Você lê os addresses
  5. Você pula para cada posição de cada address
  6. Você lê a posição do nome da função gml_Object_owo_Create_0
  7. Você lê o tamanho do código
  8. Você lê a posição do código RELATIVO a posição atual
  9. Vai até a posição do código e leia todo o código.

E quando ler o código, cada "instrução" são 32 bits, por exemplo:

imagem hxd

A VM do GameMaker: Studio é stack-based (parecido como a VM do Java funciona).

Para você parsear qual op code você está lendo, você precisa pular os 3 primeiros bytes e ler, no exemplo acima, é um op code 0xC0, que é um push de uma constante.

Depois você volta para o começo da leitura das instruções para ler os 3 primeiros bytes que foram pulados, já que eles possuem informações importantes sobre o op code.

No caso do 0xC0, você precisa pular os dois primeiros bytes (que servem para nada), você lê o byte que indica o tipo do push (neste caso é 0x06, que significa que estamos fazendo push de uma "String"), e aí você pula mais um byte (que seria o op code) e leia o próximo byte, que é o ID de referência da String, que, neste caso, é 0x03...

E olhando no dump de strings, qual é a string que fica na posição 3? owo whats this???! Então estamos pelo caminho certo!!

E depois precisei parsear as outras instruções, eu acabei implementando as seguintes instruções e tipos:

instruções e tipos

Ah, e também tive que fazer unpack do chunk FUNC, já que quando você usa uma função do GameMaker: Studio (exemplo: show_message) a função é colocada na seção FUNC e ao usar a instrução de call, ele passa o ID de referência da função.

E, finalmente, fiz a VM executar o código (coloquei para o show_message mostrar a mensagem no console)... e deu certo!

resultado

Claro, se eu implementasse todos os op codes que o GM:S usa e implementar todas as funções do GM:S, seria possível executar qualquer jogo feito no GameMaker: Studio na VM.

E por isso isso que eu fiz é inútil/idiota: É legal como um conceito, mas irá demorar MUITO para implementar uma VM completa que consiga interpretar todos os bytecodes e implementar todas as funções que o GameMaker: Studio possui. Claro, é possível eu apenas implementar os bytecodes e funções que o Undertale utiliza, só para servir como proof of concept... mas quem sabe eu decida fazer isso outro dia.