عكس السرطان العصبي. الجزء 3: الانتهاء من العرض ، جعل اللعبة


مرحبًا ، هذا هو الجزء الثالث من سلسلة منشوراتي المكرسة للتطوير العكسي لـ Neuromancer - وهو تجسيد لألعاب الفيديو لرواية تحمل نفس الاسم كتبها ويليام جيبسون.


عكس السرطان العصبي. الجزء 1: العفاريت
عكس السرطان العصبي. الجزء 2: خط التقديم

قد يبدو هذا الجزء فوضويًا إلى حد ما. والحقيقة هي أن معظم ما تم وصفه هنا كان جاهزًا وقت كتابة السابق . نظرًا لأن شهرين قد مرت بالفعل منذ ذلك الحين ، ولسوء الحظ ، ليس لدي عادة الاحتفاظ بملاحظات العمل ، لقد نسيت ببساطة بعض التفاصيل. ولكن كما هي ، دعنا نذهب.




[بعد أن تعلمت طباعة الخطوط ، سيكون من المنطقي الاستمرار في عكس عملية إنشاء مربعات الحوار. ولكن ، لسبب ما هربت مني ، بدلاً من ذلك ، انتقلت تمامًا إلى تحليل نظام العرض.] مرة أخرى ، أثناء السير على طول الخط main ، تمكنت من توطين المكالمة التي تعرض أولاً شيئًا على الشاشة: seg000:0159: call sub_1D0B2 . "أي شيء" ، في هذه الحالة ، هو المؤشر وصورة الخلفية للقائمة الرئيسية:




تجدر الإشارة إلى أن الدالة sub_1D0B2 [من الآن فصاعدًا - render ] لا تحتوي على أي وسيطات ، ومع ذلك ، يسبق الاستدعاء الأول قسمين ، متطابقين تقريبًا من التعليمات البرمجية:


 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 

قبل استدعاء cursors.imh ، يتم تفكيك المؤشرات ( cursors.imh ) والخلفية ( title.imh ) في الذاكرة ( load_imh هو sub_126CB تسميته من الجزء الأول ) ، إلى sub_126CB التاسع والعاشر على التوالي. لم تجلب لي دراسة سطحية للدالة sub_123F8 أي معلومات جديدة ، ولكن بالنظر إلى حجج sub_1CF5B ، خلصت إلى الاستنتاجات التالية:


  • الحجج 4 و 5 ، مجتمعة ، تمثل عنوان العفريت غير المضغوط ( segment:offset ) ؛
  • من المحتمل أن تكون الوسيطات 2 و 3 إحداثيات ، لأن هذه الأرقام ترتبط بالصورة المعروضة بعد استدعاء render ؛
  • قد تكون الحجة الأخيرة هي علم عتامة خلفية العفريت ، لأن العفاريت غير المعبأة لها خلفية سوداء ، ونرى المؤشر على الشاشة بدونها.

بالحجة الأولى [وفي الوقت نفسه مع العرض بشكل عام] ، أصبح كل شيء واضحًا بعد تتبع sub_1CF5B . والحقيقة هي أنه في مقطع البيانات ، بدءًا من العنوان 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 الدالة sub_1CF5B (من الآن فصاعدًا add_sprite_to_chain ) العفريت المحدد إلى السلسلة. على جهاز 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 ؛
  • تتم كتابة g_sprite_chain[index].left في g_sprite_chain[index].left g_sprite_chain[index].top و g_sprite_chain[index].top على التوالي ؛
  • يتم نسخ رأس g_sprite_chain[index].sprite_hdr (أول 8 بايت الموجود في segment:offset ) إلى g_sprite_chain[index].sprite_hdr ، اكتب imh_hdr_t (تمت إعادة تسميته 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 يسجل قيمة segment ؛
  • في g_sprite_chain[index].sprite_pixels ، تتم كتابة قيمة تساوي offset + 8 ، لذا فإن sprite_segment:sprite_pixels هو عنوان الصورة النقطية sprite_segment:sprite_pixels المضافة ؛
  • sprite_hdr sprite_segment و sprite_pixels و sprite_pixels مكررة في _sprite_hdr و _sprite_segment و _sprite_pixels على التوالي [لماذا؟ - ليس لدي أي فكرة ، وليست هذه هي الحالة الوحيدة لتكرار الحقول هذا] ؛
  • في الحقل g_sprite_chain[index].flags القيمة التي تساوي 1 + (opaque << 4) . يعني هذا السجل أن البت الأول من قيمة flags يشير إلى "نشاط" الطبقة "الحالية" ، ويشير البت الخامس إلى عتامة خلفيته. [تم تبديد شكوكي حول علامة الشفافية بعد أن قمت باختبار تأثيرها على الصورة المعروضة بشكل تجريبي. بتغيير قيمة البت الخامس في وقت التشغيل ، يمكننا ملاحظة هذه القطع الأثرية]:


كما ذكرت من قبل ، فإن وظيفة render لا تحتوي على وسيطات ، لكنها لا تحتاج إليها - فهي تعمل مباشرة مع صفيف g_sprite_chain ، وتحويل "الطبقات" بالتناوب إلى ذاكرة VGA ، من الأخيرة ( g_sprite_chain[10] - الخلفية) إلى الأولى ( g_sprite_chain[0] - المقدمة). sprite_layer_t بنية sprite_layer_t على كل ما هو ضروري لهذا وأكثر. أنا أتحدث عن update الحقول غير المراجعة ، dleft و dleft .


في الواقع ، تقوم وظيفة render بإعادة رسم ليس كل نقوش في كل إطار. تشير القيمة غير الصفرية للحقل g_sprite_chain.update إلى أن 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 سلسلة من العفاريت في كل إطار ، نظرًا لهذا .dleft .dtop و. .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]); } ... } 

