Chủ Nhật, 23 tháng 4, 2017

[ASM]Hướng dẫn dịch thuật Captain Tsubasa 3

Bài viết: Asm65816

Nhiều bạn PM hỏi tôi cách dịch game, tôi trả lời cứ đọc mấy bài hướng dẫn tôi viết. Và câu hỏi mà tôi hay nhận được nhất là:

- Giúp em cách gõ tiếng Việt trong game
- Nó (game) không hỗ trợ Unicode, em gõ tiếng Việt nó toàn ra ký tự gì đâu
- ......

Đọc bản pdf tại đây (click)

Đại loại là nhiều vấn đề liên quan tới việc hiển thị dấu tiếng Việt trong game.

Bài viết này tôi mượn Captain Tsubasa trên SFC để minh họa cụ thể nhất quá trình dịch một game từ tiếng ABCXYZ sang tiếng Việt như thế nào, áp dụng những nguyên tắc cơ bản mà tôi từng giới thiệu trong bài hướng dẫn dịch game console/PC và nhiều bài khác. Bài này cũng sẽ đi sâu vào phân tích ở góc độ Assembly để giúp người đọc hiểu cặn kẽ hơn những gì có trong game.





Chuẩn bị

Để theo được bài viết này, bạn cần chuẩn bị một số thứ tối thiểu như sau:

1. Kiến thức về hệ nhị phân (Binary, Bin) và hệ thập lục (Hexadecimal, Hex), kiến thức về bit và byte. Không nhắc lại ở đây vì những bài viết về hai hệ số này đã tràn ngập Google rồi, tôi không tốn thời gian gõ lại làm gì.

2. Một chút thời gian rãnh. Không nhiều lắm, nhưng đủ để đọc hết bài viết này và làm theo. Có thể chia thành nhiều kỳ, số kỳ không hạn chế, tùy vào độ rãnh (háng) của bạn.

3 Giả lập kiêm Debugger để chạy game SNES. Trên Net có kha khá, nhưng khuyên dùng Snes9X. Tải về tại đây (click vào). Cũng có thể dùng Bsnes plus.

4. Rom Captain Tsubasa III, tiếng Nhật.

5. Trình Hex Editor. Có vô vàn, nhưng tôi hay dùng Stirling vì sự tiện dụng của nó. Tải bản tiếng Việt của Stirling tại đây (click vào). Ngoài ra cũng cần dùng Windhex của tác giả Bongo, chủ yếu để đọc text trong Rom. Tải tại Google.


6. Trình assembler cho Snes: có kha khá, nhưng tôi dùng Xkas. Google để tải về.

7. Lunar Address, tiện ích chuyển đổi giữa địa chỉ PC và địa chỉ SNES. Tải về tại đây (click vào).

8. Lunar Expand, tiện ích để mở rộng rom SNES.

9. Cần thêm YY-CHR để biên tập font.

10. Search Relative để dò tìm tương đối. Tải tại Google.com

11. Ứng dụng trích xuất text dựa trên table. Có khá nhiều, nhưng tốt nhất là Cartographer. Tải từ Google.com. Nếu có khả năng, hãy tự code.

12. Ứng dụng chèn text vào game dựa trên table. Có khá nhiều, nhưng tốt nhất là Atlas. Tải từ Google.com. Nếu có khả năng, hãy tự code.

12. Cuối cùng, à chưa phải cuối cùng. Một chút kiên nhẫn. Không cần nhiều lắm, chỉ cần đủ để đọc lại bài này từ đầu chí cuối sau khi đọc hết mà vẫn không hiểu gì.

13. Đây mới là cuối cùng. Cần một bộ óc bình thường như cân đường hộp sữa. Không cần cái đầu quá nhanh như Core i7 hay Snapdragon 820, nhưng nếu nhanh được như vậy thì càng tốt. Chỉ cần làm được 5 phép tính cộng trừ nhân chia số có 2 chữ số trong thời gian 1 phút là đủ tiêu chuẩn.


Xác định text



Vì đây là bản dịch, nên dĩ nhiên thứ ta cần quan tâm trên hết chính là text. Nó tồn tại sẵn trong Rom, và khi CPU đọc được nó thì sẽ hiển thị lên màn hình cho bạn thấy. Tuy nhiên text trong Rom không dễ gì thấy được bằng mắt thường, ngay cả khi mở Rom bằng hex editor.

[​IMG]

Chẳng hạn như hình ảnh trên, ngay cả khi biết được địa chỉ chứa text nhưng mở Rom lên, bạn chỉ thấy được những con số. Bởi vì những game thế hệ cũ trên SNES, PSX đều không dùng chuẩn Ascii hay Shift-Jis, Unicode để lưu text. Chúng có kiểu mã hóa của riêng chúng và không game nào giống game nào. Cho nên một trong những bước đầu tiên cần phải làm khi dịch những game cũ kiểu này là xác định kiểu encoding của nó.

Có nhiều cách để xác định và ở đây tôi giới thiệu cách đơn giản nhất.

1. Đầu tiên thử tìm một đoạn text dễ kiếm, có đặc trưng và không bị trùng lặp. Tôi thường chọn những đoạn đầu game hay gần điểm save để dễ dàng kiểm chứng. 


[​IMG]

