
Bonjour, ceci est la troisième partie d'une série de mes publications consacrée au développement inverse de Neuromancer - une incarnation de jeu vidéo du roman du même nom de William Gibson.
L'inverse du neuromancien. Partie 1: Sprites
L'inverse du neuromancien. Partie 2: police de rendu
Cette partie peut sembler quelque peu chaotique. Le fait est que la plupart de ce qui est décrit ici était prêt au moment de la rédaction du précédent . Depuis que deux mois se sont déjà écoulés depuis, et, malheureusement, je n'ai pas l'habitude de prendre des notes de travail, j'ai simplement oublié quelques détails. Mais en l'état, allons-y.
[Après avoir appris à imprimer des lignes, il serait logique de continuer à inverser la construction des boîtes de dialogue. Mais, pour une raison qui m'échappe, au lieu de cela, je suis complètement allé dans l'analyse du système de rendu.] Encore une fois, en marchant le long de la main
, j'ai pu localiser l'appel qui affiche d'abord quelque chose à l'écran: seg000:0159: call sub_1D0B2
. "N'importe quoi", dans ce cas, est le curseur et l'image d'arrière-plan du menu principal:

Il est à noter que la fonction sub_1D0B2
[ci-après - render
] n'a pas d'arguments, cependant, son premier appel est précédé de deux sections de code presque identiques:
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
Avant d'appeler render
, les curseurs ( cursors.imh
) et background ( title.imh
) sont décompressés en mémoire ( load_imh
est le sub_126CB
renommé de la première partie ), dans les neuvième et dixième segments, respectivement. Une étude superficielle de la fonction sub_123F8
ne m'a pas apporté de nouvelles informations, mais, ne regardant que les arguments de sub_1CF5B
, j'ai tiré les conclusions suivantes:
- les arguments 4 et 5, pris ensemble, représentent l'adresse du sprite décompressé (
segment:offset
); - les arguments 2 et 3 sont probablement des coordonnées, car ces nombres sont en corrélation avec l'image affichée après l'appel du
render
; - le dernier argument peut être le drapeau de l'opacité du fond du sprite, parce que les sprites décompressés ont un fond noir, et on voit le curseur sur l'écran sans lui.
Avec le premier argument [et en même temps avec le rendu en général], tout est devenu clair après avoir sub_1CF5B
. Le fait est que dans le segment de données, en commençant par l'adresse 0x3BD4
, se trouve un tableau de 11 structures du type suivant:
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;
J'appelle ce concept chaîne de sprites. En fait, la fonction sub_1CF5B
(ci-après add_sprite_to_chain
) ajoute le sprite sélectionné à la chaîne. Sur une machine 16 bits, il aurait approximativement la signature suivante:
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);
Cela fonctionne comme ceci:
- le premier argument est l'index dans le tableau
g_sprite_chain
; - les arguments
left
et top
sont écrits dans les g_sprite_chain[index].left
g_sprite_chain[index].top
et g_sprite_chain[index].top
respectivement; - l'en-tête de l'image-objet (les 8 premiers octets situés sur le
segment:offset
) est copié dans le g_sprite_chain[index].sprite_hdr
, tapez imh_hdr_t
(renommé rle_hdr_t
partir de la première partie):
typedef struct imh_hdr_t { uint32_t unknown; uint16_t width; uint16_t height; } imh_hdr_t;
- le champ
g_sprite_chain[index].sprite_segment
enregistre la valeur du segment
; - dans le
g_sprite_chain[index].sprite_pixels
, une valeur égale à offset + 8
écrite, donc sprite_segment:sprite_pixels
est l'adresse bitmap du sprite ajouté; - les
sprite_hdr
, sprite_segment
et sprite_pixels
dupliqués respectivement dans _sprite_hdr
, _sprite_segment
et _sprite_pixels
[pourquoi? - Je n'en ai aucune idée, et ce n'est pas le seul cas d'une telle duplication des champs] ; - dans le champ
g_sprite_chain[index].flags
valeur égale à 1 + (opaque << 4)
écrite 1 + (opaque << 4)
. Cet enregistrement signifie que le premier bit de la valeur des flags
indique "l'activité" du calque "courant" et le cinquième bit indique l' opacité de son arrière-plan. [Mes doutes sur le drapeau de transparence ont été dissipés après avoir testé expérimentalement son effet sur l'image affichée. En modifiant la valeur du cinquième bit lors de l'exécution, nous pouvons observer ces artefacts]:

Comme je l'ai déjà mentionné, la fonction de render
n'a pas d'arguments, mais elle n'en a pas besoin - elle fonctionne directement avec le tableau g_sprite_chain
, en transférant alternativement les «couches» vers la mémoire VGA , du dernier ( g_sprite_chain[10]
- arrière-plan) au premier ( g_sprite_chain[0]
- premier plan). La structure sprite_layer_t
a tout ce dont vous sprite_layer_t
besoin et bien plus encore. Je parle de la update
champs non dleft
, dleft
et dtop
.
En fait, la fonction de render
redessine PAS TOUS les sprites dans chaque image. Une valeur non nulle du champ g_sprite_chain.update
indique que l'image-objet actuelle doit être redessinée. Supposons que nous déplaçons le curseur ( g_sprite_chain[0]
), alors quelque chose comme cela se produira dans le gestionnaire de mouvement de la souris:
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; }
Lorsque le contrôle passe à la fonction de render
, cette dernière, ayant atteint la g_sprite_chain[0]
, voit qu'elle doit être mise à jour. Ensuite:
- l'intersection de la zone occupée par l'image-objet du curseur avant la mise à jour avec toutes les couches précédentes sera calculée et dessinée;
- les coordonnées du sprite seront mises à jour:
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;
- le sprite sera dessiné aux coordonnées mises à jour.
Cela minimise le nombre d'opérations effectuées par la fonction de render
.
Il n'a pas été difficile de mettre en œuvre cette logique, même si je l'ai beaucoup simplifiée. Compte tenu de la puissance de calcul des ordinateurs modernes, nous pouvons nous permettre de redessiner les 11 sprites de chaîne dans chaque image, de ce fait, les g_sprite_chain.update
, .dleft
, .dtop
et tout le traitement qui leur est associé sont .dtop
. Une autre simplification concerne le traitement du drapeau d'opacité. Dans le code d'origine, pour chaque pixel transparent dans l'image-objet, l'intersection avec le premier pixel opaque dans les couches inférieures est recherchée. Mais j'utilise le mode vidéo 32 bits, et donc je peux juste changer la valeur de l'octet de transparence dans le schéma RGBA . En conséquence, j'ai obtenu de telles fonctions d'ajouter (supprimer) un sprite à (de) une chaîne (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)); }
La fonction de transfert d'une couche vers un tampon VGA est la suivante:
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); }
La fonction draw_to_vga
est la fonction du même nom décrite dans la deuxième partie , mais avec un argument supplémentaire indiquant la transparence du fond de l'image. Ajoutez l'appel draw_sprite_to_vga
au début de la fonction de render
(le reste de son contenu a migré depuis la deuxième partie ):
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]); } ... }
J'ai également écrit une fonction qui met à jour la position du sprite du curseur, en fonction de la position actuelle du pointeur de la souris ( update_cursor
), et un simple gestionnaire de ressources. Nous faisons tout cela ensemble:
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(); } ... }
D'accord, pour un menu principal à part entière, le menu lui-même n'est en fait pas suffisant. Il est temps de revenir à l'inversion des boîtes de dialogue. [La dernière fois, j'ai draw_frame
fonction draw_frame
, qui forme la boîte de dialogue, et, en partie, la fonction draw_string
, ne prenant que la logique de rendu du texte à partir de là.] En regardant la nouvelle draw_frame
, j'ai vu que la fonction add_sprite_to_chain
était utilisée - rien de surprenant, juste l'ajout d'une boîte de dialogue en chaîne sprite. Il était nécessaire de gérer le positionnement du texte à l'intérieur de la boîte de dialogue. Permettez-moi de vous rappeler à quoi ressemble l'appel à 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)
et la structure remplissant draw_frame
[voici un peu en avant, puisque j'ai renommé la plupart des éléments après avoir complètement compris draw_string
. Soit dit en passant, ici, comme dans le cas de sprite_layer_t
, il y a une duplication des champs] :
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;
Au lieu d'expliquer ce qui est ici, comment et pourquoi, je laisse juste cette image:

