Estudando a Máquina Virtual do jogo Another World


#1

Estou escrevendo um conjunto de ferramentas para análise da máquina virtual do jogo Another World, de Eric Chahi (1991). Já conegui gerar o disassembly do bytecode de todas as fases e agora consegui extrair todas as cenas poligonais exportando-as para arquivos SVG individuais. Ainda falta decodificar as cores. Portanto, por enquanto estou ainda apenas exportando wireframes. Estou pensando ainda como fazer pra inferir as cores corretas também.

Esse aqui é um exemplo do que o meu script está gerando no momento:

E essa é a cena original do jogo, com as cores certas:

O código dessa ferramenta de análise do bytecode está sendo desenvolvido aqui:

Como o nome e a descrição do repositório indicam, eu pretendo também escrever um assembler, uma ferramenta para gerar as estruturas de dados de polígonos a partir de arquivos SVG, e também outras ferramentas que auxiliem no desenvolvimento de jogos para essa VM. Comecei escrevendo um disassembler por que assim terei uma forma de testar o assembler com um programa bem grande e complexo.

Obviamente eu não posso distribuir a listagem completa pois isso constituiria uma infração de direitos autorais. Mas para fins educacionais e de ilustração eu posso sem problemas mostrar alguns pequenos trechos das listagens quando eu quiser discutir alguma característica do jogo ou técnicas usadas em seu programa.

Seguem abaixo mais alguns exemplos de imagens vetoriais extraídas pelo meu script a partir dos arquivos originais do jogo, seguidas por screenshots de referência:


#2

Hoje eu investiguei a ROM de SNES do jogo Out of this World para tentar extrair dela o bytecode e outros resources (como paletas de cores, estruturas de dados de polígonos, etc) deste release do Another World.

Eu sei que os resources são diferentes, por que consigo ver algumas diferenças sutis no gameplay. E alguns bugfixes. E também a censura que a Nintendo aplicou ao game (dentre várias modificações, uma das coisas que a Nintendo pediu foi que o sangue fosse pintado de verde para ser interpretado como “gosma de alien” e, supostamente, não afetar a classificação etária do jogo).

Mas eu não consegui ainda achar os dados que estou procurando. Suponho que eles estejam comprimidos. Mas não tenho idéia de qual algorítmo de compressão foi usado. Para tentar descobrir, eu usei o debugger do MAME para inspecionar a memória RAM em runtime rodando o cartucho de SNES e tentando achar na RAM similaridades com o bytecode de MSDOS que eu já conheço. Mas por enquanto isso ainda não adiantou, pois não consegui achar.

Aí resolvi usar watchpoints para interromper a execução do jogo e cair de volta no debugger assim que fosse lido um endereço especifico da ROM, contento uma string de texto “PRESS ‘B’ TO CONTINUE”. Fiz isso por que sei que o desenho de strings de texto na tela é efeito colateral da execução da instrução “drawString”. Consegui achar esse ponto da ROM. E minha esperança era que eu poderia a partir daí ver de onde foi lido o opcode que levou à execução da instrução drawString. E então eu poderia ver onde na memória ficam armazenados trechos de bytecode descompressos. E depois eu poderia investigar quem foi que escreveu o bytecode alí e, com isso, ter algum insight sobre como está armazenado o bytecode na ROM.

Mas… apesar de ter achado a execução da instrução drawString, não consegui fazer esses “passos pra trás” justamente pela falta de uma feature de back-track log no debugger do MAME. Agora fico pensando se vale a pena eu implementar essa feature no MAME pra solucionar o meu problema, ou se é melhor eu forçar um pouco mais a cabeça pra achar algum outro jeito de achar os dados de bytecode.


#3

Neste domingo eu escrevi um código de exemplo para rodar na VM do Another World. É uma animação bem simples de uma imagem zigue-zagueando pela tela (rebatendo nos cantos da tela). Algo parecido com um jogo de PONG, que aliás seria uma ótima opção para um segundo exemplo de código um pouco mais complexo. No gif animado abaixo eu mostro o programinha rodando na VM dentro do MAME.

A colisão com a borda é detectada com o ponto central do objeto como referência. O vetor é desenhado ao redor daquele ponto. Por isso que o desenho sai um pouco da tela antes de rebater. O código de exemplo (escrito em assembly da VM) está disponível no github aqui: https://github.com/felipesanches/AnotherWorld_VMTools/blob/master/example/bounce.asm

