Reverse "Neuromant". Part 3: Finished Rendering, Making the Game


Hi, this is the third part of my series of publications devoted to the reverse development of Neuromant, the video game incarnation of the novel of the same name by William Gibson.


Reverse "Neuromant". Part 1: Sprites
Reverse "Neuromant". Part 2: Render the Font

This part may seem a bit messy. The fact is that most of what was told here was ready while the previous one was being written. Since two months have already passed since that moment, and I, unfortunately, have no habit of keeping working notes, I simply forgot some details. But as it is, let's go.




[After I learned how to print lines, it would be logical to continue reversing the construction of dialog boxes. But, for some reason that eludes me, instead I went completely into the analysis of the rendering system.] Once again, walking along the main , I managed to localize the call that first displays something on the screen: seg000:0159: call sub_1D0B2 . “Something,” in this case, is the cursor and background image of the main menu:




It is noteworthy that the function sub_1D0B2 [hereinafter - render ] has no arguments, however, its first call was preceded by two, almost identical parts of the code:


 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 

Before calling render , cursors ( cursors.imh ) and background ( title.imh ) are unpacked into memory ( load_imh is the renamed sub_126CB from the first part ), into the ninth and tenth segment, respectively. A superficial study of the sub_123F8 function sub_123F8 not bring me any new information, but, just looking at the arguments of sub_1CF5B , I made the following conclusions:



With the first argument [as well as with rendering in general] everything became clear after tracing sub_1CF5B . The fact is that in the data segment, starting from the address 0x3BD4 , there is an array of 11 structures of the following type:


 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; 

I call this concept "sprite chain". In fact, the function sub_1CF5B (hereinafter - add_sprite_to_chain ) adds the selected sprite to the chain. On a 16-bit machine, it would have something like the following signature:


 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); 

It works like this:



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


As I already mentioned, the render function has no arguments, but it doesn’t need it - it works directly with the g_sprite_chain array, transferring the “layers” to the VGA memory in turn, from the last ( g_sprite_chain[10] - background) to the first ( g_sprite_chain[0] - foreground). The sprite_layer_t structure has everything you need and more. I'm talking about the unexamined fields update , dleft and dtop .


In fact, the render function redraws NOT ALL sprites in each frame. The fact that the current sprite needs to be redrawn is indicated by the non-zero value of the g_sprite_chain.update field. Suppose we move the cursor ( g_sprite_chain[0] ), then something like this happens in the mouse movement handler:


 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; } 

When control passes to the render function, the latter, having reached the g_sprite_chain[0] layer, sees that it needs to be updated. Then:



 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; 


This minimizes the number of operations performed by the render function.




It was easy to implement this logic, although I simplified it quite strongly. Taking into account the computing power of modern computers, we can afford to redraw all 11 sprites of the chain in each frame, due to which the fields g_sprite_chain.update , .dleft , .dtop and all processing related to them are .dtop . Another simplification concerns the handling of the opacity flag. In the original code, for each transparent pixel in the sprite, an intersection with the first opaque pixel in the lower layers is searched. But I use 32-bit video mode, and therefore I can simply change the value of the transparency byte in the RGBA circuit. As a result, I have got the following functions of adding (deleting) a sprite to (from) the chain (s):


Code
 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)); } 

The function of transferring the layer to the VGA buffer is as follows:


 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); } 

The draw_to_vga function is the function of the same name described in the second part , but with an additional argument indicating the transparency of the image background. Add a call to draw_sprite_to_vga to the beginning of the render function (the rest of its contents migrated from the second part ):


 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]); } ... } 

I also wrote a function that updates the position of the sprite cursor, depending on the current position of the mouse pointer ( update_cursor ), and a simple resource manager. Making it all work together:


 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



Okay, for the full-fledged main menu, the menu itself is actually not enough. It's time to return to the reversing the dialog boxes. [Last time I disassembled the draw_frame function that forms the dialog box, and partly the draw_string function, taking only the text rendering logic from there.] Looking at the new draw_frame , I saw that the add_sprite_to_chain function was used there — not surprising, just adding the dialog box in sprite chain. It was necessary to deal with the positioning of the text within the dialog box. Let me remind you what the call to the draw_string function looks like:


  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) 

