O reverso do Neuromancer. Parte 3: Renderização finalizada, faça o jogo


Olá, esta é a terceira parte de uma série de minhas publicações dedicadas ao desenvolvimento reverso do Neuromancer - uma incorporação de videogame do romance de mesmo nome de William Gibson.


O reverso do Neuromancer. Parte 1: Sprites
O reverso do Neuromancer. Parte 2: Renderizar fonte

Esta parte pode parecer um pouco caótica. O fato é que a maior parte do que é descrito aqui estava pronta no momento da escrita do anterior . Como já se passaram dois meses desde então e, infelizmente, não tenho o hábito de manter anotações de trabalho, simplesmente esqueci alguns detalhes. Mas como é, vamos lá.




[Depois que aprendi a imprimir linhas, seria lógico continuar a reverter a construção das caixas de diálogo. Mas, por algum motivo que me escapou, em vez disso, entrei completamente na análise do sistema de renderização.] Mais uma vez, percorrendo a main , consegui localizar a chamada, que primeiro exibe algo na tela: seg000:0159: call sub_1D0B2 . "Qualquer coisa", neste caso, é o cursor e a imagem de fundo do menu principal:




Vale ressaltar que a função sub_1D0B2 [doravante - render ] não possui argumentos, no entanto, sua primeira chamada é precedida por duas seções de código quase idênticas:


 loc_100E5: loc_10123: mov ax, 2 mov ax, 2 mov dx, seg seg009 mov dx, seg seg010 push dx push dx push ax push ax mov ax, 506Ah mov ax, 5076h ; "cursors.imh", "title.imh" push ax push ax call load_imh call load_imh ; load_imh(res, offt, seg) add sp, 6 add sp, 6 sub ax, ax sub ax, 0Ah push ax push ax call sub_123F8 call sub_123F8 ; sub_123F8(0), sub_123F8(10) add sp, 2 add sp, 2 cmp word_5AA92, 0 mov ax, 1 jz short loc_10123 push ax sub ax, ax mov ax, 2 push ax mov dx, seg seg010 mov ax, 2 push dx mov dx, seg seg009 push ax push dx sub ax, ax push ax push ax mov ax, 64h push ax push ax mov ax 0Ah mov ax, 0A0h push ax push ax call sub_1CF5B ; sub_1CF5B(10, 0, 0, 2, seg010, 1) sub ax, ax add sp, 0Ch push ax call render call sub_1CF5B ; sub_1CF5B(0, 160, 100, 2, seg009, 0) add sp, 0Ch 

Antes de chamar render , os cursores ( cursors.imh ) e o plano de fundo ( title.imh ) são descompactados na memória ( load_imh é o sub_126CB renomeado da primeira parte ), no nono e no décimo segmentos, respectivamente. Um estudo superficial da função sub_123F8 não me trouxe nenhuma informação nova, mas, apenas analisando os argumentos de sub_1CF5B , tirei as seguintes conclusões:


  • os argumentos 4 e 5, juntos, representam o endereço do sprite descomprimido ( segment:offset );
  • os argumentos 2 e 3 provavelmente são coordenadas, pois esses números se correlacionam com a imagem exibida após a chamada render ;
  • o último argumento pode ser a bandeira da opacidade do plano de fundo do sprite, porque os sprites descompactados têm um plano de fundo preto e vemos o cursor na tela sem ele.

Com o primeiro argumento [e ao mesmo tempo com a renderização em geral], tudo ficou claro após o rastreamento de sub_1CF5B . O fato é que, no segmento de dados, começando com o endereço 0x3BD4 , está localizada uma matriz de 11 estruturas do seguinte tipo:


 typedef struct sprite_layer_t { uint8_t flags; uint8_t update; uint16_t left; uint16_t top; uint16_t dleft; uint16_t dtop; imh_hdr_t sprite_hdr; uint16_t sprite_segment; uint16_t sprite_pixels; imh_hdr_t _sprite_hdr; uint16_t _sprite_segment; uint16_t _sprite_pixels; } sprite_layer_t; 