BALL_IMAGE    EQU 0x031E ; This is the address of one of the code wheel symbols.

; These are the VM vars we'll use:
BALL_X        EQU 0x00
BALL_Y        EQU 0x01
BALL_ZOOM     EQU 0x02
SPEED         EQU 0x03
PAUSE_SLICES  EQU 0xFF

; This programs uses a single VM thread and a single videopage.
init:
      movConst [PAUSE_SLICES], 2 ; 2*20ms = 40ms per frame = 25 frames / sec
      movConst [SPEED], 1
      movConst [BALL_X], 160
      movConst [BALL_Y], 100
      movConst [BALL_ZOOM], 0x40

; The VX and VY vars are strictly positive values, so we treat
; zero as -SPEED,
; SPEED as zero
; 2*SPEED as +SPEED
      mov [BALL_VX], [SPEED]
      add [BALL_VX], [SPEED]
      mov [BALL_VY], [SPEED]
      add [BALL_VY], [SPEED]
      setPalette 0x03 ; This is one the color palette used for that code wheel symbol.
      selectVideoPage 0x00

mainloop:
      ; fill the whole page with a background color:
      fillVideoPage 0x00, color:0x07

      ; draw the symbol:
      video off=BALL_IMAGE x=[BALL_X] y=[BALL_Y] zoom:[BALL_ZOOM]

      ; and update the screen:
      blitFramebuffer 0x00

      ; update the screen coordinates based on the current x and y velocities:
      sub [BALL_X], [SPEED]
      addConst [BALL_X], [BALL_VX]
      sub [BALL_Y], [SPEED]
      addConst [BALL_Y], [BALL_VY]

      ; mirror the velocities if the symbol collides with the borders:
      jl [BALL_X], 320, _1
      movConst [BALL_VX], 0
_1:
      jg [BALL_X], 0, _2
      movConst [BALL_VX], [SPEED]
      add [BALL_VX], [SPEED]
_2:
      jl [BALL_Y], 200, _3
      movConst [BALL_VY], 0
_3:
      jg [BALL_Y], 0, _4
      movConst [BALL_VY], [SPEED]
      add [BALL_VY], [SPEED]
_4:
      jmp mainloop

Como eu ainda não tenho um assembler, eu tive que gerar o bytecode montando o código manualmente. Fui olhando a tabela de opcodes e transcrevendo a representação binária. Tem um outro arquivo no mesmo repo chamado bounce.asm.manual que tem a transcrição dos opcodes e operandos: https://github.com/felipesanches/AnotherWorld_VMTools/blob/master/example/bounce.asm.manual

É meio chato de fazer isso manualmente, mas como o programa era pequeno, eu concluí que seria mais rápido do que escrever um assembler. Para programas maiores eu com certeza precisarei implementar o assembler. Como a linguagem é relativamente simples, estou ainda na dúvida se faço em python mesmo, ou se emprego lex e yacc (flex e bison)…

Depois de transcritos todos os valores numéricos das instruções (inclusive com o cálculo manual dos endereços de memória), usei um editor hexadecimal (eu uso o hexcurse) pra inserir os bytes em um arquivo binário que é carregado pela VM no MAME.

O desenho foi reutilizado do banco de polígonos do jogo. Tive só que descobrir qual o endereço de memória onde a estrutura de dados desse símbolo fica armazenada. Descobri esse endereço vendo o diretório de SVGs gerados pelo extrator e depois consultando o header gerado automaticamente pelo disassembler.


#4

Curtindo esse tópico! O engine desse jogo é para mim a implementação mais elegante de código que eu já li a respeito em toda a minha vida. Tudo escrito em bytecode e executado em uma VM, o que torna fácil e elegante o “port” para outras plataformas (sendo apenas necessário reescrever a VM, sem precisar mexer no código do jogo). Tudo isso se já não bastasse a roteirização e rotoscopia do jogo, todos feitos por ele. O Eric Chahi é um gênio.


#5

Eu ando pensando muito sobre esse projeto…

(A ironia foi eu ter usado a tag #weekendproject em algo que nitidamente vai levar alguns anos pra talvez virar realidade…)