and the structure that fills in draw_frame [here with a bit of advance, since most of the elements I named after I had completely figured out draw_string . By the way, here, as in the case of sprite_layer_t , there is a duplication of fields] :


 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; 

Instead of explaining what’s here, how and why, I’ll just leave this image:



The variables x_offt and y_offt are the second and third arguments of the draw_string function draw_string respectively. Based on this information, it was easy to build your own versions of draw_frame and draw_text , after renaming them build_dialog_frame and 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); ... } 


The main difference between my versions and the original ones is that I use absolute pixel values ​​- it's easier.




Already then, I was sure that the code section responsible for the creation of buttons was responsible immediately after calling build_dialog_text :


  ... 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') 

It's all about these generated comments - 'n' and 'l' , which, obviously, are the first letters in the words "New" and "load" . Further, if we argue by analogy with build_dialog_text , then the first four arguments of sub_181A3 (hereafter, build_dialog_item ) can be multipliers of coordinates and sizes [in fact, the first three arguments, the fourth, as it turned out, about something else] . Everything converges if you impose these values ​​on the image as follows:



The variables x_offt , y_offt and width in the image are, respectively, the first three arguments of the build_dialog_item function. The height of this rectangle is always equal to the height of the symbol - eight. After a very close look at build_dialog_item , I found out that in the neuro_dialog_t structure I designated as padding (now - items ) is an array of 16 structures of the following type:


 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; 

And the neuro_dialog_t.unknown field (now - neuro_dialog_t.items_count ) is the count of the number of items in the menu:


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

The dialog_item_t.unknown field dialog_item_t.unknown initialized by the fourth argument of the build_dialog_item function. Perhaps this is the index of the element in the array, but, it seems, this is not always the case, and therefore unknown . The dialog_item_t.letter field dialog_item_t.letter initialized with the fifth argument of the build_dialog_item function. Again, it is possible that in the handler of the click click the game checks if the coordinates of the mouse pointer hit the area of ​​one of the items (just sorting them out in order, for example), and if there is a hit, then this field selects the desired handler for clicking on a specific button. [I don’t know how this was actually done, but at my place I implemented just such a logic.]


This is enough so that, no longer looking at the original code, but simply repeating its behavior observed in the game, make a full main menu.


Main_Menu.GIF



If you watched the previous GIF until the end, you probably noticed the starting game screen in the last frames. In fact, I already have everything to draw it. Just take it and download the necessary sprites and add them to the sprite chain. However, placing the main character's sprite on the stage, I made one important discovery related to the imh_hdr_t structure.


In the original code, the add_sprite_to_chain function, which adds the image of the protagonist to the chain, is called with coordinates 156 and 110. Here is what I saw by repeating this in my own:



Having understood what's what, I got the following type of structure imh_hdr_t :


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

What used to be the unknown field turned out to be offset values ​​that are subtracted from the corresponding coordinates (during rendering) stored in the sprite chain.




Thus, the real coordinate of the left upper corner of the rendered sprite is calculated approximately as follows:


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

Applying it in my code, I got the right picture, and after that I started to revive the main character. In fact, all the code associated with the control of the character (mouse and keyboard), its animation and movement, I wrote on my own, without looking at the original.


Moonwalk.GIF



I wrote down the text intro for the first level. I recall that string resources are stored in .BIH files. .BIH files consist of a variable-size header and a sequence of null-terminated strings. Investigating the original code that plays the intro, I found out that the offset of the beginning of the text part in the .BIH file is contained in the fourth word of the header. The first line is the intro:


 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; 

Further, relying on the original, I implemented the splitting of the original line into substrings so that they fit into the area for displaying text, scrolling these lines, and waiting for input before issuing the next batch.


Intro.GIF



At the time of publication, beyond what has already been described in three parts, I figured out the sound reproduction. So far it is only in my head and it will take some time to implement it in my project. So the fourth part is likely to be entirely about the sound. I also plan to tell a little about the architecture of the project, but let's see how it goes.


Reverse "Neuromant". Part 4: Sound, Animation, Huffman, Github

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


All Articles