Việc xác định đặc trưng của một đoạn text phụ thuộc khá nhiều vào cái "sense" (khiếu) của bạn. Khéo thì sẽ chọn được đoạn độc đáo, không trùng lặp với những cụm từ khác để quá trình tìm kiếm được nhanh hơn. Đoạn tôi chọn trong khoanh đỏ khá độc đáo và gần như cả game chỉ có mỗi sự kết hợp này.

2. Mở Rom bằng YY-CHR, xem thử font có bị nén hay mã hóa không. Sau một hồi cuộn lên cuộn xuống cũng tìm được. May thật, bộ font lộ thiên.

[​IMG]

Trong bất kỳ ngôn ngữ nào, các chữ cái cũng đều được sắp xếp theo trật tự nhất định. Nếu trong bảng chữ La Tinh, đó là a, b, c, d,... thì trong bảng chữ cái tiếng Nhật lại là a, i, u, e,.... (あ, い, う,...)
Do vậy, nếu giả định あ có giá trị là 1, thì い đứng ngay sau đó có giá trị là 2, và う là 3,....

Để coi, căn cứ trên trật tự xếp chữ trong bộ font này thì cụm text được khoanh tròn kia là:

6-2-13-41

3. Bật Relative search, gõ tên file đối tượng và thứ tự tương đối của các chữ cái trong đoạn text kia.

[​IMG]

Chỉ có một kết quả tại địa chỉ $3D265 là thỏa mãn điều kiện đặt ra. Nếu việc xác định đoạn text để tìm kiếm ở bước 1. không khéo thì ở đây có thể sẽ có nhiều kết quả, và ta sẽ phải thử lần lượt từng kết quả để biết chính xác.

4. Thử kiểm tra kết quả vừa tìm được. Bật hex editor, tại địa chỉ $3D265 là giá trị 06. Thử đổi thành 09, save Rom và bật giả lập lên...

[​IMG]

Và kiểm tra lại kết quả...

[​IMG]

Ta thấy, thay 06 thành 09 và kết quả hiển thị thay đổi từ chữ か thành chữ け
Vậy có nghĩa là đã tìm đúng. Nếu 09=け thì chữ tiếp theo, こ sẽ là 0A.

5. Lập file table. Về cơ bản, đó là file văn bản có phần mở rộng .tbl.
Áp dụng cái logic trật tự trên để lập table cho cả bộ font.

Như thế này... 


[​IMG]

Giờ bật Windhex, load Rom và thử so sánh khi có và không load kèm table.


[​IMG]

Rõ ràng, với file table thì ta dễ dàng xác định được text trong Hex editor.

Bây giờ tôi hướng dẫn một cách xác định text khác mà không cần dùng đến thủ thuật tìm kiếm tương đối. Cách này áp dụng kiến thức về phần cứng của SNES để tìm.

1.Bật Debugger Geiger, chạy game đến một đoạn text ở đầu game cho dễ xác định, chẳng hạn như cảnh này.


[​IMG]

Hình ảnh hiển thị trên màn hình SNES là kết quả tổng hợp của nhiều lớp (layer) đồ họa, gọi là Background (bối cảnh) chồng chất lên nhau. Các loại giả lập hiện nay đều có chức năng bật, tắt từng BG. Trong Geiger, mặc định khi nhấn phím 1 thì giả lập sẽ bật/tắt BG 1, phím 2 để bật tắt BG 2,.... Kết quả sau khi nhấn thử một loạt phím số thì biết được:

- Cảnh nền (sân vận động) trong ảnh trên nằm ở BG2.
- Phần chữ (text) nằm ở BG3.
- Phình hình ảnh bình luận viên là sprite, và nó nằm ở BG5.

[​IMG]

Còn nhiều thông tin khác nữa, nhưng ta chỉ quan tâm tới BG3 chứa text. Bây giờ tắt mỗi BG2 đi, để bối cảnh không còn hiển thị phần sân vận động.

2. Nhấn vào tab "Show Hex" trên debug console của Geiger để bật Hex editor nội bộ của giả lập này. Tiếp tục, chọn dòng "Viewing" là "Vram" và click vào "Dump" để trích xuất toàn bộ Vram của thời điểm hiện tại. Có thể bật phần dump này bằng Yy-chr và ta thấy được những thành phần đồ họa đang được hiển thị trên màn hình.

[​IMG]

3. Nhấn vào nút "What's used" trên debug console của Geiger để biết các thành phần đồ họa nào đang được sử dụng trong cảnh hiện tại.

[​IMG]

Ta có các thông tin sau:


V-blank NMI enabled
H-DMA 0 [1] 0x038E3C->0x2126 inc continue 0x7E8E3C indirect addressing
H-DMA 1 [1] 0x038E51->0x2128 inc continue 0x7E8E51 indirect addressing
DMA 7 0 0x7E2200->0x2104 Num: 544 inc
VRAM write address: 0x7f1e(nByte), Full Graphic: 0, Address inc: 1
BG0: VOffset:0, HOffset:0, W:32, H:64, TS:8, BA:0x1800, TA:0x5000
BG1: VOffset:0, HOffset:65310, W:64, H:32, TS:8, BA:0x3800, TA:0x6000
BG2: VOffset:0, HOffset:0, W:32, H:32, TS:8, BA:0x7c00, TA:0x7000
BG3: VOffset:0, HOffset:0, W:32, H:32, TS:8, BA:0x0000, TA:0x0000
Main screen (always on): BG1,BG2,OBJ,
Sub-screen (always on): 

