Zelda Densetsu SFC

1. Khái quát 

Zelda no Densetsu - kamigami no toraifōsu (ゼルダの伝説 神々のトライフォース ), được tạm chuyển ngữ sang Việt văn là "truyền thuyết Zelda-tam bảo của chư thần", là phiên bản thứ 3 trong series Zelda no Densetsu do Nintendō phát hành. Phiên bản này dành cho máy Super Nintendō, xuất hiện vào cuối năm 1991 ở Nhật, đúng 1 năm sau khi hệ máy này ra đời. Bản dịch tiếng Anh chính thức được phát hành cho các hệ máy SNES Mỹ, SNES Âu châu ở các thị trường này xuất hiện sau đó vài tháng. 

Tựa đề của bản tiếng Anh chính thức khác biệt so với bản gốc tiếng Nhật, cụ thể là "The Legend of Zelda: A Link to the Past". Ngoài tựa đề ra thì các phiên bản tiếng Anh cũng còn nhiều chỗ khác biệt nho nhỏ khác so với bản tiếng Nhật như lời thoại, hình ảnh xuất hiện trong game. Đây là kết quả của quá trình kiểm duyệt để tránh những vấn đề nhạy cảm như tôn giáo ở xã hội Âu Mỹ.

Về lối chơi, phiên bản này quay về với gốc rễ, bám sát với phiên bản Zelda đầu tiên năm 1986 trên máy Famicom (NES) sau khi phiên bản thứ hai năm 1987 cũng trên máy Famicom. Ở phiên bản SFC này xuất hiện nhiều Item mà sau đó đã trở thành những món đồ cố định trong series, như thanh kiếm "Master-sword", lọ thủy tinh,... Phiên bản này cũng là một hình mẫu ảnh hưởng đến nhiều dòng game sau này, chẳng hạn như Dark Souls. Bạn có thể thấy nhiều yếu tố mà Dark Souls học tập ở phiên bản Zelda này, như cách bố trí Item trong màn, chiếc cầu vô hình,...

2. Bản dịch Việt văn

Về mặt dịch thuật, bản dịch này dựa trên nền bản gốc tiếng Nhật, cố gắng bám sát tinh thần của bản gốc hết mức có thể. Sẽ có nhiều chỗ dị biệt nếu đối chiếu với bản Anh ngữ, bởi vì bản Anh ngữ vốn đã có những điểm dị biệt đó so với bản tiếng Nhật. Chẳng hạn như tựa đề của bản gốc mang nghĩa là "Triforce của các vị thần", trong đó "Triforce" là 3 món bảo vật hoàng kim của 3 vị thần sáng tạo nên thế giới trong series Zelda, tượng trưng cho trí tuệ, sức mạnh và dũng khí. Bản dịch Việt ngữ này dựa trên tinh thần của bản tiếng Nhật, có tựa đề là "Tam bảo của chư thần", hoàn toàn khác biệt với "A Link to the Past".

Về mặt kỹ thuật, bản dịch này cũng dựa trên Rom tiếng Nhật được dump trực tiếp từ băng cassette Super Famicom, ứng dụng kỹ thuật lập trình Assembly 65816 ở mức cao độ để phục vụ cho việc dịch thuật ở mức tối đa. Chẳng hạn như khung thoại được mở rộng, thay đổi màu khung thoại, dùng nhiều kiểu font chữ,... Bản dịch này còn có một số tính năng mà vốn đã bị ẩn đi ở phiên bản gốc, một số tính năng vốn không có trong phiên bản gốc. Đây cũng là kết quả của việc ứng dụng kỹ thuật Assembly 65816 trong quá trình dịch thuật.

Lưu ý: đây là bản dịch tiếng Việt, hay nói cách khác là bản chuyển ngữ Việt văn, bản Việt ngữ hóa. Đây hoàn toàn không phải là bản Việt hóa. Dịch sang tiếng Việt (Việt ngữ hóa) và "Việt hóa" là hai khái niệm hoàn toàn khác nhau, không nên nhầm lẫn.

Bản dịch Việt ngữ này hoàn toàn tương thích với phần cứng Super Famicom/SNES cũng như các loại giả lập mô phỏng cấu trúc của phần cứng này, chẳng hạn như ZSNES, SNES9X, BSNES, HIGAN, MESEN,.... Trong trường hợp dùng giả lập thì người dịch khuyến cáo nên dùng các loại giả lập được bôi đậm. Bản dịch cũng chạy tốt trên các loại phần cứng nhái của Super Famicom/SNES như SupaBoy của Hyperkin hay Super-NT của Analogue.

Hình ảnh bản dịch trên máy nhái Super NT với Tivi màn hình phẳng hiện đại

Hình ảnh bản dịch trên giả lập Mesen

Dung lượng bản gốc tiếng Nhật là 8 triệu bit, còn ở bản dịch tiếng Việt này là 16 triệu bit. Sở dĩ bản dịch có dung lượng lớn hơn là bởi vì nó được viết thêm khoảng 15 nghìn dòng code cải tiến về mặt kỹ thuật so với bản gốc, như mô tả dưới đây.

3. Zelda tải bản Việt ngữ

Bản dịch Việt ngữ cùng mã nguồn bản dịch được đăng lên các Zelda dưới đây.

4. Quá trình dịch thuật

Phần này ghi lại những điểm kỹ thuật trong quá trình dịch thuật. Ai quan tâm có thể tham khảo.

A) Các bước để hoàn tất một bản dịch

Trừ bước đầu và bước cuối có thứ tự không đổi thì các bước còn lại có thể xáo trộn trật tự, có thể bỏ qua tùy tình huống.

B) Những thứ cần có

C) Quy trình

