神经巫师的反面。 第3部分:完成渲染,制作游戏


嗨,这是我的一系列出版物的第三部分,专门介绍Neuromancer的逆向开发-Neuro Gicer是同名小说的视频游戏体现。


神经巫师的反面。 第1部分:精灵
神经巫师的反面。 第2部分:渲染字体

这部分似乎有些混乱。 事实是,本文中描述的大多数内容在撰写前一篇文章时就已准备就绪。 从那时起已经过去了两个月,而且不幸的是,我没有保留工作记录的习惯,所以我只是忘记了一些细节。 但是,就这样吧。




[在我学会打印行之后,继续颠倒对话框的结构是合乎逻辑的。 但是,出于某种原因,我转而完全进入了渲染系统的分析。]再次,沿着main走,我能够定位首先在屏幕上显示内容的调用: seg000:0159: call sub_1D0B2 。 在这种情况下,“任何”是光标和主菜单的背景图像:




值得注意的是sub_1D0B2函数[以下简称 sub_1D0B2 ]没有参数,但是,它的第一次调用之前是两个几乎相同的代码段:


 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 

在调用render之前,将光标( title.imh )和背景( title.imh )解压缩到内存中( load_imh是从第一部分重命名的sub_126CB ),分别分为第九和第十个部分。 对功能sub_123F8研究并没有给我带来任何新信息,但是仅查看sub_1CF5B的参数,我得出以下结论:


  • 参数4和5一起代表解压缩后的Sprite的地址( segment:offset );
  • 参数2和3可能是坐标,因为这些数字与调用render之后显示的图像相关。
  • 最后一个参数可能是精灵背景不透明度的标志,因为解压后的精灵具有黑色背景,并且我们在屏幕上看到没有它的光标。

使用第一个参数(并同时进行一般渲染),在跟踪sub_1CF5B之后一切都变得清晰了。 事实是,在数据段中,从地址0x3BD4开始, 0x3BD4了以下类型的11个结构的数组:


 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; 

我称这个概念为精灵链。 实际上, sub_1CF5B函数(以下add_sprite_to_chain )将所选的sprite添加到链中。 在16位计算机上,它大约具有以下签名:


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

它是这样的:


  • 第一个参数是g_sprite_chain数组中的索引;
  • lefttop参数分别写入g_sprite_chain[index].leftg_sprite_chain[index].top
  • 将sprite标头(位于segment:offset的前8个字节)复制到imh_hdr_t类型的g_sprite_chain[index].sprite_hdr (从第一部分重命名为rle_hdr_t ):

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

  • g_sprite_chain[index].sprite_segment字段g_sprite_chain[index].sprite_segment记录segment的值;
  • g_sprite_chain[index].sprite_pixels ,写入一个等于offset + 8的值,因此sprite_segment:sprite_pixels是添加的sprite的位图地址;
  • sprite_hdrsprite_segmentsprite_pixels_sprite_hdr_sprite_segment_sprite_pixels sprite_pixels重复[为什么? -我不知道,这不是这种重复字段的唯一情况]
  • 在字段g_sprite_chain[index].flags 1 + (opaque << 4)写入等于1 + (opaque << 4) g_sprite_chain[index].flags1 + (opaque << 4) 。 该记录意味着flags值的第一位指示“当前”层的“活动”,而第五位指示其背景的不透明度[我对透明性标志的怀疑消除了,因为我通过实验测试了其对显示图像的影响。 在运行时更改第五位的值,我们可以观察到这些工件]:


正如我已经提到的, render函数没有参数,但不需要它-它直接与g_sprite_chain数组一起使用,将“层”交替地从最后一个( g_sprite_chain[10] -背景)转移到第一个( g_sprite_chain[0] -前景)。 sprite_layer_t结构具有为此所需的一切,甚至更多。 我说的是未dleft字段updatedleftdtop


实际上, render函数不会在每帧中重画所有精灵。 g_sprite_chain.update字段的值非零表示需要重绘当前的精灵。 假设我们移动了光标( g_sprite_chain[0] ),那么在鼠标移动处理程序中将发生以下情况:


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

当控制权传递给render函数时,后者已到达g_sprite_chain[0]层,因此需要对其进行更新。 然后:


  • 在更新之前的所有图层之前,将计算并绘制光标精灵占据的区域的交集;
  • 精灵坐标将被更新:

 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; 

  • 精灵将在更新的坐标处绘制。

这样可以最小化render功能执行的操作数量。




尽管我简化了很多逻辑,但实现该逻辑并不难。 鉴于现代计算机的计算能力,我们可以负担得起在每帧中重绘所有11 g_sprite_chain.update链精灵, .dleft ,可以.dtop .dleft.dtop.dtop以及与它们相关的所有处理。 另一个简化涉及不透明度标志的处理。 在原始代码中,对于子图中的每个透明像素,将搜索与较低层中的第一个不透明像素的交点。 但是我使用32位视频模式,因此可以在RGBA方案中更改透明度字节的值。 结果,我得到了向(从)链中添加(删除)子画面的功能:


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

