Giải tích kỹ thuật

FINAL FANTASY VIII

Trang phân tích mọi khía cạnh kỹ thuật của Final Fantasy VIII (FF8) do Square Soft phát triển cho máy chơi game Sony PlayStation (PSX), được phát hành ra thị trường vào năm 1999. Phần này được biên soạn lại từ những ghi chú cá nhân trong quá trình debug bản PSX sau khi hoàn thiện bản dịch tiếng Việt cho phiên bản FF8 PC vào năm 2014.

Những phân tích trong trang này dựa trên đối tượng là Rom bản tiếng Nhật. Bản Âu-Mỹ có thể khác đôi chút, nhưng những nét chính thì vẫn giống vậy.

1. Các kiểu thoại 

 Phần thoại (text) xuất hiện trong các phiên bản Final Fantasy, không riêng gì FF8, đều nằm ở ba tình huống với cách gọi tên như bên dưới.

Khi mở Rom FF8 bằng các phần mềm chuyên đọc CD-Rom như CDMage thì sẽ thấy cả 4 disc của FF8 đều có cấu trúc giống nhau, gồm có 3 file như dưới đây.

SYSTEM.CNF là file chứa các tham số liên quan tới lệnh khởi động máy PSX, còn SLPS_018.8ZZZ (ZZZ là 0 với Disc1, là 1 với Disc2,....) là file thực thi (exe) của máy PSX, chứa mã máy để thực thi game. Các lệnh chính của game đều được chứa trong file này. File FF8DISCZZZ.IMG (ZZZ là 1 với Disc1, là 2 với Disc2,...) là nơi chứa dữ liệu hình ảnh, các loại texture, dữ liệu video/âm thanh, các loại pointer, dữ liệu hội thoại và một phần lệnh thi hành. 

Dữ liệu text trong Menu và World Map được DMA từ CD-ROM (từ file FF8DISCZZZ.IMG) vào Ram ngay sau khi khởi động máy, nên máy có thể truy cập vào những đoạn text này vào bất cứ lúc nào khi người chơi nhấn nút bật Menu. Còn với dữ liệu text của Field và Battle chỉ được DMA vào Ram khi vào trận đánh hoặc vào Field tương ứng.

Dữ liệu thoại của Menu, World Map, Battle đều ở dạng không nén, còn dữ liệu thoại của Field thì được nén theo kiểu lzss. Mỗi khối dữ liệu thoại trong Field bao gồm: góc camera trong field đó, các event điều khiển nhân vật qua lại/cử chỉ khi nói chuyện, đường đi trong field, pointer dẫn tới dữ liệu thoại, dữ liệu thoại, font chữ Kanji đặc biệt (không được dùng tới trong bản Âu-Mỹ). 

Mỗi khối thoại trong Field được đặt tên theo từng Field + vị trí trong Field. Những câu thoại được kích hoạt trong cùng một khu vực được xếp chung trong một khối dữ liệu. Chẳng hạn, lời thoại ở các khu vực trên tầng 2 của Balamb Garden, gồm phòng học, hành lang,... được xếp vào các khối dữ liệu có tên: bg2f_1, bg2f_11, bg2f_21, bg2f_4,... Trong đó "bg" là viết tắt của Balamb Garden. Lưu ý là những từ viết tắt này chủ yếu là của tiếng Nhật, ít khi là viết tắt của từ tiếng Anh. Chẳng hạn, các khối thoại xảy ra trong phòng y tế của Balamb Garden gồm có: bghoke_1, bghoke_2, bghoke_3 trong đó "hoke" là viết tắt của "hokenshitsu" (保健室 - phòng y tế) hay những khối thoại xảy ra ở khu vực nhà dân ở Timber gồm có: timin1, timin21, timin22.... Trong đó "ti" là viết tắt của "Timber", còn "min" là viết tắt của "minke" trong tiếng Nhật (民家-nhà dân). Trong một số ít trường hợp thì tên của khối dữ liệu được đặt theo tiếng Anh, chẳng hạn như tipub1 là những đoạn thoại xảy ra trong quán rượu ở Timber.