Đầu tiên là bước dump nội dung game từ băng cassette. Ở đây tôi dùng băng Super UFO Pro 8 để dump. Tháo nắp phía trên của Super UFO Pro 8 ra, ta sẽ thấy khe cắm băng. Cắm trực tiếp băng Zelda vào đó. Sau đó, cắm cả cụm này vào máy Super Famicom rồi khởi động máy. Trong menu BACKUP của Super UFO Pro 8, chọn mục GAME CART TO SDC. Chức năng của mục này là copy nội dung của băng game (Game cartridge) vào trong thẻ nhớ SD gắn trong Super UFO Pro 8.

Tại mục này, sau khi đặt tên game (tối đa 8 ký tự) thì file sẽ được lưu vào thẻ SD sau một khoảng thời gian tải. Lúc này ta có thể tháo thẻ SD ra khỏi băng Super UFO Pro 8 để cắm vào máy tính, kiểm tra xem trình giả lập có thể chạy được file vừa dump không. Nếu không chạy được thì quá trình dump đã gặp lỗi, cần lặp lại bước trên. Dung lượng đúng của Rom gốc là 1MB (1024KB).

Sau khi mọi thứ đã hoàn tất thì ta có thể debug Rom bằng chính debugger hoặc dùng phần mềm disassembler để có cái nhìn tổng thể về cấu trúc Rom. Nhưng để debug hay giải mã bằng disassembler thì đầu tiên ta cần biết địa chỉ của một số vector cơ bản như Reset, NMI, IRQ. Tất cả những thông tin này đều nằm ở phần header của Rom. Mọi Rom Super Famicom/SNES đều chứa những thông tin này. Ngay khi cắm băng vào máy rồi khởi động, việc đầu tiên máy Super Famicom/SNES làm là tìm kiếm những thông tin này trong băng. Do vậy, những thông tin khai báo trên buộc phải ở vị trí cố định.

Phần header của Rom SNES bắt đầu ở địa chị $00FFB0. Do Zelda là LoRom nên địa chỉ này tương ứng với $007FB0 khi xem bằng Hex-editor trên máy tính. Có thể dùng Lunar Address để chuyển đổi địa chỉ SNES $00FFB0 thành địa chỉ PC $007FB0. Phần header này chứa thông tin về mã game, mã nhà sản xuất, kiểu Rom là LoRom hay HiRom, có dùng chip đặc thù hay không, dung lượng Rom là bao nhiêu, có dùng pin không, có Sram hay không, mã vùng sản xuất là quốc gia nào, tên game là gì,... Tên game là chuỗi ký tự dài 0x15 byte (21 ký tự) bắt đầu tại địa chỉ $00FFC0, tức $007FC0 địa chỉ PC đối với LoRom. Đây là tên mà nhà sản xuất đăng ký với Nintendō chứ không nhất thiết là tên thương mại hay tựa đề xuất hiện ở màn hình game.

Sau phần thông tin này là đến thông tin khai báo địa chỉ của các vector. Ta cần quan tâm nhất là vector Reset và NMI. Vector NMI luôn nằm ở địa chỉ $00FFEA và $00FFFA, còn vector Reset ở địa chỉ $00FFEC và $00FFFC.

Khi đã biết được vector Reset chỉ về địa chỉ $008000 thì ta có thể đặt breakpoint trong debugger hoặc disassembler để nắm được máy sẽ chạy những lệnh gì khi khởi động.

 Toàn bộ cấu trúc của Zelda như dưới đây. Phần màu xanh là các thủ tục định dạng Ram ban đầu, upload driver âm thanh. Phần màu hồng là vòng lặp chính của game.


80/8000: 78      SEI 

80/8001: 9C0042  STZ $4200

80/8004: 9C0C42  STZ $420C

80/8007: 9C0B42  STZ $420B

80/800A: 9C4021  STZ $2140

80/800D: 9C4121  STZ $2141

80/8010: 9C4221  STZ $2142

80/8013: 9C4321  STZ $2143

80/8016: A980    LDA #$80

80/8018: 8D0021  STA $2100

80/801B: 18      CLC 

80/801C: FB      XCE 

80/801D: C228    REP #$28

80/801F: A90000  LDA #$0000

80/8022: 5B      TCD 

80/8023: A9FF01  LDA #$01FF

80/8026: 1B      TCS 

80/8027: E230    SEP #$30

80/8029: 200189  JSR $8901

80/802C: 20C087  JSR $87C0

80/802F: A981    LDA #$81

80/8031: 8D0042  STA $4200

80/8034: A512    LDA $12

80/8036: F0FC    BEQ $8034

80/8038: 58      CLI 

80/8039: 8016    BRA $8051

80/803B: A5F6    LDA $F6

80/803D: 2920    AND #$20

80/803F: F003    BEQ $8044

80/8041: EED70F  INC $0FD7

80/8044: A5F6    LDA $F6

80/8046: 2910    AND #$10

80/8048: D007    BNE $8051

80/804A: ADD70F  LDA $0FD7

80/804D: 2901    AND #$01

80/804F: D009    BNE $805A

80/8051: E61A    INC $1A

80/8053: 201E84  JSR $841E

80/8056: 22B58000 JSR $0080B5

80/805A: 20FC85  JSR $85FC

80/805D: 6412    STZ $12

80/805F: 80D3    BRA $8034


80/80B5: A410    LDY $10

80/80B7: B96180  LDA $8061,Y

80/80BA: 8503    STA $03

80/80BC: B97D80  LDA $807D,Y

80/80BF: 8504    STA $04

80/80C1: B99980  LDA $8099,Y

80/80C4: 8505    STA $05

80/80C6: DC0300  JMP [$0003]