Eu chamo esse conceito de cadeia de sprites. De fato, a função sub_1CF5B (a seguir add_sprite_to_chain ) adiciona o sprite selecionado à cadeia. Em uma máquina de 16 bits, ela teria aproximadamente a seguinte assinatura:


 sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int index, uint16_t left, uint16_t top, uint16_t offset, uint16_t segment, uint8_t opaque); 

Funciona assim:


  • o primeiro argumento é o índice na matriz g_sprite_chain ;
  • os argumentos left e top são gravados nos g_sprite_chain[index].left g_sprite_chain[index].top e g_sprite_chain[index].top respectivamente;
  • o cabeçalho do sprite (os primeiros 8 bytes localizados no segment:offset ) é copiado para o campo g_sprite_chain[index].sprite_hdr , digite imh_hdr_t (renomeado rle_hdr_t da primeira parte):

 typedef struct imh_hdr_t { uint32_t unknown; uint16_t width; uint16_t height; } imh_hdr_t; 

  • o campo g_sprite_chain[index].sprite_segment registra o valor do segment ;
  • no campo g_sprite_chain[index].sprite_pixels , um valor igual ao offset + 8 gravado, então sprite_segment:sprite_pixels é o endereço do bitmap do sprite adicionado;
  • os sprite_hdr , sprite_segment e sprite_pixels duplicados em _sprite_hdr , _sprite_segment e _sprite_pixels respectivamente [por quê? - Não faço ideia, e este não é o único caso dessa duplicação de campos] ;
  • no campo g_sprite_chain[index].flags valor igual a 1 + (opaque << 4) gravado 1 + (opaque << 4) . Esse registro significa que o primeiro bit do valor dos flags indica a "atividade" da camada "atual" e o quinto bit indica a opacidade do plano de fundo. [Minhas dúvidas sobre a bandeira de transparência foram dissipadas depois que eu testei experimentalmente seu efeito na imagem exibida. Alterando o valor do quinto bit em tempo de execução, podemos observar esses artefatos]:


Como já mencionei, a função render não tem argumentos, mas não precisa - ela trabalha diretamente com o array g_sprite_chain , transferindo alternadamente as "camadas" para a memória VGA , do último ( g_sprite_chain[10] - background) ao primeiro ( g_sprite_chain[0] - primeiro plano). A estrutura sprite_layer_t tem todo o necessário para isso e ainda mais. Estou falando dos campos não revisados update , dleft e dtop .


De fato, a função de render redesenha NÃO TODOS os sprites em cada quadro. Um valor diferente de zero do campo g_sprite_chain.update indica que o sprite atual precisa ser redesenhado. Suponha que movamos o cursor ( g_sprite_chain[0] ), algo assim acontecerá no manipulador de movimento do mouse:


 void mouse_move_handler(...) { ... g_sprite_chain[0].update = 1; g_sprite_chain[0].dleft = mouse_x - g_sprite_chain[0].left; g_sprite_chain[0].dtop = mouse_y - g_sprite_chain[0].top; } 

Quando o controle passa para a função render , a última, tendo atingido a camada g_sprite_chain[0] , vê que precisa ser atualizada. Então:


  • a interseção da área ocupada pelo sprite do cursor antes da atualização com todas as camadas anteriores será calculada e desenhada;
  • As coordenadas do sprite serão atualizadas:

 g_sprite_chain[0].update = 0; g_sprite_chain[0].left += g_sprite_chain[0].dleft g_sprite_chain[0].dleft = 0; g_sprite_chain[0].top += g_sprite_chain[0].dtop g_sprite_chain[0].dtop = 0; 

  • o sprite será desenhado nas coordenadas atualizadas.

Isso minimiza o número de operações executadas pela função de render .




Não foi difícil implementar essa lógica, embora eu a tenha simplificado bastante. Considerando o poder computacional dos computadores modernos, podemos redesenhar todos os 11 sprites de cadeia em cada quadro, devido a isso os g_sprite_chain.update , .dleft , .dtop e todo o processamento associado a eles são .dtop . Outra simplificação diz respeito ao manuseio da bandeira de opacidade. No código original, para cada pixel transparente no sprite, é pesquisada a interseção com o primeiro pixel opaco nas camadas inferiores. Mas eu uso o modo de vídeo de 32 bits e, portanto, posso apenas alterar o valor do byte de transparência no esquema RGBA . Como resultado, eu tenho essas funções de adicionar (excluir) um sprite a (de) uma (s) cadeia (s):