Window 1 (0, 0, 13, 00): BG0(O-OR),BG1(O-OR),OBJ(O-OR),
Window 2 (0, 0): 
Fixed colour: 000000

Dòng BG2 chứa các thông số về tileset, tilemap hiển thị ở BG3. Trong đó:

- TS: chính là tile size, kích thước của tile. TS:8 cho biết BG này sử dụng tile có kích thước 8x8 pixel, tức kích thước font chữ của chúng ta.
- BA: chính là tilemap. BA:0x7c00 cho biết tile map của chúng ta bắt đầu tại địa chỉ $7C00 tại Vram (Video ram, phần RAM hiển thị hình ảnh của SNES).
- TA: chính là tileset. TA:0x7000 cho biết tileset của chúng ta bắt đầu tại địa chỉ $7000 trong Vram.

Giải thích ý nghĩa:

- Một đặc điểm của địa chỉ VRam của SNES là nó gấp đôi so với địa chỉ hiển thị. Nếu TA bắt đầu tại $7000 thì có nghĩa là nó bắt đầu tại $7000 x 2 = $E000 trong Vram. BA bắt đầu tại $7C00 thì trong Vram, địa chỉ của nó là $7C00 x 2 = $F800.
- TA (Tileset): hiểu nôm na là một bộ các tile. Còn nhớ khi ta mở Rom bằng YY-chr để xem font? Mỗi một chữ cái là một tile, và nguyên cả bộ font đó là một tileset. Vốn nó là một phần dữ liệu trong Rom và được tải vào trong Vram để hiển thị.

Đây là tileset trong Rom, bắt đầu tại $34800.

[​IMG]



Còn đây là tileset khi được tải vào Vram. Xem bằng cách dùng Yy-chr để mở file dump của Vram lúc nãy. Ta thấy địa chỉ của tileset bắt đầu ở $E000.

[​IMG]

- BA (Tilemap) được hiểu nôm na là một bản đồ phân bố, bố trí các tile trên màn hình. Màn hình SNES thể hiện được 32 tile trong một dòng, mỗi tile có kích thước 8 pixel theo bề ngang. Do vậy, bề ngang của màn hình SNES có độ phân giải: 32 x 8 = 256 điểm ảnh. Khi chụp ảnh màn hình giả lập, bạn sẽ thấy kích thước to hơn 256 là vì giả lập có chức năng upscale. Nếu bỏ chức năng này đi thì hình ảnh hiển thị sẽ thu nhỏ về 256 pixel.
- Mỗi một tile được thể hiện bằng một byte, đi kèm với một byte khác thể hiện tính chất của tile đó. Do vậy, một tile trong Vram chiếm 2 byte, và một dòng trên màn hình SNES là 64 byte.
- Ta đã biết BA của cảnh hiện tại bắt đầu tại $F800 trong Vram. Điều này có nghĩa, các tile được dùng để vẽ khắp màn hình từ góc trên cùng bên trái, xuống dưới cùng bên phải màn hình. Vị trí góc trên bên trái bắt đầu tại $F800. Bắt đầu từ đây, ta thấy một chuỗi giá trị 0000000000000....
Thử thay đổi byte đầu tiên (tại $F800) thành 01, và ta được kết quả sau.

[​IMG]

Nhìn lên góc trên, bên trái, ta thấy xuất hiện chữ あ. Như vậy, chữ này có giá trị là 01. Tiếp tục thử các giá trị khác, bằng cách này ta xây dựng được table mà không cần phải dò tìm tương đối. Cách này cho kết quả chuẩn xác hơn, nhanh chóng hơn.


Trích text

Sau khi lập được table, dễ dàng xác định được text nằm đâu trong Rom. Từ đó tiến hành dump (xuất) text từ Rom ra file văn bản để dịch thuật.
Mục đích của việc dump text gồm:

1. Kiểm soát code nằm lẫn trong khối text
2. Phục vụ cho quá trình chèn text sau khi dịch

Ngoài ra nó còn có tác dụng phụ là giúp việc đọc được tốt hơn so với khi đọc bằng Hex editor.
Để trích text, nếu có khả năng thì bạn nên tự code một phần mềm chuyên dụng cho việc này. Tuy nhiên cũng có khá nhiều phần mềm có sẵn phục vụ cho mục đích này, và trong số đó thì Cartographer là phần mềm tốt nhất, có thể dump text với nhiều kiểu khác nhau như dump trực tiếp khối text, hay gián tiếp qua pointer. Và tất cả mọi phần mềm dump text đều làm việc trên một nguyên tắc chung, là quy đổi mã code thập lục thành chữ cái tương ứng mà ta quy định trong table.
Giả dụ, trong table đã lập ở bước trên, ta có 01=あ thì khi gặp phải hex code 01 trong đoạn text cho sẵn, phần mềm sẽ xuất ra ký tự あ.

Cách sử dụng Cartographer được hướng dẫn cụ thể trong file readme đi kèm. Tạo một file.bat để chung với thư mục Cartographer.exe và Rom, có nội dung như sau:


cartographer Tsubasa3.smc dumpscript.txt abc -s

Trong đó .smc là tên Rom đối tượng, .txt là tên file lệnh điều khiển Cartographer, abc là file text xuất ra (mặc định là .txt), -s/-m là tham số cho phép gộp nhiều kết quả vào một file hay tách thành nhiều file.