Mặc dù đều cùng là sản phẩm của Nintendō nhưng cấu trúc của Zelda đơn giản hơn rất nhiều so với các phiên bản Fire Emblem sau này. Trong mỗi lần lặp, CPU đều xử lý routine $0080B5. Các phân đoạn trong game đều được xử lý ở routine này. Các phân đoạn này được quyết định bởi giá trị của địa chỉ Ram $10. Chẳng hạn, nếu giá trị của $10 là 0x00 thì game sẽ hiện màn hình đầu tiên (Nintendo presents), nếu giá trị là 0x01 thì là màn hình chọn nhân vật, giá trị 0x19 tương đương với cảnh căn phòng Triforce, giá trị 0x1A là cảnh ending,... Mỗi phân cảnh gồm những chuỗi sự kiện, hình ảnh, âm thanh nối tiếp nhau. Game đang xử lý tới "vị trí" nào trong phân cảnh đó là tùy thuộc vào giá trị của $11.

Lợi dụng giá trị của $10 và $11, ta có thể dễ dàng tìm hiểu xem từng phân cảnh được tạo ra như thế nào bằng cách đặt Read break point tại các địa chỉ này. Sau đó ta có thể chỉnh sửa phân cảnh đó tùy ý, bao gồm cả việc thay đổi hình ảnh, font chữ, lời thoại xuất hiện trong phân cảnh đó.

Chẳng hạn như để chuyển màn hình tựa đề game từ tiếng Nhật sang tiếng Việt, ta vẽ lại toàn bộ những thứ xuất hiện trên màn hình rồi chèn vào Rom. Cảnh này dùng chế độ hiển thị mode 1, gồm 2 thành phần:

Việc cần làm ở đây là vẽ lại tileset và bố trí lại tilemap của lớp Background 1, cũng như vẽ lại lớp sprite nếu cần thiết. Sau khi vẽ lại thì chuyển dạng ảnh này thành dạng dữ liệu đồ họa của SNES rồi chèn vào Rom. Nhưng để làm được việc đó thì ta cần phải biết vị trí của tileset và tilemap trong Vram. Bằng cách dùng công cụ Tile Viewer của Mesen debugger (Bsnes cũng có công cụ tương tự), ta dễ dàng xác định được khối tileset tựa đề game nằm ở địa chỉ Vram $6000.

Đặt Write break point tại địa chỉ này trong Vram, ta xác định được thời điểm mà khối hình ảnh này được chuyển vào Vram là ngay sau khi dòng chữ "Nintendo presents" ở màn hình đầu tiên kết thúc, qua routine $00E310. Tại đây, các thành phần đồ họa của Background và sprite đều được lần lượt chuyển vào Vram qua cổng $002118 mà không thông qua kỹ thuật DMA nên tốc độ chuyển không được nhanh. Ta có thể thay thế bằng routine dưới đây. Trong đó zelda_chr là dữ liệu đồ họa của màn hình tựa đề mới mà ta đã vẽ bằng Pixelformer rồi chuyển đổi bằng SuperFamiconv

PHP

SEP #$20

REP #$10

LDA.b #(zelda_chr>>16)

STA $4304

LDA #$80

STA $2115

LDX #$1801

STX $4300

LDX #(zelda_chr)

STX $4302

LDX #$18C0

STX $4305

LDX #$6000>>1

STX $2116

LDA #$01

STA $420B

LDX #$0000

_loop:

LDA zelda_map,x

STA $0B00,x

INX

CPX #$0700

BNE _loop

LDX #$0000

_loop2:

LDA zelda_map+1,x

CLC

ADC #$39

STA $0B01,x

INX #2

CPX #$0700

BNE _loop2

PLP

Phần _loop và _loop2 là chức năng copy tilemap của mành hình mới được chuyển đổi bằng SuperFamiconv. Tilemap được copy vào địa chỉ Ram $7E0B00 hoặc bất kỳ địa chỉ trống nào đủ 0x700 byte. Về lý thuyết, màn hình mode1 có kích thước 256x256 pixel, tương đương với tilemap 0x800 byte nhưng đặc thù của máy Super Famicom/SNES hệ NTSC là Tivi chỉ hiển thị đến scanline thứ 224 là hết, do vậy chiều cao chỉ là 224 pixel nên tilemap chỉ cần 0x700 byte là đủ. Bước copy tilemap mới vào địa chỉ Ram trống là để phục vụ cho bước DMA tilemap này vào Vram sau này.

Để xác định được vị trí của Background 1 tilemap trong Vram thì ta dùng công cụ Tilemap Viwer của debugger. Tilemap của Background 1 trong phân cảnh này bắt đầu tại $2000. Bằng cách đặt Write break point tại địa chỉ này thì ta biết được Tilemap được chuyển vào Vram ở địa chỉ $008BE2: JSR $92A1. Routine $92A1 được thực hiện trong khoảng thời gian Vblank, và là routine chung để cập nhật tilemap của Background 1 trong suốt game. Do vậy ở đây ta cần đặt điều kiện để routine mới chỉ được thực hiện trong phân cảnh tựa đề game để tránh ảnh hưởng tới những phân cảnh khác.

org $008BE2

JML freeram


freeram:

LDA $10

BNE +

PHP

SEP #$20

REP #$10

LDX #$1801

STX $4300

LDX #$0700

STX $4305

LDX #$0B00

STX $4302

LDX #$2000>>1

STX $2116

LDA #$7E

STA $4304

LDA #$80

STA $2115

LDA #$01

STA $420B

PLP

JML $008BE5

+

JSR _0092a1

JML $008BE5

Đoạn code trên DMA 0x700 byte tilemap ở địa chỉ $7E0B00 vào $2000 trong Vram chỉ khi game đang ở mode 0, tức màn hình đầu tiên. Sau đoạn code này thì ta được kết quả như dưới đây.