Código
 typedef struct sprite_layer_t { uint8_t flags; uint16_t left; uint16_t top; imh_hdr_t sprite_hdr; uint8_t *sprite_pixels; imh_hdr_t _sprite_hdr; uint8_t *_sprite_pixels; } sprite_layer_t; sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int n, uint32_t left, uint32_t top, uint8_t *sprite, int opaque) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); layer->left = left; layer->top = top; memmove(&layer->sprite_hdr, sprite, sizeof(imh_hdr_t)); layer->sprite_pixels = sprite + sizeof(imh_hdr_t); memmove(&layer->_sprite_hdr, &layer->sprite_hdr, sizeof(imh_hdr_t) + sizeof(uint8_t*)); layer->flags = ((opaque << 4) & 16) | 1; } void remove_sprite_from_chain(int n) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); } 

A função de transferir uma camada para um buffer VGA é a seguinte:


 void draw_to_vga(int left, int top, uint32_t w, uint32_t h, uint8_t *pixels, int bg_transparency); void draw_sprite_to_vga(sprite_layer_t *sprite) { int32_t top = sprite->top; int32_t left = sprite->left; uint32_t w = sprite->sprite_hdr.width * 2; uint32_t h = sprite->sprite_hdr.height; uint32_t bg_transparency = ((sprite->flags >> 4) == 0); uint8_t *pixels = sprite->sprite_pixels; draw_to_vga(left, top, w, h, pixels, bg_transparency); } 

A função draw_to_vga é a função com o mesmo nome descrito na segunda parte , mas com um argumento adicional indicando a transparência do plano de fundo da imagem. Adicione a chamada draw_sprite_to_vga ao início da função de render (o restante de seu conteúdo migrou da segunda parte ):


 static void render() { for (int i = 10; i >= 0; i--) { if (!(g_sprite_chain[i].flags & 1)) { continue; } draw_sprite_to_vga(&g_sprite_chain[i]); } ... } 

Também escrevi uma função que atualiza a posição do sprite do cursor, dependendo da posição atual do ponteiro do mouse ( update_cursor ) e de um gerenciador de recursos simples. Fazemos todo esse trabalho juntos:


 typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_BACKGRND = 10, SCI_TOTAL = 11 } spite_chain_index_t; uint8_t g_cursors[399]; /* seg009 */ uint8_t g_background[32063]; /* seg010 */ int main(int argc, char *argv[]) { ... assert(resource_manager_load("CURSORS.IMH", g_cursors)); add_sprite_to_chain(SCI_CURSOR, 160, 100, g_cursors, 0); assert(resource_manager_load("TITLE.IMH", g_background)); add_sprite_to_chain(SCI_BACKGRND, 0, 0, g_background, 1); while (sfRenderWindow_isOpen(g_window)) { ... update_cursor(); render(); } ... } 

Cursor.GIF



Ok, para um menu principal completo, o menu em si não é suficiente. É hora de voltar à reversão das caixas de diálogo. [Da última vez, draw_frame função draw_frame , que forma a caixa de diálogo e, em parte, a função draw_string , usando apenas a lógica de renderização de texto.] Olhando para o novo draw_frame , vi que a função add_sprite_to_chain era usada lá - nada surpreendente, basta adicionar uma caixa de diálogo em cadeia de sprite. Era necessário lidar com o posicionamento do texto dentro da caixa de diálogo. Deixe-me lembrá-lo de como é a chamada para draw_string :


  sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call draw_string ; draw_string("New/Load", 1, 0) 