Có thể dò tìm vị trí của từng khối dữ liệu bằng cách dùng Binary editor (Hex editor) để tìm những từ khóa (định dạng Ascii) như trên đối với file Ram dump, hoặc với FF8DISCZZZ.IMG. Tuy nhiên, đối với file IMG thì khối dữ liệu ở dạng đã nén (lzss) nên đôi khi sẽ không tìm thấy từ khóa cần thiết. Còn với file Ram dump, do ở dạng không nén nên kết quả dò tìm luôn trúng.

2. Font chữ

 Mọi hình ảnh 2D trong game PSX đều ở dạng tim, kể cả font chữ. Tim là một định dạng hình ảnh của Sony, được dùng cho máy PSX, có cấu trúc như được đề cập ở phần bên dưới. Và dĩ nhiên, font chữ trong game cũng ở định dạng tim, độ sâu màu 4bpp. Tất cả các khối thoại trong game, dù là Battle, Menu, World Map hay Field cũng đều dùng chung một bộ font dạng tim có kích cỡ mỗi ký tự là 12 x 12 pixel. Tuy nhiên, một phần cục bộ trong Menu (như phần tên của G.F.) được trình bày bằng font 8 x 8 pixel.

Cách chắc chắn nhất để tìm được font chữ trong game là dump Vram. Dùng debugger chơi tới đoạn có thoại, rồi dump Vram. Sau đó dùng công cụ Vram Viewer để xem vị trí của bộ font.

Công cụ Vram Viwer cho ta biết địa chỉ của bộ font trong Vram của máy PSX bắt đầu tại $80780. Từ đây có hai cách để xác định được vị trí của nó trong CD-Rom. Thứ nhất là reset về đầu game rồi dùng chức năng trace log của debugger để tìm xem bộ font này được DMA vào Vram từ địa chỉ nào. Cách thứ hai, là cách đơn giản hơn: dùng Binary editor để xem dữ liệu ở địa chỉ $80780 trong file Vram, copy những dữ liệu này rồi tìm kiếm trong CD-Rom (file FF8DISCZZZ.IMG) . 

Dù là cách nào đi nữa thì kết quả cũng đều giống nhau. Khối dữ liệu font (tim) bắt đầu tại $BDE4 trong các file FF8DISCZZZ.IMG, và địa chỉ này là cố định cho cả 4 disc.

Tuy nhiên, khối dữ liệu này không đơn thuần là file tim, nên các phần mềm dò định dạng tim như timviwer hay tim2view sẽ không dò được bộ font này trong Rom. Phần dữ liệu tim này chỉ là một thành phần của file "font.tdw" bắt đầu ở địa chỉ $BD80. Cấu trúc của file này như sau.

FF8 dùng kiểu font chữ có độ rộng biến thiên (proportional font/variable width font), rất thuận tiện cho việc dịch thuật sang các ngôn ngữ dùng mẫu tự La Tinh. Chỉ cần điều chỉnh các giá trị tại table này là ta có thể tùy chỉnh độ rộng của từng ký tự theo ý muốn.

Phần dữ liệu thật (hình ảnh/tim) của font chữ bắt đầu ở $01C4, tương đương với $B800 + $01C4 = $B9C4 trong file FF8DISCZZZ.IMG. Cấu trúc của file tim gồm các thành phần được sắp xếp theo thứ tự như bên dưới.

Cụ thể, ở header #1:

"xxxx-xxxx  xxxx-xxxx  xxxx-xxxx  xxxx-abbb"

Trong đó các bit x là zero, bit a cho biết file tim đó có dùng CLUT hay không. Bit a set là file tim có CLUT, bit a reset là không có CLUT. Các bit bbb có ý nghĩa:

000: tim có độ sâu màu 4bpp (16 màu)

001: 8 bpp (64 màu)

010: 16 bpp (256 màu)

111: định dạng hỗn hợp

Như vậy, nếu cụm bit này có giá trị 0x08 thì có nghĩa là file tim này có dùng CLUT, định dạng 8 bpp, nếu giá trị là 0x09 thì là file tim dạng 16 bpp và có dùng CLUT,...

Ta cũng có thể tìm được tọa độ X-Y của khối CLUT bằng công cụ Vram Viwer.