Les variables x_offt
et y_offt
sont respectivement les deuxième et troisième arguments de la fonction draw_string
. Sur la base de ces informations, il était facile de créer vos propres versions de draw_frame
et draw_text
, après les avoir renommées en build_dialog_frame
et 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); ... }

La principale différence entre mes versions et les versions originales est que j'utilise des valeurs absolues de tailles de pixels - c'est plus facile.
Même alors, j'étais sûr que la section de code suivant immédiatement l'appel à build_dialog_text
responsable de la création des boutons:
... 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')
Il s'agit de ces commentaires générés - 'n'
et 'l'
, qui sont évidemment les premières lettres des mots "New"
et "load"
. De plus, si nous build_dialog_text
par analogie avec build_dialog_text
, les quatre premiers arguments de sub_181A3
(ci-après - build_dialog_item
) peuvent être des facteurs de coordonnées et de tailles [en fait, les trois premiers arguments, le quatrième, comme il s'est avéré, au sujet de l'autre] . Tout converge si vous superposez ces valeurs sur l'image comme suit:

Les variables x_offt
, y_offt
et width
dans l'image sont respectivement les trois premiers arguments de la fonction build_dialog_item
. La hauteur de ce rectangle est toujours égale à la hauteur du symbole - huit. Après avoir examiné de très près build_dialog_item
, j'ai découvert que ce que j'ai désigné dans la structure neuro_dialog_t
comme padding
(maintenant des items
) est un tableau de 16 structures de la forme suivante:
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;
Et le champ neuro_dialog_t.unknown
(maintenant - neuro_dialog_t.items_count
) est le compteur du nombre d'éléments dans le menu:
typedef struct neuro_dialog_t { ... uint16_t flags; uint16_t items_count; dialog_item_t items[16]; ... } neuro_dialog_t;
Le champ dialog_item_t.unknown
initialisé avec le quatrième argument de la fonction build_dialog_item
. C'est peut-être l'indice de l'élément dans le tableau, mais il semble que ce ne soit pas toujours le cas, et donc unknown
. Le champ dialog_item_t.letter
initialisé avec le cinquième argument de la fonction build_dialog_item
. Encore une fois, il est possible que dans le gestionnaire de clic gauche, le jeu vérifie les coordonnées du pointeur de la souris dans la zone de l'un des éléments (il suffit de les trier dans l'ordre, par exemple), et s'il y a un hit, le gestionnaire souhaité pour cliquer sur un bouton spécifique est sélectionné dans ce champ. [Je ne sais pas comment cela se fait réellement, mais j'ai mis en œuvre exactement cette logique.]
Cela suffit pour faire un menu principal à part entière, sans regarder le code d'origine, mais simplement en répétant son comportement observé dans le jeu.
Si vous avez regardé le gif précédent jusqu'à la fin, vous avez probablement remarqué l'écran du jeu de démarrage sur les dernières images. En fait, j'ai déjà tout pour le dessiner. Prenez-le et téléchargez les sprites nécessaires et ajoutez-les à la chaîne de sprites. Cependant, en plaçant le sprite du personnage principal sur la scène, j'ai fait une découverte importante liée à la structure imh_hdr_t
.
Dans le code d'origine, la fonction add_sprite_to_chain
, qui ajoute l'image du protagoniste à la chaîne, est appelée avec les coordonnées 156 et 110. Voici ce que j'ai vu, en répétant cela moi-même:

Après avoir compris ce qui est quoi, j'ai obtenu le type de structure suivant imh_hdr_t
:
typedef struct imh_hdr_t { uint16_t dx; uint16_t dy; uint16_t width; uint16_t height; } imh_hdr_t;
Ce qui était autrefois un champ unknown
s'est avéré être des valeurs de décalage qui sont soustraites des coordonnées correspondantes (pendant le rendu) stockées dans la chaîne de sprites.
Ainsi, la coordonnée réelle du coin supérieur gauche du sprite dessiné est calculée approximativement comme ceci:
left = sprite_layer_t.left - sprite_layer_t.sprite_hdr.dx top = sprite_layer_t.top - sprite_layer_t.sprite_hdr.dy
En appliquant cela dans mon code, j'ai obtenu la bonne image, puis j'ai commencé à faire revivre le personnage principal. En fait, j'ai écrit tout le code lié au contrôle du personnage (souris et clavier), son animation et son mouvement par moi-même, sans revenir sur l'original.
Vous avez une introduction de texte pour le premier niveau. Permettez-moi de vous rappeler que les ressources de chaîne sont stockées dans des fichiers .BIH
. .BIH
fichiers .BIH
composent d'un en-tête de taille variable et d'une séquence de chaînes terminées par null. En examinant le code d'origine qui joue l'intro, j'ai découvert que le décalage du début de la partie texte dans le fichier .BIH
est contenu dans le quatrième mot d'en-tête. La première ligne est l'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;
De plus, en m'appuyant sur l'original, j'ai implémenté le fractionnement de la chaîne d'origine en sous-chaînes afin qu'elles tiennent dans la zone de sortie de texte, en faisant défiler ces lignes et en attendant l'entrée avant d'émettre le lot suivant.
Au moment de la publication, en plus de ce qui a déjà été décrit en trois parties, j'ai compris la reproduction du son. Jusqu'à présent, cela n'est que dans ma tête et il faudra du temps pour le réaliser dans mon projet. La quatrième partie devrait donc être entièrement consacrée au son. J'ai également l'intention de vous parler un peu de l'architecture du projet, mais voyons comment ça se passe.
L'inverse du neuromancien. Partie 4: Son, Animation, Huffman, Github