Xong màn hình mở đầu, tiếp theo là phần chính: text trong phần thoại chính của game. Game Super Famicom/SNES có 2 kiểu hiển thị text chính là render trong thời gian thực, và pre-render (render sẵn). Kiểu hiển thị pre-render có ưu điểm là không cần xử lý nhiều, và thường là chỉ render tilemap dựa trên tileset là bộ font chữ được chuyển vào Vram trước đó. Kiểu render này đã xuất hiện từ thời Famicom, thường được áp dụng với game không có nhiều text, hay game có bộ font chữ đơn giản. Còn nhược điểm của nó là không thể hiện được bộ font nhiều chữ, hay có chữ phức tạp, và không thể hiện được chữ có độ rộng biến thiên (proportional font). Còn kiểu render trong thời gian thực chỉ mới nở rộ từ thời Super Famicom, thường được dùng trong những game nhiều chữ như dòng Fire Emblem hay Final Fantasy. Kiểu render trong thời gian thực có ưu điểm là có thể vẽ chữ với độ phức tạp cao, do đó thể hiện được kiểu chữ có độ rộng biến thiên, ít tốn không gian Vram (vì nó không chứa sẵn bộ font trong Vram, mà chứa dữ liệu font trong Rom) nhưng lại có nhược điểm là đòi hỏi xử lý, tính toán khá nhiều.

Zelda 3 là game dùng kiểu render trong thời gian thực, bởi 3 bộ chữ được dùng trong game này là Hiragana, Katakana và Kanji nếu tính sơ sơ cũng vài trăm chữ, không thể chứa hết trong 64KB Vram được. Ngoài ra thì Vram còn chứa nhiều dữ liệu hình ảnh khác, không chỉ mỗi font chữ. Cho nên việc Zelda 3 sử dụng kiểu render trong thời gian thực là điều dễ hiểu. Tuy nhiên có một điểm đáng tiếc là font chữ trong game không phải kiểu chữ có độ rộng biến thiên. Chữ có độ rộng cố định chữ phù hợp với các ngôn ngữ có chữ vuông như Hàn, Nhật, Tàu mà thôi. Còn trong văn bản của các thứ tiếng dùng mẫu tự La Tinh thì đây là một điều cực kỳ khó chịu khi đọc. Thử tưởng tượng bạn sẽ cảm thấy như thế nào nếu chữ "i" có cùng độ rộng với chữ "w"? Do vậy ta cần phải viết thêm ít code để hiển thị kiểu chữ có độ rộng biến thiên.

Video bên dưới đây minh họa cho khái niệm render chữ trong thời gian thực.

Muốn viết thêm code để biến font chữ có độ rộng cố định thành font chữ có độ rộng biến thiên thì đầu tiên cần phải tìm được routine xử lý text. Bằng công cụ Tile Viwer và Tilemap Viewer của Mesen, ta xác định được đoạn thoại có tileset (phần hình ảnh của font chữ) được ghi vào địa chỉ Vram $F800, còn Tilemap (cách bố trí các mảnh tileset để tạo thành hình ảnh có nghĩa, cụ thể ở đây là tạo thành con chữ) được ghi vào Vram $C4CC.

Đặt write-breakpoint tại địa chỉ Vram $C4CC thì ta tìm được routine ghi dữ liệu tilemap vào Vram như dưới đây.

Đây cũng là routine được dùng khá nhiều để chuyển một số thành phần đồ họa vào Vram trong kỳ Nmi, trong đó có dữ liệu tilemap của phần text và phần khung thoại. Chức năng của routine này là ghi dữ liệu tại địa chỉ $7E1000 của Workram vào Vram. Cú pháp của routine này gồm: 16 bit địa chỉ Vram cần ghi, theo sau là tổng số byte cần ghi vào Vram - 1 byte, tiếp theo là đoạn dữ liệu cần ghi, kết thúc routine bằng $FFFF.

Hình ảnh bên trên là một đoạn dữ liệu được ghi vào Vram trong kỳ Nmi thông qua routine mà ta đã xác định bên trên. $6266 là địa chỉ Vram cần ghi ($C4CC), $0027 là kích thước của dữ liệu cần ghi (0x28 byte), theo sau đó là 0x28 byte dữ liệu cần được ghi vào Vram: 0x8039, 0x8139,.... Hết chuỗi này lại là địa chỉ Vram cần ghi: $6286, sau đó lại là 0x28 byte chỉ kích thước, nối tiếp là dữ liệu cần ghi: 0x9439, 0x9539,.... Chu trình này lặp lại cho tới khi đụng 2 byte 0xFFFF ở $7E1192.

Như vậy, muốn ghi bất kỳ dữ liệu nào vào Vram, ghi vào bất cứ vị trí nào trên màn hình thì chỉ cần ghi giá trị tương ứng vào địa chỉ $7E1002 trở đi.

Tương tự, khi đặt wite-breakpoint tại địa chỉ $F800 trong Vram thì ta tìm được routine ghi tileset của bộ font vào Vram. Routine ở địa chỉ $008CE4.


REP #$10

LDA #$80

STA $2115

LDX #$1801

STX $4300

LDY #$7C00

STY $2116

LDY #$0000

STY $4302

LDA #$7F

STA $4304

LDX #$0780

STX $4305

LDA #$01

STA $420B

SEP #$10

STZ $0710

RTS