Liền sau phần header #1 là phần dữ liệu CLUT. Nếu thay đổi những giá trị ở đây thì màu sắc của chữ trong game cũng thay đổi. Ngay sau khi kết thúc phần dữ liệu CLUT là tới phần header #2, gồm những thông tin:

Phần khai báo tọa độ X-Y ở đây chính là giá trị tọa độ X-Y mà ta có thể xác nhận bằng Vram Viewer. Và ngay sau phần header #2 là phần dữ liệu thật của tim. Bằng cách dùng Binary editor, ta có thể cắt riêng những thông tin trên để tạo thành một file tim riêng biệt, và rồi sau đó dùng những công cụ như timviewer để chuyển đổi font từ định dạng tim sang dạng hình ảnh bmp/png, chỉnh sửa thành mẫu tự tiếng Việt, rồi chuyển lại thành dạng tim, chèn vào Rom.

3. Độ rộng font chữ

 Thường thì các game tiếng Nhật không dùng tới chức năng chỉnh độ rộng cho từng ký tự, mà tất cả ký tự đều có độ rộng cố định. Kiểu font này được gọi là "monospace font" hoặc "fixed-width font". Còn với các văn bản dùng mẫu tự La Tinh thì người ta thường dùng kiểu font có độ rộng biến thiên theo từng ký tự, được gọi là "proportional font" hoặc "variable width font". Với kiểu thứ hai thì độ rộng của chữ "i" hẹp hơn của chữ "a", tạo nên tính thẩm mỹ của văn bản. Thật khó chịu khi gặp văn bản dùng ký tự La Tinh như tiếng Việt, tiếng Anh mà lại dùng kiểu fixed-width. Đối với giới dịch game thì việc chuyển từ kiểu fixed-width của tiếng Nhật sang kiểu variable width là một tiêu chí lớn. Việc này tuy không khó nhưng cũng đòi hỏi một nền tảng kiến thức vững chắc về phần cứng, phần mềm cũng như cách thức hoạt động của bộ font mới có thể làm được.

So sánh tính thẩm mỹ giữa kiểu variable width và kiểu fixed width

Nhưng may thay, FF8 không dùng font kiểu fixed-width mà dùng kiểu font có độ rộng biến thiên. Dữ liệu độ rộng của từng ký tự được tích hợp luôn trong file tim thể hiện font. Phần dưới đây phân tích cách CPU xử lý dữ liệu độ rộng của từng ký tự ở màn hình đầu tiên của game, ngay sau logo của Square Soft.

Đầu tiên, khối dữ liệu gồm cả độ rộng font chữ và dữ liệu tim của font chữ được DMA từ CD-ROM vào địa chỉ Ram $801B0000, sau đó khối dữ liệu tại địa chỉ này được copy vào địa chỉ Ram $800821F8 qua routine nằm trong file exe (SLPS_018.8ZZZ).

org $8002C4C8

LW v0, 0x0000(a2)

LW v1, 0x0004(a2)

LW a0, 0x0008(a2)

LW a1, 0x000C(a2)

SW v0, 0x0000(a3)

SW v1, 0x0004(a3)

SW a0, 0x0008(a3)

SW a1, 0x000C(a3)

ADDIU a2, a2, 0x0010

 BNE a2, t0, 0x8002C4C8

ADDIU a3, a3, 0x0010

Trong đó Register $a2 chứa địa chỉ nguồn ($801B0000) còn Register $a3 chứa địa chỉ đích ($800821F8), còn t0 là điểm kết thúc của chuỗi dữ liệu. Toàn bộ quá trình đọc text và xử lý độ rộng font chữ được trình bày qua routine bên dưới.

org $801F1804

LBU s0, 0x00(s4)  //đọc text vào s0

ADDIU v0, r0, 0x02  //v0=2

BNE v0, s0, 801F1820  

ADDIU s4, s4, 0x01 //địa chỉ text + 1 byte

LW s3, 0x18(sp)

J 0x801F1804 //xử lý code xuống dòng khi giá trị text = 0x02

ADDIU s5, s5, 0x0D  //s5: tổng chiều cao (pixel) của ký tự

801F1820:

ADDIU v0, r0, 0x05

BNE s0, v0, 801F18AC

SLTI v0, s0, 0x19  //set v0 nếu giá trị text (s0) nhỏ hơn 0x19