将图层传输到VGA缓冲区的功能如下:


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

draw_to_vga函数是第二部分中描述的同名函数,但带有一个附加参数,指示图像背景的透明度。 将draw_sprite_to_vga调用添加到render函数的开头(其余内容从第二部分迁移):


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

我还编写了一个函数,该函数根据鼠标指针的当前位置( update_cursor )和一个简单的资源管理器来更新游标精灵的位置。 我们共同努力:


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

光标GIF



好的,对于功能完善的主菜单,菜单本身实际上还不够。 现在是时候回到对话框的颠倒了。 [上次,我draw_frame构成对话框draw_frame函数,并部分地分析了draw_string函数,仅从那里提取了文本渲染逻辑。]看着新的draw_frame ,我看到那里使用了add_sprite_to_chain函数-不足为奇,仅添加一个对话框在精灵链中。 必须处理对话框中文本的位置。 让我提醒您,对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) 

和填充draw_frame的结构[这有点领先,因为我在完全弄清楚draw_string之后重命名了大多数元素。 顺便说一句,就像在sprite_layer_t的情况下sprite_layer_t ,存在重复字段]


 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; 

我没有解释这里的内容,方式和原因,而是留下了这张图片:



变量x_offty_offt分别是draw_string函数的第二个和第三个参数。 基于此信息,将名称重命名为build_dialog_framebuild_dialog_text后,可以轻松构建自己的draw_framedraw_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); ... } 


我的版本与原始版本之间的主要区别在于,我使用像素大小的绝对值-更容易。




即使这样,我也可以肯定,紧随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') 

这些都是生成的注释'n''l' ,它们显然是单词"New""load"的第一个字母。 此外,如果我们通过类推来与build_dialog_text ,那么build_dialog_text的前四个参数(以下build_dialog_item )可以是坐标和大小的因子[实际上,前三个参数,实际上是第四个参数,关于另一个] 。 如果您将这些值叠加在图像上,则一切收敛,如下所示:



图像中的变量x_offty_offtwidth分别是build_dialog_item函数的前三个参数。 此矩形的高度始终等于符号的高度-八。 在仔细查看build_dialog_item ,我发现在neuro_dialog_t结构中指定为padding (现在为items )的是以下形式的16个结构的数组:


 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; 

字段neuro_dialog_t.unknown (现在为neuro_dialog_t.items_count )是菜单中项目数的计数器:


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

dialog_item_t.unknown字段使用build_dialog_item函数的第四个参数初始化。 也许这是数组中元素的索引,但似乎并非总是如此,因此unknowndialog_item_t.letter字段使用build_dialog_item函数的第五个参数初始化。 再次,有可能在左键单击处理程序中,游戏检查鼠标指针的坐标是否落入其中一项的区域(例如,仅按顺序对其进行排序),如果有命中,则从该字段中选择所需的按钮单击处理程序。 [我不知道这实际上是如何完成的,但我完全实现了这一逻辑。


这足以构成一个完整的主菜单,而无需回顾原始代码,而只是重复其在游戏中观察到的行为。


Main_Menu.GIF



如果您观看了前一个gif到最后,您可能会注意到最后一帧的启动游戏屏幕。 实际上,我已经具备了一切准备。 只需下载并下载必要的精灵并将其添加到精灵链即可。 但是,通过将主角的精灵放置在舞台上,我发现了一个与imh_hdr_t结构有关的重要发现。


在原始代码中,使用坐标156和110调用将主角的图像添加到链中的add_sprite_to_chain函数。这是我所看到的,我自己重复一下:



弄清楚是什么之后,我得到了以下类型的结构imh_hdr_t


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

原来是unknown字段的东西原来是偏移值,这些偏移值是从精灵链中存储的相应坐标(渲染过程中)减去的。




因此,绘制的子画面的左上角的实际坐标大致如下计算:


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

将其应用到我的代码中,我得到了正确的画面,然后我开始恢复主角。 实际上,我自己编写了所有与角色控制(鼠标和键盘),动画和移动相关的代码,而无需回顾原始代码。


Moonwalk.gif



获得了第一级的文字介绍。 让我提醒您,字符串资源存储在.BIH文件中。 .BIH文件由大小可变的标头和一系列以null终止的字符串组成。 检查播放此简介的原始代码,我发现.BIH文件中文本部分开头的偏移量包含在第四个标头单词中。 第一行是简介:


 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; 

此外,依靠原始文档,我实现了将原始字符串拆分为子字符串的过程,以使它们适合用于文本输出的区域,在这些行中滚动,并在发出下一批之前等待输入。


GIF简介



在出版时,除了已经描述的三部分内容外,我还弄清楚了声音的再现。 到目前为止,这只是我的脑海,在我的项目中要花些时间才能实现。 因此,第四部分可能完全与声音有关。 我还计划讲一些有关该项目的体系结构,但让我们看看它的进展情况。


神经巫师的反面。 第4部分:声音,动画,霍夫曼,Github

Source: https://habr.com/ru/post/zh-CN415555/


All Articles