
مرحبًا ، هذا هو الجزء الثالث من سلسلة منشوراتي المكرسة للتطوير العكسي لـ 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]; uint8_t g_background[32063]; 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(); } ... }
حسنًا ، للحصول على قائمة رئيسية كاملة ، القائمة نفسها ليست كافية في الواقع. حان الوقت للعودة إلى عكس مربعات الحوار. [آخر مرة ، قمت 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; 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
. مرة أخرى ، من الممكن أنه في معالج النقر بزر الماوس الأيسر ، تقوم اللعبة بالتحقق من إحداثيات مؤشر الماوس في منطقة أحد العناصر (فقط فرزها بالترتيب ، على سبيل المثال) ، وإذا كانت هناك إصابة ، فسيتم تحديد المعالج المطلوب للنقر على زر معين من هذا الحقل. [لا أعرف كيف يتم ذلك بالفعل ، لكنني طبقت هذا المنطق بالضبط.]
هذا يكفي لإنشاء قائمة رئيسية كاملة ، لا تنظر مرة أخرى إلى الرمز الأصلي ، ولكن ببساطة تكرار سلوكها الذي لوحظ في اللعبة.
إذا شاهدت الصورة السابقة حتى النهاية ، فربما لاحظت شاشة بدء التشغيل في الإطارات الأخيرة. في الواقع ، لدي بالفعل كل شيء لأرسمه. فقط خذه وقم بتنزيل الرموز الضرورية وإضافتها إلى سلسلة الرموز المتحركة. ومع ذلك ، من خلال وضع 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
بتطبيق هذا في رمزي ، حصلت على الصورة الصحيحة ، وبعد ذلك بدأت في إحياء الشخصية الرئيسية. في الواقع ، لقد كتبت جميع التعليمات البرمجية المتعلقة بالتحكم في الشخصية (الماوس ولوحة المفاتيح) ، والرسوم المتحركة والحركة الخاصة بي ، دون النظر إلى الأصل.
حصلت على مقدمة نصية للمستوى الأول. دعني أذكرك أنه يتم تخزين موارد السلسلة في ملفات .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;
علاوة على ذلك ، بالاعتماد على الأصل ، قمت بتطبيق تقسيم السلسلة الأصلية إلى سلاسل فرعية بحيث تتناسب مع منطقة إخراج النص ، والتمرير عبر هذه الأسطر ، وانتظار الإدخال قبل إصدار الدفعة التالية.
في وقت النشر ، بالإضافة إلى ما تم وصفه بالفعل في ثلاثة أجزاء ، اكتشفت استنساخ الصوت. حتى الآن هذا في رأسي وسيستغرق بعض الوقت لتحقيق ذلك في مشروعي. لذا من المحتمل أن يكون الجزء الرابع بالكامل عن الصوت. أخطط أيضًا لإخبار القليل عن بنية المشروع ، لكن دعنا نرى كيف تسير الأمور.
عكس السرطان العصبي. الجزء الرابع: الصوت ، الرسوم المتحركة ، هوفمان ، جيثب