Tương tự routine ghi dữ liệu tilemap, routine ghi tileset này cũng chỉ được thực hiện trong kỳ Nmi. Tuy nhiên, có điểm khác biệt là nếu routine trước ghi từng byte dữ liệu vào Vram (cách thức từ thời Famicom) thì ở routine này, một nhóm nhiều byte (cụ thể ở đây là 0x780 byte) được chuyển vào Vram với tốc độ cao thông qua kênh DMA #0. Dữ liệu được ghi từ địa chỉ Workram $7F0000. Thử dump CPU memory rồi xem bằng phần mềm YY-CHR thì có thể thấy hình ảnh của font chữ ở địa chỉ $7F0000.

Như vậy, ta cần truy lùng tiếp dữ liệu được ghi vào $7F0000 từ đâu, với cách thức như thế nào. Bằng cách đặt tiếp write-breakpoint ở $7F0000 thì sẽ tìm được routine dưới đây khi nhân vật nói chuyện.

Đây là routine chính để đọc các byte đại diện cho từng ký tự trong chuỗi text, sau đó liên kết từng byte đó với dữ liệu đồ họa của từng chữ cái (font) trong Rom, thực hiện các bước tính toán để ghi hình ảnh của từng chữ cái vào vùng Ram $7E0000, sau đó DMA cả khối dữ liệu hình ảnh này vào địa chỉ Vram $F800 trong giai đoạn Nmi. $1CD9 là địa chỉ chứa giá trị index trong chuỗi hội thoại. Chuỗi này bắt đầu ở địa chỉ Workram $7F1200, kết thúc bằng byte 0xFF. Như vậy, bằng cách thay đổi giá trị các byte trong vùng Workram từ $7F1200 trở đi, quan sát thay đổi tương ứng trên màn hình thì ta có thể lập được table thể hiện giá trị byte nào tương ứng với ký tự nào.

Như trên thì thấy mỗi ký tự trong game này chiếm 2 byte, chẳng hạn như 0x0000 tương đương với , còn 0x0001 tương đương với . Tuy nhiên, thông thường thì đại đa số game trong thời Super Famicom đều dùng 1 byte để biểu diễn cho 1 ký tự Kana, còn đến khi biển diễn Kanji, do số lượng của chúng vượt quá phạm vi biểu diễn của 1 byte (0~255) nên người ta mới dùng tới kiểu 2 byte để biểu diễn. Còn ở đây lại dùng 2 byte để biểu diễn 1 ký tự Kana, thật là kỳ lạ.

Nếu tiếp tục đặt write-breakpoint ở $7F1200 để xác định những giá trị này được ghi vào vùng Ram này từ đâu, như thế nào thì ta sẽ thấy giá trị ID của mỗi chữ Kana trong Rom chỉ chiếm 1 byte mà thôi. Chỉ đến khi được ghi vào vùng Workram $7F1200 thì mới trở thành 2 byte. Mục đích của việc này là để đơn giản hóa quá trình đọc text, bởi vì các control code như code xuống dòng, code đổi màu chữ, code đổi tốc độ ghi chữ, code hiệu ứng âm thanh,... đều chiếm 2 byte, nếu code ký tự chiếm 1 byte thì việc xử lý sẽ phức tạp hơn. Vì vậy Nintendō đã biến tất cả code ký tự Kana từ 1 byte trong Rom thành 2 byte trong Ram cho đồng bộ với control code.

Sau khi đã xác định được table thì ta có thể xem vị trí của text trong Rom một cách trực quang với phần mềm Windhex, hoặc cũng có thể dump text với phần mềm Cartographer.

Cũng bằng cách đặt write-breakpoint tại địa chỉ Workram $7F1200, ta có thể xác định được text được ghi từ Rom vào Workram qua pointer ở $04.


Qua routine trên thì thấy được pointer chỉ đến một đoạn text xuất phát từ $04, mà giá trị tại đây lại xuất phát từ địa chỉ Ram $7E1CF0. Mọi đoạn hội thoại trong game đều xuất phát từ đây. Nếu muốn hiển thị đoạn hội thoại đầu tiên lên màn hình thì chỉ cần ghi giá trị 0x0000 vào địa chỉ Ram này, còn ghi giá trị 0x0001 để hiển thị đoạn thoại thứ hai.

Tuy nhiên, qua đoạn code trên thì cũng thấy được là pointer chỉ đến địa chỉ của text là 16 bit, nghĩa là toàn bộ lời thoại trong game phải được gói gọn trong một bank. Zelda 3 là game có kiểu mapping Lorom, tức một bank có dung lượng 32KB, trong khoảng $8000 đến $FFFF. Dung lượng 32KB là quá ít ỏi đối với một bản dịch tiếng Việt, bởi với cùng một câu thoại thì số lượng ký tự trong tiếng Việt thường nhiều hơn tiếng Nhật, chiếm nhiều byte hơn. Do vậy, với kiểu pointer 16 bit này thì không đủ để chứa hết lời thoại của bản dịch nên ở đây cần mở rộng kích thước pointer lên 24 bit, để có thể chỉ đến text ở bất kỳ vị trí nào trong Rom. 


REP #$30

LDA $1CF0

PHA

ASL

CLC

ADC $01,s

TAX

PLA

LDA pointer_table,x

STA $04

INX

SEP #$20

LDA pointer_table,x

STA $06

Chỉ với đoạn code đơn giản trên là ta đã đổi pointer hội thoại thành 2 byte sang 3 byte. Dĩ nhiên cần phải thay đổi cả dữ liệu pointer và dữ liệu text trong Rom. Có thể dễ dàng làm việc này với phần mềm chèn text có hỗ trợ pointer như Atlas.

Tiếp theo là đến phần chính của vấn đề, làm thế nào để biến font có độ rộng cố định thành font chữ có độ rộng biến thiên? Tất cả đều phụ thuộc vào quá trình xử lý font mà ta sẽ viết sau đây.