LUI v1, 0xE100   //phần này trở đi biến hình ảnh chữ cái thành con trỏ (giá trị text = 0x05)

ORI v1, v1, 0x041F


org $801F18AC

BNE v0, r0, 801F1920

SLTI v0, s3, 0x0181 //set v0 nếu tổng độ rộng ký tự trong chuỗi nhỏ hơn 0x0181 pixel

BEQ v0, r0, 801F1920  //rẽ nhánh tới $801F1920 nếu tổng độ rộng không nhỏ hơn 0x0181 pixel

SLTI v0, s0, 0x20  //set v0 nếu giá trị text nhỏ hơn 0x20 (control code)

BNE v0, r0, 801F18CC

SLL v0, s0, 0x03  //v0 = s0 <<3

J 0x801F81E4

ADDIU s0, s0, 0xFFE0  //s0 = s0 -20 do ký tự Kana có giá trị từ 0x20 trở lên

SUBU v0, v0, s0 //v0 = s0<<3 - s0

SLL s0, v0, 0x05  //s0 = v0<<5

LBU v0, 0x00(s4)   //đọc byte tiếp theo của control code

ADDIU s4, s4, 0x01

ADDU s0, s0, v0  

ADDIU s0, s0, 0xEAE0

801F81E4:

SLL v0, s5, 0x10  //v0 = tổng pixel chiều cao <<16

ANDI v1, s3, 0xFFFF //s3: tổng pixel độ rộng của chuỗi

ORA v0, v0, v1

ADDU a0, s2, r0

ADDU a1, s1, r0

LW a3, 0x5C(sp)

ADDU a2, s0, r0  //chuyển giá trị text sang a2

JAL 801F16D8

SW v0, 0x10(sp)  //ghi tổng pixel vào 0x10(sp)

ADDU s2, v0, r0

ADDIU s1, s1, 0x14

JAL 8002E3EC

ADDU a0, s0, r0

J 801F1804  //đọc byte tiếp theo trong chuỗi

ADDU s3, s3, v0


org $801F6D8

ADDU t0, a0, r0

ADDIU v0, r0, 0x04 //v0=4

SB v0, 0x03(a1)

SLL v1, a1, 0x08 

SWL t0, 0x2(a1)

ADDU t0, v0, r0

ANDI v0, a2, 0x01  //giá trị text AND với 0x1 để biết đó là Hiragana hay Katakana

BEQ v0, r0, 801F1700

ADDIU a0, r0, 0x3812  //lấy hình ảnh Hiragana

ADDIU a0, r0, 0x3852

801F1700:

SRL v1, a3, 0x03

ANDI a3, a3, 0x07

ADDU v0, a0, v0

BEQ v1, r0, 801F1728

SH v0, 0x0E(a1)

LUI v0, 0x8008

LW a3, 0x26B4(v0)

J 801F1734

LUI v0, 0x303C

801F1728:

LUI v0, 0x8008

LW a3, 0x26B0(v0)

LUI v0, 0x30C3

ORI v0, v0, 0x0C31

SRA a0, a2, 0x01

MULT a0, v0

LUI v0, 0x0C

ORI v0, v0, 0x0C

SW v0, 0x10(a1)

SW a3, 0x04(a1)

LW v0, 0x10(sp)

NOP

SW v0, 0x08(a1)

SRA v0, a2, 0x1F

MFHI t1

SRA v1, t1, 0x02

SUBU v1, v1, v0

SLL v0, v1, 0x02

ADDU v0, v0, v1

SLL v0, v0, 0x02

ADDU v0, v0, v1

SUBU a0, a0, v0

SLL v1, v1, 0x08

OR a0, a0, v1

SLL v0, a0, 0x01

ADDU v0, a0, v0

SLL v0, v0, 0x02

SH v0, 0x0C(a1)

JR ra

ADDU v0, t0, r0


org $8002E3EC

ANDI v0, a0, 0x400

BEQ v0, r0, 8002E404

LUI v0, 0x8008

ANDI a0, a0, 0x03FF

J 8002E40C

ADDIU v1, v0, 0x23BC

8002E404:

LUI v0, 0x8008