Đây là kết quả dump một đoạn.

[​IMG]


Đọc bản pdf tại đây (click)

V. Sửa font


Sau khi xuất text, ta có thể tiến hành dịch luôn. Nhưng có chèn bản dịch vào game thì cũng chưa thể xem là hoàn thành, vì còn rất nhiều việc phải làm. Một trong số đó là sửa bộ font. Bởi lẽ font chữ lúc này đang là chữ Nhật, cần phải sửa lại thành font La Tinh để hiển thị tiếng Việt.

Có khá nhiều phần mềm dành cho dân dịch game, có thể sửa được font chữ. Yy-char là một trong những phần mềm cho phép chỉnh sửa font chữ và hình ảnh trong game SNES.

Font chữ cho máy SNES thuộc loại 1bpp (1 bit per pixel/1 bit plane, mỗi 1 bit thể hiện 1 điểm ảnh) hoặc 2bpp. Captain Tsubasa III dùng font 1bpp, tức font đơn sắc và không có đổ bóng.

[​IMG]

Về kích thước, font chữ game SNES chỉ thuộc một trong các kiểu sau:
1. 8x8 pixel
2. 8x16 pixel
3. 16x16 pixel
4. Độ rộng thay đổi theo từng chữ

Đối với Captain Tsubasa III, game này dùng kiểu 8x8 pixel. Mỗi một chữ cái nằm gọn trong khung với kích thước 8x8 điểm ảnh. Ta có thể vẽ các chữ cái La Tinh dễ dàng trong ô vuông 64 pixel này, nhưng dường như không thể vẽ thêm dấu tiếng Việt, nhất là cái chữ có dấu mũ như â, ô, ê. Cách giải quyết vấn đề này sẽ bàn ở phần sau.

[​IMG]


Sau khi chỉnh sửa font chữ, dễ dàng nhận thấy rằng dù font đã được thay đổi nhưng trật tự của các chữ cái vẫn còn là của tiếng Nhật, và hiện tượng này được gọi là "tiếng mọi".

VI. Phân tích cơ chế bỏ dấu

Sau khi đã sửa bộ font và dump text, về cơ bản thì đã có thể tiến hành dịch. Nhưng trước đó cần phải giải quyết vấn đề dấu tiếng Việt như đã chỉ ra trước đó. Ta biết, Tsubasa 3 dùng font 8x8 pixel cho mỗi chữ và rất khó, nếu không muốn nói là không thể, vẽ thêm các dấu sắc, huyền, hỏi ngã, mũ, mốc,.... phía trên đầu các nguyên âm.
Chả lẽ bó tay....?
Thử quan sát, thấy trong tiếng Nhật, có dấu trọc âm (゛) và bán trọc âm (゜) đó. Các dấu này cũng nằm ngay bên trên chữ cái tiếng Nhật. Vậy chỉ cần áp dụng, biến 2 dấu này thành dấu tiếng Việt là được. Chẳng hạn như chữ S xuất hiện trên đầu chữ kana dưới đây.

[​IMG]

Nhưng gốc gác nó vốn chỉ có 2 dấu, trong khi tiếng Việt thì nhiều hơn (sắc, huyền, hỏi ngã, mũ, mốc, sắc mũ, huyền mũ, hỏi mũ, ngã mũ, sắc móc, huyền móc, hỏi móc, ngả móc) thì phải làm sao?

Tới đây phải dùng tới kiến thức Assembly để giải quyết, vì các thủ thuật hack rom thông thường không làm gì được.

1. Tìm một đoạn text có chứa chữ cái mang dấu trọc âm hay bán trọc âm. Xác suất xuất hiện của dấu trọc âm trong tiếng Nhật khoảng 80% trong một văn bản bình thường.
2. Xác định địa chỉ của chữ mang dấu trọc âm/bán trọc âm đó.
3. Chuyển đổi địa chỉ của dấu trọc âm đó sang địa chỉ SNES. Vì Tsubasa 3 là Lorom nên địa chỉ $00 mà ta thấy trong hex editor tương đương với $8000 trong bộ nhớ SNES. Có thể dùng Lunar Address để chuyển đổi.
4. Đặt Read break point địa chỉ đó trong debugger. Khi game đọc tới địa chỉ để hiển thị chữ cái đó thì nó sẽ dừng, click liên tục vào Step Into để đi vào khối code xử lý đó.

[​IMG]



Nhìn hình trên, ta thấy 9B và 9C là giá trị của 2 dấu trọc âm, bán trọc âm trong game. Khi thay bằng giá trị khác vào đoạn code này thì game sẽ hiển thị chữ cái có giá trị mà ta thay lên đầu của chữ Kana. Chẳng hạn, trong hình trên, tôi thay LDY #$9C (9C là giá trị của dấu bán trọc âm) tại dòng lệnh $0085DC bằng LDY #$93 (93 là giá trị của chữ S) thì dấu bán trọc âm xuất hiện trên đầu chữ ハ đã biến thành chữ S.
Tuy cơ chế đã có sẵn, nhưng mới chỉ có 2 dấu, trong khi tiếng Việt cần tới 14 dấu xuất hiện trên đầu. Do vậy ta cần viết thêm dựa trên cơ chế có sẵn.
Và để viết được thì đầu tiên cần hiểu rõ cơ chế bỏ dấu lên đầu chữ của game. 