1) Tạo một bộ font chữ cái La Tinh có dấu tiếng Việt. Có thể dùng YY-CHR hoặc các phần mềm xử lý ảnh dạng pixel, sau đó convert thành định dạng 2bpp. Bởi vì text trong Zelda 3 được hiển thị ở mode 1, trong đó lớp Background 3 được dùng để hiển thị text có độ sâu màu 2bpp, tức mỗi tile có thể hiển thị tối đa 4 màu.

2) Tại routine đọc ký tự text ở vùng Workram $7F1200, đọc từng giá trị tại chuỗi này. Nếu giá trị là control code thì không cần can thiệp, nếu giá trị là ký tự Kana thì xử lý tiếp các bước đánh số bên dưới.

REP #$30

LDX $1CD9

LDA $7F1200,x

CMP #$FF00

BEQ +

SEP #$30

LDA #$0C

STA $012F

REP #$30

+

LDA $1CDD

ASL

TAX

SEP #$20

LDA $1CDC //tile pro

STA $1301,x

STA $1303,x

STA $1305,x

STA $1329,x

STA $132B,x

STA $132D,x

JSR write_text_pos

3) Render ký tự 

Đây là phần phức tạp nhất của toàn bộ quá trình, gồm các bước như dưới đây.

a. Đầu tiên là liên kết từ giá trị ID của mỗi ký tự đến phần tileset tương ứng của ký tự đó trong bảng font. Cách tạo liên kết phụ thuộc vào cách mà ta sắp xếp dữ liệu font trong Rom. Có khá nhiều cách xếp, nhưng phổ biến nhất là 2 cách xếp như dưới đây.

Ở hình trên, dữ liệu của chữ "A" được sắp xếp mặc định theo chế độ hiển thị của Super Famicom, trong khi những ký tự còn lại được sếp theo kiểu của máy Famicom. Với bất kỳ kiểu xếp dữ liệu nào thì cũng cần phải chọn kiểu xem dữ liệu tương ứng với nó trong YY-CHR để cho kết quả hiển thị đúng. Tương tự, cách xếp dữ liệu cũng ảnh hưởng tới cách mà ta vẽ chữ (render) ở bước sau, cũng như cách xếp tilemap. Chẳng hạn, bên trên là bộ font có kích thước 16 x 16 pixel cho mỗi ký tự, đủ để ta vẽ dấu tiếng Việt. Nếu coi phần bên trên của chữ "A" có địa chỉ tilemap là $00 thì đối với kiểu xếp mặc định, tile nửa bên dưới sẽ có địa chỉ là $10, còn tile thứ hai, bên trên của chữ "A" có địa chỉ là $01. Còn nếu xếp theo kiểu Famicom 16 thì tile nửa bên dưới có địa chỉ là $01, còn tile thứ hai bên trên có địa chỉ là $02,... Có thể vẽ tùy ý bằng YY-CHR rồi quan sát sự thay đổi của quỹ đạo byte bằng Hex editor để nắm quy luật.

Dưới đây là một ví dụ về cách liên kết từ giá trị ID của mỗi ký tự đến tileset của ký tự đó trong trường hợp xếp dữ liệu tileset theo kiểu Famicom 16.

REP #$20

LDA $7F1200,x

XBA

AND #$00FF

CMP #$00E8

BNE +

xử lý abcxyz...

+

ASL #6

TAX

LDY #$0000

SEP #$20

-

LDA font0,x

XBA

LDA font0+0x20,x

JSR shift_pix

ORA {char_buffer}+0x20,y

STA {char_buffer}+0x20,y

XBA

ORA {char_buffer},y

STA {char_buffer},y

LDA font0+0x10,x

XBA

LDA font0+0x30,x

JSR shift_pix

ORA {char_buffer}+0x30,y

STA {char_buffer}+0x30,y

XBA

ORA {char_buffer}+0x10,y

STA {char_buffer}+0x10,y

INY

INX

DEC {buffer1}

BNE -

PLB

REP #$20

LDA #$BFC0

STA $04

LDA #$007E

STA $06

RTS

Như vậy, chỉ cần shift giá trị của ký tự sang trái 6 lần, hay nói cách khác là nhân đôi giá trị đó 6 lần thì sẽ tìm được địa chỉ bắt đầu của phần tileset tương ứng. Tại sao lại như vậy? Qua hình minh họa về cách xếp dữ liệu ở trên, ta thấy tile liền kề bên phải một tile bất kỳ có giá trị = giá trị của tile đó + 2. Trong khi đó, do bộ font có độ sâu màu là 2bpp nên mỗi tile có dung lượng là 0x10 byte. Cụ thể:

Mỗi tile có diện tích 8 x 8 pixel = 0x40 pixel

Ở chế độ 2bpp, mỗi pixel chiếm 2 bit nên mỗi tile có dung lượng 2 x 40 = 0x80 pixel

Trong khi đó, nhóm 8 bit gọp thành 1 byte nên mỗi tile có dung lượng: 80/8 = 0x010 byte

Tuy nhiên, bộ font của ta ở đây là 16 x 16 pixel, tức gồm 4 mảnh tile 8 x 8 ghép lại với nhau, nên ký tự liền sau đó cách 0x40 byte so với ký tự trước đó.

Như vậy, giả dụ bộ font bắt đầu tại địa chỉ $0A8000 trong Rom, và chữ cái khởi đầu là chữ "A" thì chữ "A" có địa chỉ ở $0A8000, còn chữ "B" liền kề sau đó có địa chỉ $0A8080. Nếu coi "A" có ID là 00, thì "B" liền sau đó sẽ có ID là 02 thì địa chỉ của B là: 02 x 2 x 2 x 2 x 2 x 2 x 2 = $80. Dĩ nhiên $80 chỉ mới là giá trị Index/ giá trị tương đối để dẫn đến địa chỉ tuyệt đối của hình ảnh của "B". Để tính được địa chỉ tuyệt đối của "B" thì cần cộng với địa chỉ bắt đầu của bộ font: $80 + $0A8000 = $0A8080.

