|
After helping out on some other rom hacking projects, I'm now back at working on the 1.0 version of the Cotton 2 translation. Currently I'm only aware of one minor error in the script that needs to be fixed. If there is anything else, now would be a good time to report it. Since this alone would be such a minor update, I decided to do a popular challenge for advanced rom hackers: Making a variable width font (vwf) for the game.
Like most Japanese games Cotton 2 uses a monospace font, i.e. all text characters are printed on fixed grid. For the current version of the patch I just changed the spacing of the grid. This looks fine to be honest, but there is some awkward spacing here and there. Mainly around narrow characters like "i" and "l". So what you want to do is adjust the spacing depending on the width of the characters.
To achieve this, I need to change the games code in some way. If already done this in some minor ways to adjust the monospace font spacing, text speed and to reduce the frequency of the typing sound fx. These were mostly minor changes, where I just changed some immediates, literals and constants in the assembly code. Only the sound fx required some code injection. I actually wrote it directly in machine language, which is only really viable for very simple stuff, because things like loading literals relative to the PC can get annoying very fast. Luckily I already have a working assembly toolchain ready now, because I needed one for the Rent-A-Hero No.1 hacking project. The toolchain/IDE I use is HEW aka "High-performance Embedded Workshop". It's meant to be used to program Renesas MCUs like the SuperH family and works like a charm for this application.
To modify the text drawing routine, you first have to understand how it works. I believe I've written about how the game works before, but not in great detail. All of the game's assets are stored in a container format with the extension *.MF, all the game's code is stored in *.bin files. The game uses a different bin file for each scene. So every cut scene and every level has it's own file. This also means that there is a lot of redundant code. To create a vwf, the game needs to know the width of each character, so the first task is to get this information into the game.
Unlike most text heavy games like RPGs, Cotton 2 doesn't really have a dedicated text rendering routine. I suspect the devs basically repurposed their normal level code to create the cut scenes and the whole thing turned out a bit hack-y. Here are my reasons: There is no traditional font table in the game, instead the game stores each character as a separate image together with all the other images of that scene. Meaning functionally there isn't really a difference between the letter "A" and a character sprite, for example. All images are stored together in a section called *.SPT of the *.MF file. Each image in this section has it's own header that contains it's width, height and size. I wrote a script that decomposes the *.SPT section into a human readable format, so here is an example from the first cut scene:
Code:
| | 0000,0000_CHR_VS0.tlm,16842752,352,224,0
0001,0001_CHR_VS0.tlm,16842752,352,224,0
0002,0002_CHR_VS0.tlm,16842752,296,448,0
0003,0003_CHR_VS0.tlm,16842752,296,112,0
0004,0004_CHR_VS0.lz00,0,296,112,4
0005,0005_CHR_VS0.lz00,0,296,112,4
0006,0006_CHR_VS0.lz00,0,296,112,4
0007,0007_CHR_VS0.lz00,0,296,112,4
0008,0008_CHR_VS0.lz00,0,88,72,4
0009,0009_CHR_VS0.lz00,0,88,72,4
0010,0010_CHR_VS0.lz00,0,64,64,4
0011,0011_CHR_VS0.lz00,0,64,64,4
0012,0012_CHR_VS0.lz00,0,64,72,4
0013,0013_CHR_VS0.lz00,0,88,80,4
... |
Since each character is an image and each image has a header that contains it's width, I thought getting the width information would be straight forward. Just change the width of each character image to it's true width (e.g. i -> 3px, A -> 8px, comma -> 4px, ...) and then read the information from the image header. (The image width doesn't affect the font spacing though, so I'd still need write my own routine.) But hardware limitations made this approach impossible. As it turns out, the VDP1 can only draw sprites with a width divisible by 8. So my character images have to retain the 8x16 size or otherwise they can't be drawn. Luckily I found out that I can append information to images without breaking anything. Since images are loaded by pointer and the size is calculated by width*height*bpp (bit per pixel), data appended to the image gets completely ignored by the game. I can just append the true width the character as 4 byte integer and load this information later to determine the correct spacing.
With the spacing information now in the game, the next step is to write the new spacing calculation. The text of the game is stored in the *.SCH section of the *.MF file. This section contains instruction on how to load images, again not just text images, but all the images of the scene. Here is what it looks like decomposed to a text file:
Code:
| | 0000,0,0,1,1,0,0,0,0
0001,0,0,1,1,0,0,0,0
0002,0,0,1,1,0,0,0,0
0003,0,0,1,1,0,0,0,0
0004,64,0,1,1,0,0,0,0
0005,65,0,1,1,0,0,0,0
0006,65,0,1,1,0,0,0,0
[...]
#pause4,74,0,1,1,0,0,0,0
#scroll,74,0,1,1,0,0,0,0
0114,74,0,1,1,0,0,0,0
#end,74,0,1,1,0,0,0,0
#endz,74,0,1,1,0,0,0,0
#starta,74,0,1,1,0,0,0,0
H,74,0,1,1,0,0,0,0
u,74,0,1,1,0,0,0,0
h,74,0,1,1,0,0,0,0
?,74,0,1,1,0,0,0,0
#pause4,74,0,1,1,0,0,0,0
0114,74,0,1,1,0,0,0,0
\n,74,0,1,1,0,0,0,0
#end,74,0,1,1,103,0,0,0
#starta,74,0,1,1,0,0,0,0
H,74,0,1,1,0,0,0,0
e,74,0,1,1,0,0,0,0
0123,74,0,1,1,0,0,0,0
y,74,0,1,1,0,0,0,0
!,74,0,1,1,0,0,0,0
... |
The first item in each row is a relative pointer to the image in the *.SPT section. I've replaced it with a symbolic name of the image to make the translation easier. I actually translated all the text in the game by editing this decomposed text view and recomposing it to the binary representation. But back to the problem at hand. To draw text, the game uses a construct it calls "Event Manager". In a nut shell it's a way to schedule function calls, like a task manager. Here the game schedules a call to the function "InitVisualMessage_Grp" which schedules a call to "InitVisualMessage_Line" for each line of text which schedules a call to "InitVisualMessage_Chr" for each character. InitVisualMessage_Chr is where the magic happens. Here the spacing is calculated. In the unmodified version the position calculation works like this:
Character position = Start Offset + Line Position X 13
Here, 13 is the fixed character spacing or the text grid. In assembly it looks like this:
Code:
| | mov.w @r12, r2 ; line position
mov #h'D, r1 ; default distance to next character
muls r1, r2
sts macl, r2
mov.l @r8, r1 ; start offset
shll16 r2
add r2, r1
mov.l r1, @r8 |
So what I have to do instead is:
- Store the last position I've written to in memory
- Load the character width from the appended image data
- Add this width to the last position to get the current position
- Add this to start offset and write it to appropriate memory position
Here is what it looks like in assembly (written in HEW):
Code:
| | ; ###################################################################
;
; Write custom vwf character width
;
; arg0 = char_idx
; arg1 = p_mem
;
; Variables
;
mf_file_bp: .assign h'6069920
sch_idx_: .reg r6
p_img: .reg r7
mf_idx: .reg r8
p_line_pos .reg r9
.section new,code
f_vwf_gwidth:
mov.l r8, @-r15
mov.l r9, @-r15
sts.l pr, @-r15
; Reset the line position if we're at position 0
mov.l literal3, p_line_pos
tst r4, r4
bf/s skip_lpos_rst
mov r5, r8
mov #0, r0
mov r0, @p_line_pos
skip_lpos_rst:
add #h'48, r8
mov.l @r8, r8 ; (mf_idx, sch_idx)
extu.w r8, sch_idx_ ; sch_idx
shlr16 mf_idx ; mf_idx
mov #h'4c, r1 ; sizeof(mf_meta_t)
mul.l r1, mf_idx
mov.l #mf_file_bp, r1 ; mf_meta_t base pointer
sts macl, r3
add r1, r3 ; pointer to mf_meta_t of specific mf file
mov sch_idx_, r1
shll2 r1
mov r1, r0
mov.l @(8, r3), r2 ; bp_sch from mf_meta_t
mov.l @(r0, r2), r1 ; p_sch from pointer table
mov.l @(12, r3), p_img ; bp_spt from mf_meta_t
mov r2, r0
mov.l @(r0, r1), r1 ; load spt_offset
add r1, p_img ; p_img = bp_spt + spt_offset
add #h'60, p_img ; offset to width information
mov.l @p_img, r1 ; load width of character
mov @p_line_pos, r2
mov r2, r3
add r1, r3
mov r3, @p_line_pos
mov.l @r5, r1 ; left line offset
shll16 r2
add r2, r1
mov.l r1, @r5 ; write char position
lds.l @r15+, pr
mov.l @r15+, r9
rts
mov.l @r15+, r8
literal3:
.data.l var |
And after all this rather dry explanation, here is the final result:
|