Dùng debugger, đặt read breakpoint cho địa chỉ của một chữ cái bất kỳ như đã nói bên trên, và debugger sẽ dừng lại ngay khi đọc được code xử lý chữ cái đó. Ví dụ dưới đây là trường hợp chữ cái không có dấu dakuten hay handakuten, giá trị nằm trong khoảng 00 ~ 9F.

Đoạn code (routine) này bắt đầu ở $85CA và được viết lại như sau:

tại 85CA: php ; lưu giữ giá trị của register P vào stack, để dùng lại sau
tại 85CB: sep #$30
tại 85CD: ldy #$00 ; tải giá trị 00 vào register y. 00 là không thể hiện dấu gì
tại 85CF: cmp $a0 ; so sánh giá trị hiện tại của A với A0
tại 85D1: bcc $28 [$85fb] ; nhảy tới địa chỉ $85fb nếu carry = 0, nói cách khác là nếu giá trị trong A nhỏ hơn A0 thì nhảy tới $85FB
tại 85FB: PLP ; lấy lại giá trị của P được lưu trữ trong stack
tại 85FC: RTL ; kết thúc routine

[​IMG]

Nhìn vào đây, có thể thấy giá trị của A (tức là chữ cái mà A đang đọc) nhỏ hơn A0 nên bộ xử lý mới nhảy tới 85FB và kết thúc với Y = 00, tức không vẽ thứ gì lên đầu chữ cái đứng trước nó.

Giờ thử đặt read break point tại địa chỉ của một chữ kana với dấu handakuten (゜) trên đầu. Khi game đọc tới chữ này thì nó sẽ dừng và cho biết những thông tin tương tự.


$00/85CA 08 PHP ; lưu trữ giá trị của Status register
$00/85CB E2 30 SEP #$30 ; chuyển Register A, X, Y về chế độ 8 bit
$00/85CD A0 00 LDY #$00 ; tải giá trị 0 vào Y
$00/85CF C9 A0 CMP #$A0 ; so sánh A với A0 
$00/85D1 90 28 BCC $28 [$85FB] ; nếu A nhỏ hơn A0, nhảy tới 85FB
$00/85D3 A0 9B LDY #$9B ; tải giá trị 9B vào Y 
$00/85D5 C9 C8 CMP #$C8 ; so sánh A với C8 
$00/85D7 90 0C BCC $0C [$85E5] ; nếu A nhỏ hơn, nhảy tới 85E5
$00/85D9 A0 9C LDY #$9C ; tải giá trị 9C vào Y
$00/85DB E9 AE SBC #$AE ; trừ A cho AE 
$00/85DD C9 1F CMP #$1F ; so sánh kết quả cho 1F 
$00/85DF 90 1A BCC $1A [$85FB] ; nếu kết quả nhỏ hơn, nhảy tới 85FB
$00/85E1 E9 05 SBC #$05 ; trừ kết quả cho 05 
$00/85E3 B0 13 BCS $13 [$85F8] ; nếu kết quả >05, nhảy tới 85F8

$00/85F8 18 CLC reset carry về 0
$00/85F9 69 40 ADC #$40 A + 40
$00/85FB 28 PLP 
$00/85FC 6B RTL ; kết thúc routine

Giả sử, đặt

$85FB = label1
$85E5 = label2
$85F8 = label3

thì đoạn trên viết lại là

$00/85CA 08 PHP 
$00/85CB E2 30 SEP #$30 
$00/85CD A0 00 LDY #$00 
$00/85CF C9 A0 CMP #$A0 
$00/85D1 90 28 BCC label1
$00/85D3 A0 9B LDY #$9B 
$00/85D5 C9 C8 CMP #$C8 
$00/85D7 90 0C BCC label2
$00/85D9 A0 9C LDY #$9C 
$00/85DB E9 AE SBC #$AE 
$00/85DD C9 1F CMP #$1F 
$00/85DF 90 1A BCC label1
$00/85E1 E9 05 SBC #$05 
$00/85E3 B0 13 BCS label3 


Label 1:
PLP
RTL

Đoạn này chỉ có chức năng kết thúc routine, không làm gì hơn.

Label 2: đoạn hiển thị dấu dakuten trên đầu chữ.

Label 3:

CLC
ADC #$40
PLP
RTL

Đoạn này cộng giá trị của A với $40 và kết thúc routine.
Nhìn vào table, lấy giá trị của một chữ Hiragana bất kỳ và cộng với 40, ta sẽ được giá trị của chữ Katakana tương ứng.
Vd: 
0A=こ (ko, Hiragana)
4A=コ (ko, Katakana)

Vậy đoạn code này có tác dụng chuyển đổi chữ Hiragana thành Katakana.

Vậy, toàn bộ routine trên có thể diễn dịch như sau:

- Tải 00 vào Y (không dấu gì cả)
- So sánh A với A0 (từ A0 trở về sau là chữ có dấu ゛ hoặc ゜)
- Nếu A nhỏ hơn A0, nhảy tới label 1 (kết thúc routine, chữ vẫn không có dấu)
- Tải 9B vào Y (9B=゛, tức giờ chữ đã có dấu ゛)
- So sánh A với C8 (từ C8 về sau là chữ mang dấu ゜)
- Nếu A nhỏ hơn C8, nhảy tới routine 2 (mang dấu ゛)
- A lớn hơn hoặc bằng C8, tải Y với 9C (9C=゜, tức giờ đã có dấu ゜)
- Lấy A trừ AE 
- So sánh với 1F
- Nếu kết quả nhở hơn 1F, nhảy tới label1: kết thúc routine
- Trừ tiếp kết quả cho 05
- Nếu kết quả lớn hơn 05, nhảy tới label 3: biến Hiragana thành Katakana, giờ đã mang dấu ゜ và kết thúc routine.