b. Tiếp theo, sau khi đã tạo được liên kết từ giá trị ID của mỗi ký tự tới tileset tương ứng của nó, việc cần làm là biến hình ảnh tileset trong Rom từ dạng mỗi tile mỗi chữ thành dạng mỗi tile có thể chứa nhiều chữ. Đây là mục đích chính của toàn bộ quá trình, nhằm tạo ra bộ font chữ có độ rộng biến thiên tùy vào từng ký tự.

Bên trái là hình ảnh tileset của bộ chữ được chứa trong Rom, mỗi ký tự là một tile. Nếu không xử lý gì và để nguyên như thế này mà ghi vào Vram thì từ nào có bao nhiêu ký tự thì sẽ tốn bấy nhiêu tile, không đẹp về mặt thẩm mỹ và cũng không hiệu quả về mặt không gian. Còn bên phải là hình ảnh bộ chữ sau khi được xử lý, một tile có thể chứa nhiều ký tự, mỗi ký tự chỉ chiếm một khoảng không gian vừa đủ với độ rộng của nó.

Để làm được việc này thì ta cần đến phép shift bit về bên phải. Nói đại khái thì sau khi đọc dữ liệu hình ảnh của ký tự, ta sẽ đẩy các bit của dữ liệu đó về bên phải với số lần tương ứng với độ rộng của ký tự đứng trước đó. Chẳng hạn, đối với từ "file" ta có 4 ký tự gồm "f", "i", "l" và "e". Giả định "f" có độ rộng là 5 pixel, "i" có độ rộng 1 pixel, "l" có độ rộng 2 pixel và "e" có độ rộng là 4 pixel. Một tile luôn có độ rộng không đổi là 8 pixel. Do vậy, khi ghi ký tự đầu tiên là "f" trong chuỗi này thì ta shift (đẩy) hình ảnh của "f" về phía phải 0 pixel, tức là không cần shift. Do nó chỉ chiếm 5 pixel trong tile, mà tile có 8 pixel nên vẫn còn lại 3 pixel trống về phía phải. Ký tự tiếp theo là "i" chỉ chiếm 1 pixel nên hoàn toàn chứa vừa trong phần còn lại của tile thứ nhất. Lúc này, khi ghi hình ảnh của "i" thì phải đẩy nó về phía phải 5 pixel để nó đứng liền sau hình ảnh của ký tự "f" trong tile. Các ký tự còn lại trong chuỗi cũng được xử lý tương tự.

LDA font0,x

XBA

LDA font0+0x20,x

   JSR shift_pix

Điểm lưu ý: đọc 1 byte của tile thứ nhất và 1 byte của tile liền kề sau đó trước khi shift. Mỗi lần shift sang phải, giá trị của byte giảm đi một nửa.

shift_pix:

REP #$20

CMP #$0000

BEQ end_shift_pix

PHX

LDX {shift}

CPX #$0000

BEQ end_shift_pix-1

-

LSR

DEX

BNE -

PLX

end_shift_pix:

SEP #$20

RTS

Routine đẩy bit về bên phải đơn giản như bên trên. Khi giá trị shift là 00 thì thoát khỏi routine ngay lập tức. Hình bên dưới thể hiện mối tương quan giữa vị trí của pixel với giá trị byte của nó.

c. Sau khi đã shift pixel về bên phải, việc cần làm tiếp theo là ghi giá trị đó vào một vùng Ram đệm A có kích thước 0x40 byte, đủ để chứa 4 tile của 1 ký tự. Tuy nhiên, cần thực hiện phép luận lý ORA với vùng Ram đệm đó trước khi ghi vào nó, để chồng lớp hình ảnh của ký tự ta vừa shift vào lớp hình ảnh của ký tự trước đó. Việc lồng ghép này lợi dụng tính chất của phép ORA: 0 ORA 1 = 1, 1 ORA 1 = 1, 0 ORA = 0. Các biến số bên dưới: {char_buffer} là vùng Ram đệm, dùng để chứa hình ảnh của nửa bên trái của ký tự, còn {char_buffer}+0x30 là vùng đệm chứa nửa bên phải của ký tự. 

-

LDA font0,x

XBA

LDA font0+0x20,x

JSR shift_pix

ORA {char_buffer}+0x20,y

STA {char_buffer}+0x20,y

XBA

ORA {char_buffer},y

STA {char_buffer},y

LDA font0+0x10,x

XBA

LDA font0+0x30,x

JSR shift_pix

ORA {char_buffer}+0x30,y

STA {char_buffer}+0x30,y

XBA

ORA {char_buffer}+0x10,y

STA {char_buffer}+0x10,y

INY

INX

DEC {buffer1}

BNE -

4) Vẽ phần vừa render vào vùng Ram đệm khác (vùng đệm B), nơi mà ta sẽ DMA toàn bộ hình ảnh vào Vram. Vùng đệm này phải có kích thước đủ lớn để chứa đủ tất cả các tile chữ trong hộp thoại. Phần này chỉ đơn thuần là copy lại toàn bộ 0x40 byte của vùng đệm A vào vùng đệm B mà không phải tính toán hay xử lý gì khác. Ở phần code bên dưới, $04 là địa chỉ vùng đệm A, còn $7F8000 là vùng đệm B.

-

LDA [$04],y

ORA $7F8000,x

STA $7F8000,x

INY #2

INX #2

CPY #$0040

BNE -