e a estrutura preenchendo draw_frame [aqui está um pouco à frente, pois draw_frame maioria dos elementos depois de descobrir completamente a draw_string . A propósito, aqui, como no caso de sprite_layer_t , há uma duplicação de campos] :


 typedef struct neuro_dialog_t { uint16_t left; // word[0x65FA]: 0x20 uint16_t top; // word[0x65FC]: 0x98 uint16_t right; // word[0x65FE]: 0x7F uint16_t bottom; // word[0x6600]: 0xAF uint16_t inner_left; // word[0x6602]: 0x28 uint16_t inner_top; // word[0x6604]: 0xA0 uint16_t inner_right; // word[0x6604]: 0xA0 uint16_t inner_bottom; // word[0x6608]: 0xA7 uint16_t _inner_left; // word[0x660A]: 0x28 uint16_t _inner_top; // word[0x660C]: 0xA0 uint16_t _inner_right; // word[0x660E]: 0x77 uint16_t _inner_bottom; // word[0x6610]: 0xA7 uint16_t flags; // word[0x6612]: 0x06 uint16_t unknown; // word[0x6614]: 0x00 uint8_t padding[192] // ... uint16_t width; // word[0x66D6]: 0x30 uint16_t pixels_offset; // word[0x66D8]: 0x02 uint16_t pixels_segment; // word[0x66DA]: 0x22FB } neuro_dialog_t; 

Em vez de explicar o que está aqui, como e por que, apenas deixo esta imagem:



As variáveis x_offt e y_offt são o segundo e o terceiro argumentos para a função draw_string respectivamente. Com base nessas informações, foi fácil criar suas próprias versões de draw_frame e draw_text , depois de renomeá-las para build_dialog_frame e build_dialog_text :


 void build_dialog_frame(neuro_dialog_t *dialog, uint16_t left, uint16_t top, uint16_t w, uint16_t h, uint16_t flags, uint8_t *pixels); void build_dialog_text(neuro_dialog_t *dialog, char *text, uint16_t x_offt, uint16_t y_offt); ... typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_DIALOG = 2, ... } spite_chain_index_t; ... uint8_t *g_dialog = NULL; neuro_dialog_t g_menu_dialog; int main(int argc, char *argv[]) { ... assert(g_dialog = calloc(8192, 1)); build_dialog_frame(&g_menu_dialog, 32, 152, 96, 24, 6, g_dialog); build_dialog_text(&g_menu_dialog, "New/Load", 8, 0); add_sprite_to_chain(SCI_DIALOG, 32, 152, g_dialog, 1); ... } 


A principal diferença entre minhas versões e as originais é que eu uso valores absolutos de tamanhos de pixel - é mais fácil.




Mesmo assim, eu tinha certeza de que a seção de código imediatamente após a chamada para build_dialog_text responsável pela criação dos botões:


  ... mov ax, 5098h ; "New/Load" push ax call build_dialog_text ; build_dialog_text("New/Load", 1, 0) add sp, 6 mov ax, 6Eh ; 'n' -  push ax sub ax, ax push ax mov ax, 3 push ax sub ax, ax push ax mov ax, 1 push ax call sub_181A3 ; sub_181A3(1, 0, 3, 0, 'n') add sp, 0Ah mov ax, 6Ch ; 'l' -      push ax mov ax, 1 push ax mov ax, 4 push ax sub ax, ax push ax mov ax, 5 push ax call sub_181A3 ; sub_181A3(5, 0, 4, 1, 'l') 

É tudo sobre esses comentários gerados - 'n' e 'l' , que são obviamente as primeiras letras das palavras "New" e "load" . Além disso, se argumentarmos por analogia com o build_dialog_text , os quatro primeiros argumentos do sub_181A3 (doravante - build_dialog_item ) podem ser fatores de coordenadas e tamanhos [de fato, os três primeiros argumentos, o quarto, como se viu, sobre o outro] . Tudo converge se você sobrepuser esses valores na imagem da seguinte maneira:



As variáveis x_offt , y_offt e width na imagem são, respectivamente, os três primeiros argumentos da função build_dialog_item . A altura deste retângulo é sempre igual à altura do símbolo - oito. Após uma análise build_dialog_item do build_dialog_item , descobri que o que neuro_dialog_t estrutura neuro_dialog_t como padding (agora items ) é uma matriz de 16 estruturas da seguinte forma:


 typedef struct dialog_item_t { uint16_t left; uint16_t top; uint16_t right; uint16_t bottom; uint16_t unknown; /* index? */ char letter; } dialog_item_t; 