Lấy ví dụ:
CD=パ, khi A đọc tới giá trị này thì đầu tiên nó:

- Vẽ chữ ハ mà không có dấu gì (Y = 00)
- So sánh A (CD) với A0
- Vì CD > A0 nên tiếp tục: vẽ thêm dấu ゛ (Y = 9B) vào, thành ra バ
- So sánh A với C8
- Vì CD > C8 nên tiếp tục: vẽ dấu ゜ (Y = 9C), thành ra パ
- Lấy A trừ cho AE: CD-AE=1F
- So sánh kết quả với 1F
- Vì kết quả không nhỏ hơn 1F nên tiếp tục: trừ kết quả này cho 05: 1F-05=1A (1A=は)
- Nhảy tới label 3: cộng A với 40: 1A + 40 = 5A = ハ, lúc này vẫn còn mang dấu ゜ (Y=9C) nên kết quả thể hiện là パ.

VII. Viết routine bỏ dấu tiếng Việt

Sau khi phân tích routine bỏ dấu nguyên bản của game ở phần trước, ta thấy game dùng register A để đọc giá trị của ký tự, và register Y để đọc giá trị của con dấu. Ở đây ta áp dụng cùng nguyên tắc này để viết cơ chế bỏ thêm các dấu cho nguyên âm. Có nhiều cách bỏ dấu. Dưới đây là 2 ví dụ.

Cách 1:



org $85CA ;bắt đầu viết code tại $85CA

jml $201234 ;nhảy tới vị trí trống
org $201234 ;bắt đầu viết code tại vị trí mới
php ;như cũ
sep #$30 ;như cũ
ldy #$00 ;như cũ, tải "không dấu" vào Y
cmp #$90 ;so sánh A với $90 (giá trị của nguyên âm 1 có dấu 1, chẳng hạn "á")
beq label_sắc ; nếu A = $90, nhảy tới label bỏ dấu sắc
cmp #$91 ;so sánh A với $91 (giá trị của nguyên âm 1 có dấu 2, chẳng hạn "à")
beq label_huyền ; nếu A = $91, nhảy tới label bỏ dấu huyền
cmp #$92 ;so sánh A với $92 (giá trị của nguyên âm 1 có dấu 3, chẳng hạn "ả")
beq label_hỏi ; nếu A = $92, nhảy tới label bỏ dấu hỏi
cmp #$93 ;so sánh A với $93 (giá trị của nguyên âm 1 có dấu 4, chẳng hạn "ã")
beq label_ngã ; nếu A = $93, nhảy tới label bỏ dấu ngã
cmp #$94 ;so sánh A với $94 (giá trị của nguyên âm 2 có dấu 1, chẳng hạn "é")
beq label_sắc ; nếu A = $94, nhảy tới label bỏ dấu sắc
cmp #$95 ;so sánh A với $95 (giá trị của nguyên âm 2 có dấu 2, chẳng hạn "è")

........
plp ;như nguyên bản
rtl ; kết thúc routine

Tương tự, viết hết các trường hợp kết hợp các nguyên âm và các dấu tiếng Việt. Cuối đoạn code, đừng quên định nghĩa các label.



label_sắc:
ldy #$70 ; tải giá trị $70 vào Y. Ở đây $70 là giá trị của tile có dấu sắc
jml $0085e5 ; nhảy tới vị trí thực hiện lệnh bỏ dấu lên đầu nguyên âm như nguyên bản

label_huyền:
ldy #$71 ; tải giá trị $71 vào Y. Ở đây $71 là giá trị của tile có dấu huyền
jml $0085e5 ; nhảy tới vị trí thực hiện lệnh bỏ dấu lên đầu nguyên âm như nguyên bản

label_hỏi:
ldy #$72 ; tải giá trị $72 vào Y. Ở đây $72 là giá trị của tile có dấu hỏi
jml $0085e5 ; nhảy tới vị trí thực hiện lệnh bỏ dấu lên đầu nguyên âm như nguyên bản

label_ngã:
ldy #$73 ; tải giá trị $73 vào Y. Ở đây $73 là giá trị của tile có dấu ngã
jml $0085e5 ; nhảy tới vị trí thực hiện lệnh bỏ dấu lên đầu nguyên âm như nguyên bản

Với cách này, bình thường game sẽ bỏ dấu "không có gì" (không dấu, Y=00) vào các giá trị của A khác với 90, 91, 92,.... Trong đó, ta gán 90=á, 91=à, 92=ả,...
Còn nếu giá trị của A=90, thì game sẽ bỏ dấu sắc (ở đây gán Y=71) lên đầu chữ cái đứng trước, và tương tự với các dấu còn lại. CMP (CoMPare A) là lệnh so sánh giá trị của A với một giá trị khác. BEQ (Branch if EQual) là lệnh phân nhánh nếu giá trị so sánh trước đó = 0.