قمت أيضًا بكتابة وظيفة تقوم بتحديث موضع sprite للمؤشر ، اعتمادًا على الوضع الحالي لمؤشر الماوس ( 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_frame تسمية معظم العناصر بعد أن اكتشفت draw_string بالكامل تمامًا. بالمناسبة ، هنا ، كما في حالة 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_offt و y_offt هما y_offt الثانية والثالثة draw_string على التوالي. بناءً على هذه المعلومات ، لم يكن من الصعب إنشاء draw_frame الخاصة من draw_frame و draw_text ، بعد إعادة تسميتها إلى build_dialog_frame و 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); ... } 


الفرق الرئيسي بين نسخي والإصدارات الأصلية هو أنني أستخدم القيم المطلقة لأحجام البكسل - إنه أسهل.




حتى ذلك الحين ، كنت متأكدًا من أن قسم الشفرة الذي يلي المكالمة إلى 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 ، فإن الوسيطات الأربعة الأولى من sub_181A3 (فيما يلي - build_dialog_item ) يمكن أن تكون عوامل إحداثيات وأحجام [في الواقع ، الحجج الثلاثة الأولى ، الرابعة ، كما اتضح ، عن الأخرى] . يتقارب كل شيء إذا قمت بتراكب هذه القيم على الصورة على النحو التالي:



المتغيرات x_offt و y_offt width في الصورة هي ، على التوالي ، الوسيطات الثلاثة الأولى للدالة build_dialog_item . يساوي ارتفاع هذا المستطيل دائمًا ارتفاع الرمز - ثمانية. بعد إلقاء نظرة فاحصة جدًا على build_dialog_item ، اكتشفت أن ما عيّنته في بنية neuro_dialog_t (الآن 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 تهيئة حقل dialog_item_t.unknown الرابعة للدالة build_dialog_item . ربما يكون هذا هو فهرس العنصر في المصفوفة ، ولكن يبدو أن هذا ليس هو الحال دائمًا ، وبالتالي unknown . dialog_item_t.letter تهيئة حقل dialog_item_t.letter . dialog_item_t.letter مع الوسيطة الخامسة للدالة build_dialog_item . مرة أخرى ، من الممكن أنه في معالج النقر بزر الماوس الأيسر ، تقوم اللعبة بالتحقق من إحداثيات مؤشر الماوس في منطقة أحد العناصر (فقط فرزها بالترتيب ، على سبيل المثال) ، وإذا كانت هناك إصابة ، فسيتم تحديد المعالج المطلوب للنقر على زر معين من هذا الحقل. [لا أعرف كيف يتم ذلك بالفعل ، لكنني طبقت هذا المنطق بالضبط.]


هذا يكفي لإنشاء قائمة رئيسية كاملة ، لا تنظر مرة أخرى إلى الرمز الأصلي ، ولكن ببساطة تكرار سلوكها الذي لوحظ في اللعبة.


Main_Menu.GIF



إذا شاهدت الصورة السابقة حتى النهاية ، فربما لاحظت شاشة بدء التشغيل في الإطارات الأخيرة. في الواقع ، لدي بالفعل كل شيء لأرسمه. فقط خذه وقم بتنزيل الرموز الضرورية وإضافتها إلى سلسلة الرموز المتحركة. ومع ذلك ، من خلال وضع imh_hdr_t الشخصية الرئيسية على المسرح ، اكتشفت اكتشافًا مهمًا يتعلق ببنية imh_hdr_t .


في الكود الأصلي ، يتم add_sprite_to_chain وظيفة add_sprite_to_chain ، التي تضيف صورة بطل الرواية إلى السلسلة ، بالإحداثيات 156 و 110. إليك ما رأيت ، وأكرر هذا بنفسي:



بعد أن اكتشفت ما هو ، حصلت على النوع التالي من البنية 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 من رأس متغير الحجم وتسلسل من سلاسل منتهية بقيمة خالية. عند فحص الكود الأصلي الذي يقوم بتشغيل المقدمة ، اكتشفت أن إزاحة بداية جزء النص في ملف .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



في وقت النشر ، بالإضافة إلى ما تم وصفه بالفعل في ثلاثة أجزاء ، اكتشفت استنساخ الصوت. حتى الآن هذا في رأسي وسيستغرق بعض الوقت لتحقيق ذلك في مشروعي. لذا من المحتمل أن يكون الجزء الرابع بالكامل عن الصوت. أخطط أيضًا لإخبار القليل عن بنية المشروع ، لكن دعنا نرى كيف تسير الأمور.


عكس السرطان العصبي. الجزء الرابع: الصوت ، الرسوم المتحركة ، هوفمان ، جيثب

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


All Articles