Hi, you are reading a continuation of an article on reverse engineering of Neuromant, a video game released by Interplay Productions in 1988 based on the novel by William Gibson. And, if you have not seen the first part , then I recommend starting with it, where I talk about my motivation and share the first results.
Reverse "Neuromant". Part 1: Sprites
And we continue literally from the same place where we stopped last time.
Based on the assumption that the same .PIC
are stored in .PIC, you can try to apply the same functions to the .PIC
files that I unpacked .IMH
resources. I take the first .PIC
( R1.PIC
) and read it into the buffer:
R1.PIC: 4934 0x0000: 0008 010a 000f 020c 0102 020f 0600 0f0f 0x0010: 0004 060e 0000 0607 0309 010f 0a0d 0d0f 0x0020: 981d 0000 2002 0611 74a2 38c5 d003 e9cb 0x0030: fb18 ac3b 8fbf 2713 459e 8c3f 3aa1 6ca7 ... 0x1330: 6f8a c3f5 d9c7 53e5 f47c d945 51d9 c753 0x1340: e5bf 1ebf 3f00
Great, this file starts the same way as any .IMH
:
0x00:0x1F
- 32 bytes of unknown purpose [in the last part I mistakenly thought that it was a palette, but that it really was - I hadn’t yet figured out] ;word 0x20
is the byte size of the data after decompression [I understood this a bit later when I noticed that the size of the output data of the decompression algorithm coincides with this value] ;word 0x22
- a zero separating the data [it is quite possible that this is the second Word length, but here I have not met such large values] .Now carefully, under the debugger, we try to decode it. The decompression function completes successfully and returns the length of the decompressed data:
R1.PIC_decompd: 7576 0x0000: e901 1130 1113 0111 3011 1301 1130 1113 0x0010: 0111 3011 1301 3130 0113 f801 3330 1333 ... 0x1d80: 00ff 0816 00fe 8000 0108 0100 0180 ff08 0x1d90: 2b00 ff80 0288 0200
But what I see here is somewhat different from what I observed after decompression of .IMH
resources. No, it still looks like a bitmap encoded by the same Run-Length algorithm, but without the _ [ struct rle_hdr_t
from the last part] header , containing pixel dimensions. This is not good, because I used these dimensions to stop the decoding algorithm in time [the decode_rle()
function from the last part] _ and build the .BMP
header. If we assume that only one image is encoded there, then the length of the input data would be enough to stop the algorithm, but to save the image in .BMP
, one cannot do without linear dimensions.
But we will solve problems as they come. Suppose that in each .PIC
one image is actually stored, and their pixel dimensions are the same. I decode_rle()
function so that it counts the number of processed input bytes, and not the decoded output. I pass through it the buffer r1_pic_decompd
. It completes successfully, returning a bitmap with a size of 34048 bytes (17024 * 2, taking into account the fact that the values of two pixels are written in one byte):
R1.PIC_decoded: 17024 0x0000: 0111 3011 1301 1130 1113 0111 3011 1301 0x0010: 1130 1113 0131 3013 1301 3330 1333 0333 ... 0x4270: 7777 7777 7777 7777 7708 8800 0088 8880
Width and height are unknown in the wake-up, but I think that I am dealing with backgrounds of locations, which means that we can try to measure them right in the game. But it is still easier, because among the exported .IMH
resources there was an image of the in-game user interface in the natural size ( NEURO.IMH
). I open the image in Paint and make measurements:
The multiplication of the width and height gives the necessary 34048 bytes - you can safely wrap this and all other backs in .BMP
(55 pieces in total):
On this wave, you can try to split the resource types of an unknown purpose - .BIH
and .ANH
. I act according to the old scheme: I read a random file; if it looks like you can unclench - unclench it; if it is similar to RLE - I decode. And, in general, the plan worked, but what is inside, so to speak ... ambiguous. I take, for example, the contents of the resource R1.BIH
after decompression:
R1.BIH_decompd: 2151 0x0000: 00 00 00 00 00 00 fe 00 2a 00 00 00 00 00 fb 00 .......*...... 0x0010: fd 00 fc 00 00 00 c7 00 46 5a 5a 5f 9b 5f 9f 73 .....FZZ_._џs 0x0020: 46 73 5a 77 00 5f 02 73 01 00 2e 00 7a 00 01 02 FsZw._.s....z... 0x0030: 13 00 03 04 05 12 00 7f 2c 00 08 14 00 2e 13 00 ........,....... 0x0040: 0e 3d 00 01 00 0e 12 00 ff ff 0e 14 00 00 00 03 .=............ 0x0060: 01 11 16 db 00 01 1f 02 20 0e 14 00 00 00 0e 12 ....... ....... 0x0070: 00 ff ff 12 06 10 00 ff cc ff 05 10 00 06 07 00 ........... 0x0080: 01 08 04 04 00 01 07 04 bc ff 01 0b 13 00 0c 03 ........ј...... 0x0090: 17 05 10 00 ff 05 00 04 f9 ff 05 10 00 0c 07 00 ............. 0x00A0: 01 0f 04 0a 00 05 10 00 0d 0b 00 01 10 13 00 0c ................ 0x00B0: 03 04 df ff 01 13 13 00 17 02 06 10 00 ff f8 ff ........... 0x00C0: 05 10 00 17 07 00 01 19 04 ed ff 13 00 1b 02 01 .............. 0x00D0: 1a 06 10 00 ff fc ff 05 10 00 1b 07 00 01 1d 04 ............. 0x00E0: 04 00 01 1e 13 00 ff 00 04 ff ff e8 13 00 81 c3 ..........Ѓ 0x00F0: 04 00 8b 1f 8b 47 14 01 87 ca 00 83 97 cc 00 00 .....G....ѓ—.. 0x0100: cb e8 01 00 90 5b 81 eb f4 00 c3 cb cb cb 59 6f ..ђ[Ѓ.Yo 0x0110: 75 27 76 65 20 6a 75 73 74 20 73 70 65 6e 74 20 u've just spent 0x0120: 74 68 65 20 6e 69 67 68 74 20 73 6c 65 65 70 69 the night sleepi 0x0130: 6e 67 20 66 61 63 65 2d 64 6f 77 6e 20 69 6e 20 ng face-down in 0x0140: 61 20 70 6c 61 74 65 20 6f 66 20 73 79 6e 74 68 a plate of synth 0x0150: 2d 73 70 61 67 68 65 74 74 69 20 69 6e 20 61 20 -spaghetti in a 0x0160: 62 61 72 20 63 61 6c 6c 65 64 20 74 68 65 20 43 bar called the C 0x0170: 68 61 74 73 75 62 6f 2e 20 41 66 74 65 72 20 72 hatsubo. After r 0x0180: 75 62 62 69 6e 67 20 74 68 65 20 73 61 75 63 65 ubbing the sauce 0x0190: 20 6f 75 74 20 6f 66 20 79 6f 75 72 20 65 79 65 out of your eye 0x01A0: 73 2c 20 79 6f 75 20 63 61 6e 20 73 65 65 20 43 s, you can see C 0x01B0: 68 69 62 61 20 73 6b 79 20 74 68 72 6f 75 67 68 hiba sky through 0x01C0: 20 74 68 65 20 77 69 6e 64 6f 77 2c 20 74 68 65 the window, the 0x01D0: 20 63 6f 6c 6f 72 20 6f 66 20 74 65 6c 65 76 69 color of televi 0x01E0: 73 69 6f 6e 20 74 75 6e 65 64 20 74 6f 20 61 20 sion tuned to a 0x01F0: 64 65 61 64 20 63 68 61 6e 6e 65 6c 2e 0d 0d 41 dead channel...A ... 0x0840: 3f 00 0d 0d 52 61 74 7a 20 72 65 66 75 73 65 73 ?...Ratz refuses 0x0850: 20 74 6f 20 74 61 6b 65 20 79 6f 75 72 20 63 72 to take your cr 0x0860: 65 64 69 74 73 2e 00 edits..
[Yes, this is exactly what you thought.] Other files with the names R%n.BIH
have a similar structure: a certain number of bytes above, followed by a text canvas. Obviously, I deal with in-game strings divided by locations to which they belong [ R1
is the first, R2
is the second, and so on. The starting location, for example, loads the backdrop from R1.PIC
, and the first text that we will see there is this: You've just spent the night sleeping face-down in a plate of synth-spaghetti in a bar called the Chatsubo
. The bytes from the upper part organize some kind of control structure, but apart from the code, it will not be possible to disassemble it, I will try to look into other .BIH
files:
CORNERS.BIH_decompd: 128 0x0000: ff ff f0 00 ff f0 0f ff ff 0f ff ff f0 ff ff ff ... 0x0010: f0 ff ff ff 0f ff ff ff 0f ff ff ff 0f ff ff ff ... 0x0020: 00 00 ff ff 0f ff 00 ff 0f ff ff 0f 0f ff ff f0 ....... 0x0030: 0f ff ff f0 0f ff ff ff 0f ff ff ff 0f ff ff ff .... 0x0040: 0f ff ff ff 0f ff ff ff 0f ff ff ff f0 ff ff ff ... 0x0050: f0 ff ff ff ff 0f ff ff ff f0 0f ff ff ff f0 00 ... 0x0060: ff ff ff f0 ff ff ff f0 ff ff ff f0 ff ff ff 0f . 0x0070: ff ff ff 0f ff ff f0 ff ff f0 0f ff 00 0f ff ff ....
ROOMPOS.BIH_decompd: 1160 0x0000: 68 8f 75 0d 4b 69 00 02 8e 63 02 17 71 74 29 02 hЏu.Ki..Ћc..qt). 0x0010: 0e 63 02 17 68 8f 75 15 72 69 24 02 8e 63 02 17 .c..hЏu.ri$.Ћc.. 0x0020: 15 74 7a 02 16 63 02 17 68 8f 75 0d 2a 69 00 02 .tz..c..hЏu.*i.. 0x0030: 8e 63 02 17 08 74 8c 02 0e 63 02 17 70 63 75 0d Ћc...tЊ..c..pcu. ... 0x0470: 0e 6b 02 0f 6e 8f 75 0d 08 6f 8c 02 8e 69 02 11 .k..nЏu..oЊ.Ћi.. 0x0480: 08 74 8c 02 0e 69 02 11 .tЊ..i..
What I see here is not at all like the pattern that I observed in R%n.BIH
. They don't even look like each other! The contents of CORNERS.BIH
reminiscent of bitmap, and ROOMPOS.BIH
, judging by the name, may be related to the positioning of objects on a location, but its contents are not clear.
In addition to these, there is still: COPEN%n.BIH
, DB%nBIH
, FIJU0.BIH
, HITACHI0.BIH
and many others, but they are similar to R%n.BIH
even though they contain readable text, but the headings places differ. I'll leave it for later and see what's there with .ANH
.
Here the situation is better. All .ANH
titled as R%n.ANH
, which means that they somehow belong to locations. There are not many of them: if for all n
R%n.PIC
and R%n.BIH
, then the corresponding R%n.ANH
is found only for some n
. They are compressed by the same algorithm, let's see what's inside:
R1.ANH_decomp: 1100 0x0000 04 00 4e 01 0f 00 17 00 00 00 0c 00 00 00 02 00 ..N............. 0x0010 0e 00 03 00 0e 00 01 00 0e 00 02 00 0e 00 02 00 ................ 0x0020 0e 00 03 00 0e 00 01 00 0e 00 02 00 0e 00 0e 00 ................ 0x0030 00 00 15 00 19 00 03 00 72 00 11 00 72 00 03 00 ........r...r... 0x0040 ba 00 23 25 03 17 fd 87 00 87 3e 00 ff 44 01 00 є.#%.....>.D.. ... 0x0160 73 00 04 00 73 00 03 00 73 00 14 00 73 00 04 00 s...s...s...s... 0x0170 00 00 03 00 00 00 03 00 73 00 04 00 73 00 04 00 ........s...s... 0x0180 00 00 03 00 00 00 03 00 73 00 04 00 73 00 0e 00 ........s...s... 0x0190 00 00 17 00 00 00 03 00 73 00 04 00 73 00 04 00 ........s...s... 0x01A0 00 00 03 00 00 00 03 00 73 00 04 00 73 00 1b 30 ........s...s..0 0x01B0 0b 10 01 00 ff 03 09 00 ff 30 08 00 fc 13 30 08 .......0...0. ... 0x0430 30 02 05 fe 08 88 02 08 fb 88 08 00 08 00 71 36 0...€..€....q6 0x0440 02 05 fe 00 80 02 08 ff 88 03 08 00 ...Ђ..€...
Alas, there is little useful, well, okay, I will understand along the way. While you can do other things. [Once, when launching the starting location in the game, I noticed that the backdrop of this location was animated. And this is interesting, considering that static images are in .PIC
. Not yet verified, but I think it is in .ANH
that these animations are contained.]
I spent some time writing a simple window utility - a resource viewer. In the process, I had to move from 2017 studio to 2015 due to the fact that the first one broke MFC. In the same solution, I created the LibNeuroRoutines
library LibNeuroRoutines
, into which I will gradually add reversed procedures from the original game. First of all, the functions of decompression and decoding of resources got there. It is convenient to keep these things apart.
After reading a note on reverse engineering on the ScummVM project wiki, I decided to reverse the local text rendering. [At first, I was hoping to just extract the fonts, but in the end, it allowed me to achieve much more.] It was easy to take the first steps in this direction - studying the main
function in the disassembled listing, I discovered a function that takes the address of the string displayed in the main game menu - " New/Load
":
... sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call sub_13C6E ; sub_13C6E("New/Load", 1, 0) ...
Having sub_13C6E
function under the debugger, I make sure that it is this one that displays the transferred string:
And then you could start to trace it, but there is a nuance, it does not accept anything like coordinates. The text is drawn exactly in the center of the frame. Maybe it also draws in this function? But what have the arguments 1 and 0? Then I noticed a call to another function, immediately above sub_13C6E
:
... sub ax, ax push ax push ax mov ax, 1 push ax mov ax, 0Ah push ax mov ax, 14h push ax mov ax, 5 push ax mov ax, 6 push ax call sub_13A9E ; sub_13A9E(6, 5, 20, 10, 1, 0, 0) add sp, 0Eh 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) ...
By tracing this code, I saw that the sub_13A9E
function sub_13A9E
frame, and the draw_string
function - the text in it. It is likely that these calls are connected through some global variables. In any case, it's better to start with sub_13A9E
, especially since it takes an interesting set of arguments.
We need to make measurements and see if the measurements somehow correlate with the values of these arguments. There is no obvious dependence between the numbers here, so, we will trace sub_13A9E
.
This is what happens there: first, a certain structure is filled in the data segment (in the comments I indicated the sequence of execution and specific operations that calculate the recorded value):
; sub_13A9E(6, 5, 20, 10, 1, 0, 0) ; (a, b, c, d, e, f, g) .dseg ... word[0x65FA]: 0x20 ; 10: word[0x6602] - 8 = 32 word[0x65FC]: 0x98 ; 11: word[0x6604] - 8 = 152 word[0x65FE]: 0x7F ; 12: word[0x6606] + 8 = 127 word[0x6600]: 0xAF ; 13: word[0x6608] + 8 = 175 word[0x6602]: 0x28 ; 2: b << 3 = 40 word[0x6604]: 0xA0 ; 3: c << 3 = 160 word[0x6606]: 0x77 ; 4: (d << 3) + word[0x6602] - 1 = 119 word[0x6608]: 0xA7 ; 5: (e << 3) + word[0x6604] - 1 = 167 word[0x660A]: 0x28 ; 6: word[0x6602] = 40 word[0x660C]: 0xA0 ; 7: word[0x6604] = 160 word[0x660E]: 0x77 ; 8: word[0x6606] = 119 word[0x6610]: 0xA7 ; 9: word[0x6608] = 167 word[0x6612]: 0x06 ; 1: a = 6 word[0x6614]: 0x00 ; 14: 0 ... word[0x66D6]: 0x30 ; 17: (d << 2) + 8 = 48 word[0x66D8]: 0x02 ; 15: 2 word[0x66DA]: 0x22FB ; 16: seg11
It is impossible not to notice that the coordinates of the left (32) top (152) corner of the frame, respectively, are located in the Word addresses 0x65FA
and 0x65FC
. If we go further and subtract these values from those recorded in 0x65FE
and 0x6600
, we get 95 and 23. Adding one at a time, the width (96) and height (24) of the frame will be exactly. Very entertaining. Now, if you name the arguments of the function draw_frame
( sub_13A9E
) from a
to e
[the last two arguments are not used by the function, they are probably just added by the compiler] , then you can display the following expressions:
left = b * 8 - 8 = (b - 1) * 8 top = c * 8 - 8 = (c - 1) * 8 width = (d * 8) + (b * 8) + 8 - ((b * 8) - 8) = (d * 8) + 16 = (d + 2) * 8 height = (e * 8) + (c * 8) + 8 - ((c * 8) - 8) = (e * 8) + 16 = (e + 2) * 8
After filling in the considered structure, the address [0x66DA]:[0x66D8]
( 22FB: 0002 - segment: offset ) builds a bitmap image of the frame of the calculated sizes, which is then transferred line by line to VGA memory, starting from the address (in the VGA buffer), the corresponding calculated coordinate of the upper left corner ( 152 * 320 + 32 = 0xBE20, A000:BE20
):
SEG11: 22FB:0002 0000 0000 3000 1800 22FB:000A 000000000000000000000000...000000000000000000000000 22FB:003A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:006A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:009A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:00CA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:00FA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:012A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:015A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:018A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:01BA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:01EA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:021A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:024A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:027A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:02AA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:02DA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:030A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:033A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:036A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:039A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:03CA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:03FA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:042A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0 22FB:045A 000000000000000000000000...000000000000000000000000
VGA: A000:BE20 000000000000000000000000...000000000000000000000000... + 0x140 (320) A000:BF60 000F0F0F0F0F0F0F0F0F0F0F...0F0F0F0F0F0F0F0F0F0F0F00... + 0x140 (320) A000:C0A0 000F0F0F0F0F0F0F0F0F0F0F...0F0F0F0F0F0F0F0F0F0F0F00... + 0x1000 (320 * 21) A000:DAE0 000000000000000000000000...000000000000000000000000
You can move on to the draw_string
function.
draw_string
into the draw_string
, hooked on the following piece of code:
loc_1DB47: mov bx, dx inc dx sub ax, ax mov al, ss:[bx] shl ax, 1 jz short loc_1DB97 shl ax, 1 shl ax, 1 add ax, 0FA6Eh mov si, ax mov cx, 8 mov bx, 4BA1h loc_1DB62: lodsb mov ah, al sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb add di, ss:4BA5h loop loc_1DB62 sub di, ss:4BA7h jmp short loc_1DB47 loc_1DB97: ...
It was “hooked”, because of the large accumulation of instructions lodsb
, stosb
, xlat
, [for myself I concluded that these instructions do most of the useful work, in the end, all this programming comes down to the trivial movement of data from one area memory in another, is not it?] and began to trace it. Coming from the loc_1DB47
tag:
dx
register is the address of the source string " New/Load
", save it in bx
and increment it;ax
( sub ax, ax
);al
code of the symbol of the source string pointed to by bx
( mov al, ss:[bx]
, al = 0x4E ('N')
);loc_1DB97
(check for null-terminator);ax
two more bits to the left and add 0xFA6E
(together with the previous shift it is equivalent: ax = ax * 8 + 0xFA6E
, ax = 0xFCDE
);ax
to si
(and therefore ax
- the address);cx
- 8, and in bx
- 0x4BA1
(probably, also the address).Considering the further loop loc_1DB62
instruction loop loc_1DB62
, a cycle of 8 ( cx
) iterations was prepared here, I deal with it:
lodsb
instruction in al
loads the byte value at ds:si
, si
then incremented. In my case, ds:si = F000:FCDE
, the value 0xC6
loaded into al
, si
becomes equal to 0xFCDF
;ax
written to high ( mov ah, al
, ax = 0xC6C6
);al
vanishes, ax
shifts two bits to the left ( ax = 0x1803
);xlat
instruction uses al
as an index in the byte array at ds:bx
and stores the value written there in al
, and the ds
segment can be redefined. This is where the array is addressed to ss:bx
( 47EA:4BA1 = {0xFF, 0xF0, 0x0F, 0x00}
), and as a result of the instruction: al = ss:bx[al] = 0x00
;stosb
value from al
written to es:di
, di
then incremented. I have es:di = 22FB:0192
, and this is important, because it is there that the bitmap frame is located in which the text will be displayed. After executing the instruction at address 22FB:0192
, the value 0x00
( al
) will be written, and di
will become equal to 0x0193
.The last three steps are repeated three times, then the ss:4BA5
saved to ss:4BA5
( 0x2C
) is added to di
, and the loop
instruction loc_1DB62
code from the loc_1DB62
label to itself again 7 times. After completing the cycle, the ss:4BA7
added to di
, stored at ss:4BA7
( 0x17C
), and the program jumps back to the loc_1DB47
label.
So in this convoluted way, all characters of the source string are processed in turn here. As a result, in the frame the specified text is formed [highlight zeros, or reduce the page] :
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF FFF00FFF00FFFFFFFFFFFFFFFFFFFFFF00F0000FFFFFFFFFFFFFFFFFFFFFFF000FFF FFF000FF00FFFFFFFFFFFFFFFFFFFFF00FFF00FFFFFFFFFFFFFFFFFFFFFFFFF00FFF FFF0000F00FF0000FFF00FFF00FFFF00FFFF00FFFFFF0000FFFF0000FFFFFFF00FFF FFF00F0000F00FF00FF00F0F00FFF00FFFFF00FFFFF00FF00FFFFFF00FFF00000FFF FFF00FF000F000000FF0000000FF00FFFFFF00FFF0F00FF00FFF00000FF00FF00FFF FFF00FFF00F00FFFFFF0000000F00FFFFFFF00FF00F00FF00FF00FF00FF00FF00FFF FFF00FFF00FF0000FFFF00F00FF0FFFFFFF0000000FF0000FFFF000F00FF000F00FF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
Here, in principle, everything is clear, except for the magic number 0xFA6E
, adding to which the symbol code multiplied by eight, we got the address in the F000
segment. This is clearly outside the memory of the program itself, which means you need to google the MS-DOS memory scheme. And googled , specifically, it’s what interests you there: FFA6:E ROM graphics character table
. Considering the segment addressing, the FFA6:E
record is equivalent to F000:FA6E
, and at this address the font is sewn to display characters in the native DOS coding of CP437 . For each character there is allocated 8 bytes. Here is how it works on the example of the letter 'N':
1. : 0x4E 2. : 0x4E * 8 = 0x270 3. : 0xFA6E + 0x270 = 0xFCDE 4. : C6 E6 F6 DE CE C6 C6 00 5. , : C6: 11000110 E6: 11100110 F6: 11110110 DE: 11011110 ( 'N') CE: 11001110 C6: 11000110 C6: 11000110 00: 00000000
And this code:
sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx]
can be illustrated as follows:
Everything is sufficient just to implement this logic on its own and display the font on the screen. [What I did, leaving the positioning of the text for later.]
Under this case, in one solution with ResourceBrowser and LibNeuroRoutines , created the project NeuromancerWin32 . I decided to use SFML as a multimedia backend . Before that, I already had a very positive experience with SDL2 , but here I wanted to try something new. I liked SDL2 , including the fact that it is implemented in C and, accordingly, out of the box has a C- compatible interface. SFML , in turn, is written in C ++ . In order not to complicate my project, I will be leading to C , and, fortunately, SFML has an official C- binding, CSFML .
Already from the start, CSFML was pleased with its ease of use. Here, for example, everything you need to create a window:
sfEvent event; sfVideoMode mode = { 320, 200, 32 }; sfRenderWindow *window = sfRenderWindow_create(mode, "NeuromancerWin32", sfClose, NULL); while (sfRenderWindow_isOpen(window)) { while (sfRenderWindow_pollEvent(window, &event)) { if (event.type == sfEvtClosed) { sfRenderWindow_close(window); } } sfRenderWindow_clear(window, sfBlack); sfRenderWindow_display(window) } sfRenderWindow_destroy(window);
The original game for displaying graphics on the screen uses 256-color VGA mode ( mode 0x13 ). In this mode, drawing is reduced to recording pixel color values in VGA memory ( A000:0000
, 320 * 200
bytes). One byte, in this case, corresponds to one pixel. I decided to act in a similar way, but with the amendment that the 32-bit video mode will be used. Thus, I got myself a buffer uint8_t *g_vga[320*200*4]
(each pixel in 32-bit mode is represented by 4 components - RGBA ), which will serve me as an analogue of the original VGA memory. In this buffer, I'll draw, and then, using SFML , display the contents on the screen:
sfRenderWindow *g_ window = NULL; sfTexture *g_texture = NULL; uint8_t *g_vga[320*200*4]; ... g_texture = sfTexture_create(320, 200); ... void render() { sfTexture_updateFromPixels(g_texture, g_vga, 320, 200, 0, 0); sfSprite *sprite = sfSprite_create(); sfSprite_setTexture(sprite, g_texture, 1); sfRenderWindow_clear(g_window, sfBlack); sfRenderWindow_drawSprite(g_window, sprite, NULL); sfRenderWindow_display(g_window); sfSprite_destroy(sprite); } ... sfTexture_destroy(g_texture);
:
for (int i = 0; i < 320 * 240 * 4; i++) { g_vga[i] = rand() % 256; } while (sfRenderWindow_isOpen(g_window)) { ... render(); }
, . , VGA -: void draw_to_vga(int32_t l, int32_t t, uint32_t w, uint32_t h, uint8_t *pixels)
[ VGA, , ] :
uint8_t red_rectangle[96*48]; memset(red_rectangle, 0x44, 96*48); draw_to_vga(10, 10, 96, 48, red_rectangle); ... render();
, (8x8) :
static uint8_t cp437_font[1024] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00 NULL ... 0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00, // 0x41 A 0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00, // 0x42 B 0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00, // 0x43 C ... }; static uint8_t cp437_font_pixels[4] = { 0xFF, 0xF0, 0x0F, 0x00 }; static uint8_t cp437_font_mask[4] = { 0xC0, 0x30, 0x0C, 0x03 }; void build_character(char c, uint8_t *dst) { uint32_t index = c * 8; /* unprintable */ if (c < 0x20 || c > 0x7E) { return; } memset(dst, 0, 32); for (int i = 0; i < 8; i++) { uint8_t al = cp437_font[index++]; for (int j = 0; j < 4; j++) { dst[i * 4 + j] = cp437_font_pixels[(al & cp437_font_mask[j]) >> (6 - j * 2)]; } } }
:
memset(g_vga, 0xFF, 320 * 200 * 4); uint8_t character_bm[32]; int left = 2, top = 2; for (char c = 0x20; c <= 0x7E; c++) { if (left + 8 >= 320) { left = 2; top += 10; } build_character(c, character_bm); draw_to_vga(left, top, 8, 8, character_bm); left += 8; } render();
build_character
build_string(char *s, uint32_t w, uint32_t h, uint8_t *dst)
, :
memset(g_vga, 0xFF, 320 * 200 * 4); uint8_t string[320 * 20]; build_string("The future is here.\n" "It's just not widely distributed yet.", 320, 20, dst); draw_to_vga(20, 88, 320, 20, string); render();
. , - . , . .
Reverse "Neuromant". Part 3: Finished Rendering, Making the Game
Source: https://habr.com/ru/post/357972/
All Articles