Lưu ý: cách này sử dụng nhiều câu lệnh BEQ, trong khi lệnh phân nhánh chỉ có hiệu lực trong khoảng 256 byte kể từ vị trí lệnh. Tức game chỉ cho phân nhánh (nhảy tới vị trí) trong phạm vi 256 byte, nếu code dài quá khoảng này thì sẽ báo lỗi.


Cách 2:


org $85CA ;bắt đầu viết code tại $85CA
jml $201234 ;nhảy tới vị trí trống
org $201234 ;bắt đầu viết code tại vị trí mới
PHP ; như cũ, lưu giá trị của register P vào stack
pha ;lưu giá trị của A vào stack
SEP #$30 ;như cũ
tax ;chuyển giá trị của A cho X
lda table,x ;tải giá trị tại địa chỉ của table + giá trị của X vào A
tay ;chuyển giá trị của A cho Y
pla ;lấy lại giá trị của A lúc nãy cất giữ trong stack
plp ;lấy lại giá trị của P lúc nãy cất giữ trong stack
rtl ; kết thúc routine

table:
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $28
db $00, $00, $00, $00, $00, $28, $29, $2a, $2b, $00, $32, $00, $2a, $28, $29, $2a
db $2b, $2c, $2d, $2e, $2f, $30, $2c, $40, $41, $42, $43, $44, $40, $28, $29, $2a
db $2b, $2c, $2d, $2e, $2f, $30, $2c, $28, $29, $2a, $2b, $28, $29, $2a, $2b, $2c
db $2d, $2e, $2f, $30, $2c, $28, $29, $00, $00, $00, $00, $00, $00, $2b, $28, $29
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00
db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $2a, $2b, $00, $00, $00

db là lệnh chèn byte. Trong label mang tên "table" bên trên, ta chèn một loạt byte có giá trị lần lượt: 00, 00, 00,....2B, 2C, 2D,....

Giải thích: 
1. TAX chuyển giá trị của register A sang register X. Khi A đọc đến ký tự nào thì nó chuyển giá trị của ký tự đó cho X.
2. LDA table,x tải giá trị tại địa chỉ của label mang tên "table", cộng với giá trị của register X vào trong A. 
3. TAY chuyển giá trị mới của A vào Y.

Như vậy, giả sử ban đầu A=03, thì PHA lưu giữ giá trị này vào vùng nhớ gọi là stack.
Tiếp theo, nếu TAX khiến X lúc này có giá trị = 03.
Tiếp đó, LDA table,X tải giá trị tại địa chỉ tương đối của label "table" + giá trị của X. Chẳng hạn, nếu table nằm tại địa chỉ $206789 thì LDA table,X sẽ tải giá trị tại địa chỉ $206789 + $03= $20678C. Tại đây có giá trị 00 nên A lúc này sẽ mang giá trị 00 (nhìn vào bảng table). 
Kế đến, TAY chuyển giá trị 00 cho Y. Lúc này Y mang giá trị 00, tức không dấu.
PLA lấy lại giá trị 03 đã lưu trữ trước đó cho A.
Như vậy, đoạn code này cho thấy nếu A=03 (giá trị một ký tự nào đó) thì Y=00 và sẽ không có dấu nào được vẽ lên trên ký tự đó.

Giả sử A=4F, thì PHA lưu giá trị này (4F) vào vùng nhớ gọi là stack.
Tiếp theo, nếu TAX khiến X lúc này có giá trị = 4F.
Tiếp đó, LDA table,X tải giá trị tại địa chỉ tương đối của label "table" + giá trị của X. Chẳng hạn, nếu table nằm tại địa chỉ $206789 thì LDA table,X sẽ tải giá trị tại địa chỉ $206789 + $4F= $2067D8. Tại đây có giá trị 00 nên A lúc này sẽ mang giá trị 2A (nhìn vào bảng table).
Kế đến, TAY chuyển giá trị 2A cho Y. Lúc này Y mang giá trị 2A, tức có dấu (dấu gì thì tùy ta vẽ tại tile 2A).
PLA lấy lại giá trị 4F đã lưu trữ trước đó cho A. 

Như vậy, đoạn code này cho thấy nếu A=4f (giá trị một ký tự nào đó) thì Y=2A và sẽ vẽ lên trên ký tự đó con dấu có giá trị 2A.

Kết quả bỏ dấu cho hình như dưới đây.

[​IMG]

Còn đây là hình ảnh sau khi chỉnh lại trật tự câu chữ.

[​IMG]


VIII. Kết

Sau khi đã sửa font chữ và giải quyết vấn đề dấu tiếng Việt, ta có thể ung dung đến phần dịch thuật số text đã dump trước đó. 

[​IMG]

Tới đây coi như đã giải quyết xong 70% số việc cần làm. Vì sao chỉ mới có 70%? Vì trong quá trình dịch sẽ phát sinh rất nhiều vấn đề cần giải quyết, chẳng hạn như giới hạn không gian trống, chữ lệch hàng, mất phần hiển thị dấu,...

[​IMG]

Những vấn đề này không thể giải quyết bằng các thủ thuật thông thường, mà cần phải dùng đến kiến thức về Assembly. Một trong những vấn đề thường gặp nhất chính là không gian trống. Phần text hội thoại thường nằm trong một không gian hạn hẹp giữa code game như sau:

<code 1><text hội thoại><code 2>