5) Tạo liên kết từ giá trị ID của ký tự đến giá trị độ rộng của ký tự đó. Cách thức không khác với cách tạo liên kết đến tileset của bộ font ở phần trên, nhưng thay vì dẫn đến địa chỉ hình ảnh của font trong Rom thì ở đây dẫn đến địa chỉ của table chứa độ rộng của các ký tự. Có thể chứa table này trong Rom hoặc Ram tùy ý.

LDX $1CD9

LDA $7F1200,x

XBA

AND #$00FF

TAX

LDA font0_width,x

AND #$00FF

STA {char_haba}

Biến số {char_haba} là nơi chứa độ rộng của ký tự. Sở dĩ cần phải tính độ rộng của mỗi ký tự vừa ghi vào vùng đệm là để tính số pixel còn thừa lại của tile.

6) Sau khi tính độ rộng của ký tự vừa render, ta cần tính số lần shift sang phải cho ký tự tiếp theo. Ý niệm ở đây rất đơn giản. Nếu ký tự vừa rồi, tính cả số pixel đã shift của nó không dùng hết 8 pixel thì vẫn giữ nguyên tile đó cho lần vẽ tiếp theo, và ở ký tự tiếp theo phải shift sang phải với số lần bằng với số pixel còn thừa lại trong tile. Còn nếu tổng độ rộng của ký tự vừa rồi và số lần shift của nó có độ rộng từ 8 pixel trở lên thì chuyển sang tile tiếp theo.

LDA {shift}

CLC

ADC {char_haba}

CMP #$0008

BCS +

AND #$0007

STA {shift}

RTS

+

CMP #$0010

BCS +

SEC

SBC #$0008

AND #$0007

STA {shift}

INC {tile_no}

RTS


7) Bước cuối cùng của quá trình render một ký tự là chuyển tileset của nó vào Vram. Cách tốt nhất để làm việc này là thực hiện một cách tự động trong thời gian V-blank. Sau khi tính toán xong mọi thứ cần thiết thì chỉ cần ghi địa chỉ Vram cần chuyển dữ liệu, kích thước dữ liệu, địa chỉ dữ liệu và byte kích hoạt chức năng tự động DMA trong kỳ Vram. Đoạn code dưới đây ghi các thông số cần thiết để DMA 0x800 byte hình ảnh các chữ cái ở $7F8000 vào địa chỉ Vram $F800.

REP #$20

LDA #$F800>>1

STA {vram_address}

LDA #$0800

STA {dma_size}

LDA #$8000

STA {dma_adress}

SEP #$20

LDA #$7F

STA {dma_bank}

INC {trigger}


Còn trong routine NMI, chỉ cần đặt một chức năng như dưới đây là có thể tự động DMA vào địa chỉ Vram đã chỉ định.

SEP #$20

LDA {trigger}

BNE +

RTS

+

REP #$20

LDA {vram_address}

STA $2116

LDA {dma_size}

STA $4305

LDA {dma_adress}

STA $4302

LDA #$1801

STA $4300

SEP #$20

LDA #$80

STA $2115

LDA {dma_bank}

STA $4304

LDA #$01

STA $420B

STZ {trigger}

RTS

Như vậy là kết thúc toàn bộ quá trình biến bộ font từ kiểu có độ rộng cố định thành kiểu có độ rộng biến thiên.

Theo engine hiển thị nguyên bản của game thì phần khung thoại trong suốt, không có màu khiến việc đọc chữ đôi khi gặp trở ngại do hình ảnh của cảnh nền chen lấn với phần chữ. Nếu ta đặt nền khung thoại có màu đục thì sẽ khắc phục được tình trạng trên, nhưng nếu nền khung thoại vừa có màu, mà vẫn giữ được độ trong suốt để thấy cảnh nền bên dưới thì sẽ ra sao?

Ý niệm đằng sau kỹ thuật tạo màu nền trong suốt cho khung thoại là sử dụng tính năng cộng trừ màu của máy Super Famicom kết hợp với chức năng IRQ. Ta tạo ra lớp màu cố định bằng cách ghi 3 giá trị màu R, G, B lần lượt vào register $2132 trong khi IRQ đang được kích hoạt. Tuy nhiên cũng có vài điểm cần lưu ý như dưới đây.

Đầu tiên, khi bật khung thoại thì cần thiết lập IRQ sao cho nó chỉ được kích hoạt khi tia laser của Tivi chạm đến scanline bằng với tung độ trên của khung thoại, và IRQ phải được giải trừ sau khi tia laser này chạm đến tung độ dưới của khung thoại. Trong quá trình IRQ được kích hoạt, nó sẽ quét toàn bộ khung thoại với thuật toán cộng trừ màu mà ta sẽ thiết lập để tạo ra vùng màu không vượt quá tung độ trên và tung độ dưới của khung thoại.

Thứ đến là thiết lập giới hạn trái, giới hạn phải của 2 cửa sổ qua các register $2126 ~ $2129, rồi thiết lập giá trị window mask cho register $2125 để vùng màu không bị lan ra khỏi hoành độ trái, hoành độ phải của khung thoại. 

LDA {color1}

STA $2132

LDA {color2}

STA $2132

LDA {color3}

STA $2132

LDA #$20

STA $2130

LDA {color_math}

STA $2131

LDA {window1_hidari}

STA $2126

LDA {window1_migi}

STA $2127

LDA {window2_hidari}

STA $2128

LDA {window2_migi}

STA $2129

LDA #$C0

STA $2125

LDA #$04

STA $212B

RTS

Trên đây là toàn bộ phần khẩu quyết hướng dẫn tạo một bản dịch tiếng Việt cho Zelda 3 dành cho người đã có nền tảng về cấu trúc phần cứng của máy Super Famicom, cũng như nền tảng về ngôn ngữ phần mềm của nó.