E o campo neuro_dialog_t.unknown (agora - neuro_dialog_t.items_count ) é o contador do número de itens no menu:


 typedef struct neuro_dialog_t { ... uint16_t flags; uint16_t items_count; dialog_item_t items[16]; ... } neuro_dialog_t; 

O campo dialog_item_t.unknown inicializado com o quarto argumento da função build_dialog_item . Talvez este seja o índice do elemento na matriz, mas parece que esse nem sempre é o caso e, portanto, unknown . O campo dialog_item_t.letter inicializado com o quinto argumento da função build_dialog_item . Novamente, é possível que, no manipulador do botão esquerdo, o jogo verifique as coordenadas do ponteiro do mouse na área de um dos itens (apenas classificando-os em ordem, por exemplo), e se houver um acerto, o manipulador desejado para clicar em um botão específico é selecionado nesse campo. [Não sei como isso é realmente feito, mas implementei exatamente essa lógica.]


Isso é suficiente para criar um menu principal completo, não olhando para o código original, mas simplesmente repetindo o comportamento observado no jogo.


Main_Menu.GIF



Se você assistiu o gif anterior até o final, provavelmente notou a tela do jogo inicial nos últimos quadros. Na verdade, eu já tenho tudo para desenhar. Basta pegar e baixar os sprites necessários e adicioná-los à cadeia de sprites. No entanto, ao colocar o sprite do personagem principal no palco, fiz uma importante descoberta relacionada à estrutura imh_hdr_t .


No código original, a função add_sprite_to_chain , que adiciona a imagem do protagonista à cadeia, é chamada com as coordenadas 156 e 110. Aqui está o que eu vi, repetindo isso sozinho:



Tendo descoberto o que é o quê, obtive o seguinte tipo de estrutura imh_hdr_t :


 typedef struct imh_hdr_t { uint16_t dx; uint16_t dy; uint16_t width; uint16_t height; } imh_hdr_t; 

O que costumava ser um campo unknown acabou sendo valores de deslocamento que são subtraídos das coordenadas correspondentes (durante a renderização) armazenadas na cadeia de sprites.




Assim, a coordenada real do canto superior esquerdo do sprite desenhado é calculada aproximadamente da seguinte maneira:


 left = sprite_layer_t.left - sprite_layer_t.sprite_hdr.dx top = sprite_layer_t.top - sprite_layer_t.sprite_hdr.dy 

Aplicando isso no meu código, obtive a imagem correta e, depois disso, comecei a reviver o personagem principal. De fato, escrevi todo o código relacionado ao controle do personagem (mouse e teclado), sua animação e movimento por conta própria, sem olhar para o original.


Moonwalk.gif



Obteve uma introdução de texto para o primeiro nível. Deixe-me lembrá-lo de que os recursos de string são armazenados em arquivos .BIH . Arquivos .BIH consistem em um cabeçalho de tamanho variável e uma sequência de seqüências terminadas em nulo. Examinando o código original que reproduz a introdução, descobri que o deslocamento do início da parte do texto no arquivo .BIH está contido na quarta palavra do cabeçalho. A primeira linha é a introdução:


 typedef struct bih_hdr_t { uint16_t unknown[3]; uint16_t text_offset; } bih_hdr_t; ... uint8_t r1_bih[12288]; assert(resource_manager_load("R1.BIH", r1_bih)); bih_hdr_t *hdr = (bih_hdr_t*)r1_bih; char *intro = r1_bih + hdr->text_offset; 

Além disso, confiando no original, implementei a divisão da sequência original em substrings para que eles se ajustassem à área de saída de texto, percorrendo essas linhas e aguardando a entrada antes da emissão do próximo lote.


Intro.GIF



No momento da publicação, além do que já foi descrito em três partes, descobri a reprodução do som. Até agora, isso está apenas na minha cabeça e levará algum tempo para perceber isso no meu projeto. Portanto, a quarta parte provavelmente será inteiramente sobre o som. Também pretendo contar um pouco sobre a arquitetura do projeto, mas vamos ver como ele vai.


O reverso do Neuromancer. Parte 4: Som, Animação, Huffman, Github

Source: https://habr.com/ru/post/pt415555/


All Articles