Vì tiếng Nhật có tính cô đọng hơn các ngôn ngữ khác trong cả ngữ nghĩa lẫn các biểu thị ra con chữ, cùng một âm tiết thì tiếng Nhật chỉ dùng một chữ cái, còn các ngôn ngữ khác dùng nhiều chữ cái.
Chẳng hạn: âm "ka" (2 ký tự) được biểu thị bằng 1 ký tự か trong tiếng Nhật. Vì lý do này mà các bản dịch thường dài hơn bản gốc. Nếu phần text dịch dài hơn thì sẽ ghi đè lên phần code khác trong game, khiến hư hỏng Rom. Để giải quyết việc này thì phải can thiệp vào phần code điều khiển hội thoại.
Thử phân tích một ví dụ với khối text thể hiện tên giải đấu. Khối này bắt đầu tại $255FC cho pointer và $25628 cho phần text. Khối text này nằm trong không gian chỉ có vài trăm byte nên sẽ không đủ cho phần dịch thuật. Lúc này ta cần chuyển text ra vùng không gian trống khác, chẳng hạn vùng trống sau khi mở rộng Rom bằng công cụ Lunar Expand.

Giống như ở phần bỏ dấu, cách giải quyết vấn đề nằm ở kết quả debug. Chỉ có thông qua debug ta mới biết chính xác CPU xử lý cái gì, ra sao và từ đó mới đề ra được giải pháp.
Đầu tiên, đổi các địa chỉ trên sang địa chỉ Snes. $255FC = $04:D5FC, $25628 = $04:D628.
Đặt một read break point tại $04:D5FC, chơi đến khi vào đến màn hình hiển thị trên trận đấu. Lúc này CPU sẽ ngừng xử lý khi đến địa chỉ ta chỉ định, click vào "Step Into" để bám sát các lệnh CPU sẽ thực hiện.

[​IMG]

$02/E8DD B1 19       LDA ($19),y[$04:D5FC]   A:D500 X:004A Y:0000 P:envmXdIZc

$02/E8DF 85 19       STA $19    [$00:1819]   A:D628 X:004A Y:0000 P:eNvmXdIzc
$02/E8E1 A0 00       LDY #$00                A:D628 X:004A Y:0000 P:eNvmXdIzc
$02/E8E3 E2 20       SEP #$20                A:D628 X:004A Y:0000 P:envmXdIZc
$02/E8E5 B1 19       LDA ($19),y[$04:D628]   A:D628 X:004A Y:0000 P:envMXdIZc
$02/E8E7 C9 FC       CMP #$FC                A:D600 X:004A Y:0000 P:envMXdIZc
$02/E8E9 F0 10       BEQ $10    [$E8FB]      A:D600 X:004A Y:0000 P:envMXdIzc
Từ kết quả này thì hiểu được:

1. Game đọc pointer 2 byte từ vị trí $D5FC trong bank 04
2. Chứa giá trị của pointer đó vào $1819 trong Ram ($19 + giá trị của DP Register lúc này là 1800)
3. Reset Register Y về 0
4. Set Register A về chế độ 8 bit, để A chỉ đọc được 1 byte
5. Đọc giá trị tại $1819 + giá trị của Y, tức $D628 trong bank 04 vào trong A --> hiển thị chuỗi text tại $04D628 ra màn hình
6. So sánh A với FC (mã kết thúc chuỗi text)
7. Nếu A = FC thì nhảy tới $E8FB, xử lý kết thúc câu
8. Các xử lý khác


Có rất nhiều cách giải quyết vấn đề từ kết quả này. Một trong những cách đơn giản là biến dòng LDA ($19),y vốn đang đọc dữ liệu tại bank 04 sang đọc tại bank khác.
Giả dụ, ta muốn viết đoạn text này vào $105628 thay vì $25628 như nguyên bản. $105628 tức là $5628 tại bank 20 ($205628). Có thể viết đơn giản như sau:




org $02e8e5  ; bắt đầu ghi code tại $02E8E5
    jml $20e8e5      ; chuyển khối code tại $02E8E5 sang $20E8E5
  
org $20e8e5  ; bắt đầu ghi code tại $20E8E5
    phb         ; lưu giữ bank hiện tại (04)
    lda #$20   ; tải $20 vào A
    pha        ; lưu giữ A hiện tại ($20)
    plb        ; kéo giá trị của A ($20) vào bank
    lda ($19),y ; đọc giá trị tại bank (20) + $1819 + Y
    plb  ;  lấy lại giá trị bank cũ (04)
    CMP #$FC   ; so sánh A với $FC
    BEQ label   ; nếu bằng $FC, nhảy tới label
    INY         ; tăng Y lên 1
    PHY        ; lưu giữ Y
    jml $02E8ED   ; về địa chỉ trong khối code cũ

label:
    jml $02E8FB

Ngoài ra còn nhiều cách khác để giải quyết cùng một vấn đề. Song, tất cả đều dùng đến kiến thức Asm. Vì vậy, việc học hỏi về Assembly của một hệ CPU là điều cần thiết và rất quan trọng trong việc hack/dịch game hệ đó, nếu muốn làm một cách bài bản. Tất nhiên, trong nhiều trường hợp thì các thủ thuật dịch game thông thường như lập table, dump text, chèn text,... đều có thể thực hiện mà không cần đến kiến thức Asm. Nhưng để giải quyết được hết thảy mọi vấn đề nảy sinh, một cách triệt để, thì cần phải dùng đến Asm.

Không có nhận xét nào:

Đăng nhận xét