ADDIU v1, v0, 0x21F8 //v1= 800821F8, địa chỉ khối dữ liệu độ rộng

SRA v0, a0, 0x01 //giá trị text chia đôi vì độ rộng 1 ký tự = giá trị 1 nibble trong byte

ADDU v0, v1, v0  //địa chỉ độ rộng = 800821F8 + giá trị nibble của text

LBU v1, 0x00(v0)  //đọc độ rộng

ANDI v0, a0, 0x01 //xác định độ rộng là của ký tự Hiragana hay Katakana

BEQ v0, r0, 8002E428

NOP

SRL v1, v1, 0x04

8002E428:

JR ra

ANDI v0, v1, 0x0F  //bỏ 1 nibble


4. Font phụ

 Ngoài bộ font chính có kích cỡ 12 x 12 pixel được dùng trong phần thoại của Field, Battle, World Map và một số phần ở Menu thì FF8 còn sử dụng một bộ font phụ có kích cỡ 8 x 8 pixel trong một số phần của Menu. Tên của G.F. trong Menu kết nối (Junction) là một trong số những phần được thể hiện bằng bộ font phụ này.

Bộ font phụ này cũng chính là hình ảnh tim đầu tiên ta tìm được trong CD nếu scan bằng những công cụ như tim2view. Và cũng như bộ font chính, bộ font phụ là kiểu hình ảnh 4 bpp, trong đó gồm 1 bit plane cho kiểu chữ Hiragana và 1 bit plane cho kiểu Katakana.

Bằng cách chỉnh sửa nội dung của bộ font phụ này thành ký tự La Tinh thì ta có thể biến tên G.F. và những phần khác trong Menu thành tiếng Việt/Anh. Nhưng trước hết, có một số yếu tố khác ngoài hình ảnh của font chữ mà ta cần phải quan tâm. Khi CPU đọc chuỗi ký tự trong Menu thì nó xử lý qua các giai đoạn như bên dưới (ví dụ về các chuỗi ký tự "ジャンクション", "はずす", "さいきょう", "アビリティ").

1) Giai đoạn 1: đọc ID của chuỗi text, lấy địa chỉ text

org $801F1050

SLL a1, a1, 0x01 //a1: ID của chuỗi ký tự (00:ジャンクション, 01:はずす,...), ID này dẫn tới pointer

ADDU v1, a0, a1 //a0: địa chỉ bắt đầu của khối text, gồm cả pointer

ADDIU v1, v1, 0x02  //Pointer dẫn tới các chuỗi ký tự bắt đầu ở byte #02 trong khối dữ liệu

LHU v0, 0x00(v1) //đọc giá trị pointer

NOP

BNE v0, r0, 801F1070  //nếu pointer khác zero thì thoát khỏi routine này

ADDU v0, a0, v0  //cộng giá trị pointer vào địa chỉ bắt đầu khối dữ liệu để được địa chỉ bắt đầu của chuỗi text

ADDIU v0, r0, r0  //v0 = zero

801F1070:

JR ra

NOP


2) Giai đoạn 2: đọc chuỗi text, căn chỉnh vị trí chuỗi text

Sau khi thoát khỏi routine trên, CPU sẽ đọc từng ký tự trong chuỗi cho đến khi gặp control code báo kết thúc chuỗi (0x00). Mục đích của phần xử lý này là để đếm xem chuỗi đó có bao nhiêu ký tự, rồi từ đó tính vị trí trình bày chuỗi tiếp theo lên màn hình ở giai đoạn sau đó.

org $801F61C0

ADDU a0, v0, r0 //a0 = v0 = địa chỉ bắt đầu của chuỗi text

ADDU v1, r0, r0 //v1 = zero: bộ đếm  ký tự

801F61C8:

LBU v0, 0x00(a0) //đọc byte tại địa chỉ text

NOP

BEQ v0, r0, 801F61E0  //xử lý kết thúc chuỗi ở $801F61E0

ADDIU a0, a0, 0x01 //tăng địa chỉ bắt đầu text thêm 1 byte

J 801F61C8  //lặp lại chu trình đọc text

ADDIU v1, v1, 0x01  //tăng giá trị của bộ đếm ký tự

801F61E0:

SLL v1, v1, 0x03 //giá trị bộ đếm x 8, vì mỗi ký tự chiếm 8 pixel chiều ngang

ADDIU v1, v1, 0x0C //cộng thêm 12 pixel khoảng cách giữa các chuỗi

ADDU s2, s2, v1 //s2: số pixel khoảng cách giữa các chuỗi ký tự

SH s2, 0x00(s0)  //ghi số pixel khoảng cách vào địa chỉ ở s0

ADDIU s0, s0, 0x02 //tăng 2 byte vào địa chỉ s0

J 801F6198  //xử lý chuỗi ký tự tiếp theo

ADDIU s3, s3, 0x01 

Từ đoạn xử lý trên thì ta dễ dàng chỉnh sửa được khoảng cách giữa các ký tự trong chuỗi, cũng như khoảng cách giữa các chuỗi ký tự với nhau.

3) Giai đoạn 3: xuất ký tự ra màn hình

Sau giai đoạn tính toán vị trí text thì CPU sẽ đọc lại chuỗi text lần nữa rồi xử lý những gì được xuất ra màn hình ở bước tiếp theo.

org $8002C5A8

LBU v1, 0x00(t3)  //t3: địa chỉ bắt đầu của chuỗi text

NOP

BEQ v1, r0, 8002C6C0  //nếu ký tự là 00 thì kết thúc đọc chuỗi

ADDIU t3, t3, 0x01  //tăng địa chỉ text lên 1 byte

SLTIU v0, v1, 0x19 //set v0 nếu giá trị text nhỏ hơn 0x19

BNE v0, r0, 8002C5A8 //v1 nhỏ hơn 0x19 thì rẽ đến 8002C5A8

SLTIU v0, v1, 0x20 //set v0 nếu giá trị text nhỏ hơn 0x20

BEQ v0, r0, 8002C5E8 //nếu là ký tự thông thường (0x20 trở lên) thì rẽ đến 8002C5E8

NOP

ADDIU v1, v1, 0xFFE8 //giá trị control code trừ 0x17

SLL v0, v1, 0x03 //v0 = v1 <<3

SUBU v0, v0, v1

SLL v1, v0, 0x05

LBU v0, 0x00(t3)

ADDIU t3, t3, 0x01

ADDU v1, v1, v0

8002C5E8:

ADDIU v1, v1, 0xE0 //v1 = giá trị text + 0xE0

SLTIU v0, v1, 0x0200  //set v0 nếu v1 nhỏ hơn 512 

BEQ v0, r0, 8002C6B8 //nếu v1 (tọa độ ký tự) lớn hơn 512 pixel thì đến 8002C6B8, còn ký tự Kana thông thường luôn nhỏ hơn 512

LUI v0, 0x8005

ADDIU t0, v0, 0x28A8 //t0 = 800528A8: table chứa tọa độ từng ký tự trong Vram

SLL v0, v1, 0x02 //v0 = tọa độ ký tự x 4

ADDU v0, v0, t0 //v0 = địa chỉ chứa tọa độ của ký tự tương ứng

LW v0, 0x04(v0) //v0 = pointer dẫn tới từng ký tự

ADDU t2, a1, r0 //t2 = a1

SRL t1, v0, 0x10  //t1 = pointer >>16 để lấy byte thứ #3

ANDI v0, v0, 0xFFFF //giữ 2 byte cuối trong tọa độ ký tự

ADDU t0, v0, t0 //t0 = tọa độ trên + 800528A8: địa chỉ table

SLL v0, s2, 0x07 //v0 = s2 x 128 để đổi CLUT màu 

BLEZ t1, 8002C6B4 // rẽ đến 8002C6B4 nếu tọa độ ký tự >>16 không lớn hơn zero

ADDIU v0, v0, 0x0002 //CLUT + 2

SLL t4, v0, 0x10 // t4 = (CLUT + 2) <<16

ADDIU a1, a1, 0x000A

LW v0, 0x00(t0) //v0 = tọa độ dấu trọc âm/bán trọc âm và phần Kana

NOOP

Qua phần xử lý trên thì ta có thể chỉnh sửa giá trị byte nào chỉ đến ký tự nào trong bảng font phụ bằng cách sửa đổi các giá trị ở table $800528A8.