Ninja mèo

I. Bối cảnh

Sau khi công khai mã nguồn bản dịch Contra (Famicom 1988), một vài người đã hỏi tôi cách dịch game Famicom/NES là như thế nào. Thật ra câu trả lời rất đơn giản: cần có kiến thức về đối tượng mà mình đang làm. Cụ thể ở đây là kiến thức về phần cứng của máy Famicom/NES, kiến thức về cách làm game trên hệ máy đó, kiến thức về cách mà Tivi đèn hình đời cũ (CRT) hoạt động, kiến thức về ngôn ngữ lập trình của hệ máy đó (WDC6502). Nói thì ngắn gọn vậy thôi, chứ để lãnh hội được những kiến thức này ở mức độ cơ bản thì cũng cần khá nhiều thời gian. 

Nói rốt ráo là vậy, chứ thật ra để dịch game thì chuyện đơn giản hơn để làm game rất nhiều. Từ năm 2015 trở đi là thời kỳ bùng nổ của các nhóm dịch game. Việc này cho thấy "kỹ thuật dịch game" cũng không có gì khó khăn, trừ khi muốn hiểu vào cốt lõi, bản chất của vấn đề.

Dù đã công bố toàn bộ mã nguồn bản dịch game Contra, nhưng tất cả chỉ là mã assembly của Famicom mà không có giải thích gì nên nhiều người cảm thấy khó hiểu. Đó là đặc điểm của các ngôn ngữ bậc thấp, dù nắm trong tay mã nguồn nhưng nếu không có giải thích thêm thì người đọc/người tiếp nhận cũng khó biết thêm được gì. Điều này trái ngược với các ngôn ngữ phần cứng. Do vậy nên trong các dự án dịch thuật tiếp theo, tôi sẽ cố gắng giải thích về cốt lõi của vấn đề, song song với các "kỹ thuật dịch game" để người đọc dễ dàng tiếp cận hơn. Đây là một nỗ lực nhằm phổ biến kiến thức về game Retro như Famicom, Super Famicom đến những đông đảo đại chúng, một mảng được coi là "thâm cung bí sử", "nan giải nan nhập" không chỉ đối với cộng đồng gamer Việt Nam mà còn đối với cả Thế giới.

Dự án dịch thuật Famicom tiếp theo trong năm 2022 là một game được biết đến với cái tên "Ninja mèo" ở Việt Nam. Hầu hết gamer thuộc thế hệ 8x, đầu 9x đều biết đến nó, nhưng không mấy người rõ về nó, bởi vì bản thân game này chưa từng bước chân ra khỏi Nhật Bản. Mãi đến những năm gần đây mới xuất hiện bản dịch Anh ngữ. 

Thật ra "Ninja mèo" có tên gốc là "Kyattō Ninden Teyandei" (tạm dịch là "câu chuyện băng Ninja mèo Sàm xí đú"). Đây là tựa game ăn theo series phim hoạt hình cùng tên được chiếu trền đài truyền hình ở Nhật từ năm 1990 đến năm 1991. Bộ phim hoạt hình này thuộc thể loại hài kịch, lấy bối cảnh giả tưởng mô phỏng thời Edo ở Nhật với các chiến binh Samurai và Ninja là thú vật, Robot trong thành phố Edoropolis. Phiên bản game do Tecmo phát hành, có nội dung bám khá sát nguyên tác, có nhiều đoạn hài hước từ lối chơi chữ (tiếng Nhật) giống như phiên bản hoạt hình. Bản dịch Việt văn này cũng cố gắng tái hiện lại tính hài hước qua ngôn từ của bản tiếng Nhật.

Nói thêm, từ "teyandei" trong tiêu đề của tự game/Anime này là một từ tiếng Nhật thời Edo, nghĩa là "nói tào lao", "nói xằng", "nói nhảm", "nói sàm",... Đây là từ dùng để đáp lại khi thấy đối phương nói chuyện tầm phào, mang sắc thái miệt thị. Đây cũng là câu cửa miệng của một nhân vật chính trong game. Bộ phim hoạt hình này cũng được hãng Saban mua bản quyền, phát sóng ở Mỹ quốc với tựa đề "Samurai Pizza Cats".

II. Những thứ cần có

Dưới đây là danh sách những thứ cần có/cần biết để dịch "Ninja mèo" sang tiếng Việt. Mọi thứ đều có thể học được/kiếm được từ Google hoặc tự làm/tự code nếu có khả năng. Những mục đánh dấu * là mục biết/có càng tốt, không thì cũng không sao, vì chúng thuộc phạm vi hướng dẫn của bài này.


III. Chuẩn bị ROM

Muốn dịch một game nào đó từ ngôn ngữ A sang ngôn ngữ B thì yếu tố đầu tiên chúng ta cần có chính là sở hữu game đó. Game Famicom được nhà sản xuất bán ra ở dạng băng Cartridge với độ bền vô đối, nên ngày nay ta có thể dễ dàng mua băng này từ các chợ đồ cũ trong và ngoài nước. Tuy nhiên, để dịch được nội dung trong băng thì cần có thiết bị hút Rom từ băng ra. Có 2 cách để hút Rom từ băng: dùng thiết bị chuyên dụng (dumper) hoặc dùng chức năng dump của một số loại máy game nhái Famicom/NES. Nếu là một dân chơi Retro thì hẳn bạn đã quá quen thuộc với những khái niệm này. Ở đây tôi không đề cập sâu vào phần này. Bạn có thể tự tìm hiểu từ Youtube và Google, vì thông tin không hề thiếu. 

Toàn bộ lưu trình dịch game gồm những bước lớn như dưới đây.

Cần lưu ý và hiểu cho đúng về luật tác quyền là nếu bạn sở hữu băng game thì việc bạn chơi giả lập với Rom được xuất từ băng đó là hoàn toàn hợp pháp. Bạn có thể cho bạn bè, bà con mượn băng game đó trong một giới hạn nhất định. Vậy nếu chỉnh sửa nội dung của Rom thì có phải là vi phạm tác quyền hay không? Đó lại là một câu chuyện khác, nhưng thôi cũng kệ. Việc ghi đè bản dịch lên nội dung gốc trong Rom cũng bị coi là xâm hại tác quyền. Còn nếu bản dịch đó tồn tại ở dạng patch, chỉ có tác dụng khi chơi bằng giả lập thì thôi, ai nỡ làm khó nhau.... Mà cũng chẳng ai biết được anh Ninh Tiền Đô sẽ phản ứng như thế nào với các bản dịch. Về nguyên tắc thì anh ấy có quyền...

IV. Lập table

File table thực chất là một file text, nội dung của nó là những phép gán: gán một giá trị Hex với một chữ cái. Chẳng hạn, gán 09=A, 0A=B, 0B=C thì chuỗi hex 0x09, 0x0A, 0x0B sẽ được xem tương đương với chuỗi text "ABC". Ta dùng file table vào hai việc: trích xuất text và chèn text. Do vậy cần có hai table khác nhau. Table ngôn ngữ nguồn thì dùng chung với phần mềm trích xuất text như Cartographer. Qua ví dụ các entry trong table trên, khi phần mềm gặp các giá trị hex 0x09, 0x0A, 0x0B thì nó sẽ xuất chuỗi text là "ABC". Còn với table ngôn ngữ đích thì dùng chung với phần mềm chèn text như Atlas. Khi phần mềm gặp chuỗi text "ABC" thì nó sẽ chèn vào rom chuỗi giá trị hex là 0x09, 0x0A, 0x0B.

Để lập file table thì trước hết cần biết giá trị text nào tương đương với ký tự nào trong game. Đối với game Famicom/NES thì đây là việc cực kỳ đơn giản. Hầu hết các phần mềm giả lập kiêm debugger đều có chức năng xem Vram của máy Famicom/NES. Bật game bằng debugger, đến đoạn có text trên màn hình thì bật chức năng CHR Viewer để xem các CHR (viết tắt của "character", nghĩa là thành phần đồ họa) trong Vram tại thời điểm đó. Font chữ cũng là một thành phần đồ họa, nên khi chữ hiện trên màn hình thì có nghĩa là nó đang tồn tại trong Vram tại thời điểm đó. Giá trị của mỗi ký tự trong bộ font chính là số ID/số thứ tự của chr (character/thành phần đồ họa) đó trong Vram.

Chẳng hạn như qua đoạn text mở đầu game, khi xem CHR Viewer thì ta biết ký tự あ có số thứ tự là 0x01, ký tự リ có số thứ tự là 0x14. Do vậy ta lập table là file text có phần mở rộng .tbl với các entry như sau:

01=あ

02=い

03=う

04=え

05=お

....

File table ngôn ngữ nguồn (tiếng Nhật) cần được lưu ở định dạng Shift-JIS. Sau khi lập file table thì bật WindHex, load Rom rồi load file table vừa lập. Lúc này ta có thể xem được text trong Rom, nếu như nó không bị nén. Bước xem text bằng WindHex không phải là bước bắt buộc, hoàn toàn có thể bỏ qua. Ta có thể xác định được vị trí của text trong Rom bằng cách debug. Tuy nhiên, "Ninja mèo" là một trong số ít ỏi game Famicom có lượng text khá nhiều (so với mặt bằng chung của Famicom/NES) nên việc dùng WindHex sẽ giúp ta nắm bắt được vị trí của text trong Rom tốt hơn.

V. Kiến thức dự bị về Famicom/NES

Trước khi sang phần tiếp theo thì cần nắm một số kiến thức về đặc thù phần cứng của máy Famicom/NES. Đầu tiên cần biết bộ não của máy này là bộ vi xử lý RP2A03 (đối với hệ NTSC, tốc độ 1.79MHz) hoặc RP2A07 (đối với hệ PAL, tốc độ 1.66MHz) của hãng Ricoh. Đây là CPU 8bit dựa trên nền tảng 6502 của hãng Western Design Center. Con người cần nắm ngôn ngữ assembly 6502 để có thể giao tiếp với CPU. Thực chất, đây là một tập hợp các ký hiệu (tập hợp lệnh), mỗi ký hiệu gồm 3 chữ cái viết tắt biểu thị cho một từ có nghĩa đối với con người (chẳng hạn: "CMP" là từ viết tắt của "Compare A". Tập hợp lệnh 6502 khá đơn giản so với tập hợp lệnh của các CPU thời kỳ sau đó. Trong khái niệm "ngôn ngữ 6502" còn có một ý niệm khác được gọi là "addressing mode". Đây là kiểu load dữ liệu, các quy tắc truy cập địa chỉ quan trọng cần phải nắm vững. Bạn có thể tự học ngôn ngữ 6502 từ Google, mà trang web này (click) là một ví dụ. Bài này chỉ giới thiệu khái quát chứ không đi sâu vào phần ngôn ngữ này.

Khái niệm tiếp theo cần nắm rõ là các Register cả Famicom/NES. Máy console này có 3 kiểu Register là CPU Register chuyên xử lý, tính toán dữ liệu; PPU Register chuyên điều khiển các chức năng đồ họa (PPU là viết tắt của từ "Picture Processing Unit", có thể coi đây như một kiểu card đồ họa trong máy tính hiện đại); APU Register chuyên xử lý các chức năng âm thanh (APU là viết tắt của "Audio Processing Unit"). Số lượng Register của Famicom/NES cực kỳ ít so với Super Famicom/SNES. Bạn có thể tìm hiểu thêm về Register tại trang web này (click).

Tiếp theo, bạn cần nắm về độ phân giải của Famicom/NES và cách mà nó vẽ hình ảnh trên màn hình Tivi. Máy Famicom/NES có độ phân giải 256 x 240 pixel, tuy nhiên bạn sẽ không thấy được hết 240 pixel của chiều đứng, mà chỉ thấy được 224 pixel mà thôi. Việc này liên quan tới khái niệm V-blank và cách mà Tivi kiểu cũ (Tivi CRT) hiển thị hình ảnh. Bạn có thể đọc thêm về những khái niệm này trong một bài khác mà tôi đã viết (click). Nói nôm nay, khi tia âm cực quét đến hết dòng thứ 224 thì màn hình sẽ "tắt", mở đầu cho chu kỳ V-blank. Lúc này mọi dữ liệu hình ảnh được cập cho khung hình tiếp theo trong khoảng thời gian này. Đây là quy tắc vàng không thể phá bỏ. Chỉ có thể cập nhật dữ liệu đồ họa lên màn hình trong khoảng thời gian này. Sau khi kết thúc chu kỳ V-blank, tia âm cực trở về gốc tọa độ ban đầu, màn hình được "bật" trở lại và hiển thị hình ảnh mới, vừa được cập nhật trong giai đoạn V-blank liền trước đó. Có một khái niệm khác cần quan tâm, là "NMI". Đây là một đoạn code, một routine định kỳ được CPU thực hiện vào đầu mỗi chu kỳ V-blank. Thông thường, nội dung chính của đoạn code này là ghi dữ liệu đồ họa (gồm cả text) lên màn hình, để rồi chúng được cập nhật trong khung hình tiếp theo.

Nói về PPU thì có hai khái niệm cần quan tâm, là Nametable và CHR. Ở phần trên đã đề cập đến CHR (character) chính là các thành phần đồ họa cấu thành nên mọi hình ảnh trên màn hình. Còn Nametable chính là ma trận mà các CHR đó được sắp xếp theo những trật tự nhất định để tạo nên hình ảnh. Nametable tương đương với khái niệm Tilemap, còn CHR tương đương với khái niệm Tileset/Character set đối với máy Super Famicom/SNES. Bạn có thể hình dung cách mà Famicom/NES vẽ hình ảnh lên màn hình giống như cách ta sắp xếp các mảnh ghép trong trò chơi xếp hình (Jigsaw), trong đó CHR trong PPU chính là từng mảnh ghép, còn hình ảnh trên Nametable chính là hình ảnh được lắp ghép hoàn chỉnh từ các mảnh. Nói cách khác, CHR (Character) chính là các tile đồ họa với kích thước 8x8. Máy Famicom/NES cũng như Super Famicom/SNES đều là các loại console hiển thị đồ họa kiểu tile (mảng).

Rom Famicom/NES tồn tại ở dạng băng Cartridge (hay còn gọi là "Cassette" ở Nhật, trong tiếng Việt gọi là "băng"). Không phải băng nào cũng như băng nào, mà chúng khác nhau ở kiểu bố trí mạch và các thành phần phần cứng đi kèm. Vậy tại sao phải cần đến các loại băng khác nhau? Bởi vì phần cứng của máy Famicom/NES có rất nhiều hạn chế, không đáp ứng được hết yêu cầu của phần mềm (game) nên người ta mới khắc phục bằng phần cứng bổ sung đi kèm trong băng. Chẳng hạn như có loại băng cho phép trường độ của Rom vượt quá ngưỡng giới hạn có thể đọc của CPU, có loại băng bổ sung thêm Ram, có loại băng thêm channel âm thanh,... Và mỗi nhóm chức năng như vậy được gọi là một kiểu Mapper. Có một điểm chung giữa các loại Mapper là hầu hết chúng đều được thiết kế để dung lượng Rom vượt qua giới hạn dung lượng có thể đọc của CPU.

Rom Famicom/NES được cấu thành bởi hai thành phần: các PRG bank và các CHR bank. Bank là khái niệm chỉ một nhóm byte liên tục với kích thước nhất định, thường là 8KB hoặc 16KB, tùy vào từng kiểu Mapper. PRG bank là các bank dữ liệu chương trình, các routine điều khiển mọi thứ trong game. Còn CHR bank là các bank chứa dữ liệu đồ họa được dùng đến trong quá trình chơi. Nội dung của PRG bank được load vào CPU memory, rồi CPU sẽ thi hành các mệnh lệnh trong đó. Tương tự, nội dung của các CHR bank được load vào PPU, các thành phần đồ họa trong đó sẽ được hiển thị lên màn hình theo chỉ định của CPU: thành phần nào được hiển thị ở vị trí nào của màn hình, tại thời điểm nào.

Vấn đề nằm ở chỗ CPU của Famicom/NES chỉ có thể truy cập vào các địa chỉ trong giới hạn từ $0000 đến $FFFF, tức nó chỉ có thể truy cập các địa chỉ trong phạm vi 64KB mà thôi. Nếu như dung lượng Rom của game, gồm tất cả mọi thứ: code game, dữ liệu đồ họa, âm thanh đều nằm gọn trong 64KB này thì không có chuyện gì để nói. Chẳng hạn như Super Mario Bros chỉ có 41KB, và một số game khác trong thời kỳ đầu của NES cũng nằm trong khoảng 64KB này. Đối với những game này, mọi nội dung trong băng game đều được gói gọn trong khoảng từ $0000 đến $FFFF của CPU Memory, nên CPU có thể thực hiện mọi mệnh lệnh, hiển thị mọi hình ảnh (CHR), đọc mọi dữ liệu chứa trong băng game. Nhưng chỉ trong một thời gian ngắn sau đó, đã xuất hiện nhiều game với lượng lớn hơn (chủ yếu do dữ liệu đồ họa và âm thanh), vượt khỏi giới hạn 64KB. Lúc này nảy sinh vấn đề: làm cách nào để chứa hết nội dung của một băng game có dung lượng 128KB hay 256KB vào trong CPU memory chỉ có 64KB? Để giải quyết vấn đề này, người ta chia Rom thành nhiều phần nhỏ với kích thước bằng nhau. Mỗi phần được gọi là một bank, là một chuỗi byte liên tiếp nhau. Kích thước của mỗi bank phụ thuộc vào định nghĩa của Mapper. Tại một thời điểm, chỉ có một phần của Rom (một số bank) được bố trí (mapping) vào CPU memory. Lúc này, CPU chỉ có thể thi hành các mệnh lệnh có trong phần bank đó, chỉ có thể sử dụng dữ liệu được chứa trong bank đó. Đến khi cần thi hành mệnh lệnh hay cần truy cập dữ liệu khác nằm tại vị trí khác của Rom thì CPU sẽ giải phóng phần Rom vừa rồi, mà load phần Rom cần thiết vào CPU memory. Việc này được gọi là "chuyển đổi bank". Đây là khái niệm quan trọng bậc nhất trong kỹ thuật lập trình/hack Famicom/NES. Tương tự, trường hợp dung lượng của các CHR bank lớn hơn sức chứa của PPU (VRam) thì người ta cũng dùng kỹ thuật "chuyển đổi bank" để đưa thành phần đồ họa cần hiển thị vào PPU. Khái niệm về kỹ thuật này được mô tả chi tiết trong video dưới đây. 

VI. Mở rộng Rom

Mở rộng Rom là một khái niệm thường gặp trong mảng hack Rom. Việc này giúp ta có thêm không gian trống để chứa code mới cũng như bản dịch. Thường thì bản dịch sẽ tốn nhiều không gian hơn ngôn ngữ gốc, và đôi khi là cần phải thêm code mới, nhiều chức năng mới để hỗ trợ ngôn ngữ đích. Nếu như Rom gốc còn nhiều không gian trống thì ta có thể lợi dụng những chỗ trống đó để chứa code mới, bản dịch ngôn ngữ đích. Nếu như Rom gốc không còn đủ chỗ trống thì ta cần phải mở rộng Rom. Kích thước của Rom NES được quy định ở 0x10 byte ở phần header, tuy nhiên thủ thuật mở rộng Rom không chỉ đơn giản là chèn thêm byte trống vào trong Rom gốc. Có một số công cụ giúp việc mở rộng Rom trở nên đơn giản hơn, chẳng hạn như phần mềm Nflate. Tuy nhiên, những phần mềm này không hoạt động đúng với 100% Rom. Lúc đó, ta cần phải mở rộng Rom bằng tay với kiến thức về Mapper. May thay, Ninja mèo không phải là Rom đặc biệt cần phải mở rộng theo cách thủ công. Tính khả thi của việc mở rộng Rom còn nằm ở yếu tố dung lượng của Rom gốc so với dung lượng tối đa của kiểu Mapper đó cho phép. Do vậy ta cần xác định kiểu Mapper cũng như dung lượng của Rom gốc.

Đối với Ninja mèo, Rom gốc có kích thước (gồm cả PRG và CHR) là 256 KB và kiểu Mapper là MMC3. Một số Debugger như Mesen hay FCEUX đều có chức năng thông báo kiểu Mapper của Rom.

Sau khi xác định được kiểu Mapper, việc cần làm tiếp theo là xác định dung lượng tối đa của nó. Theo thông tin tại trang này (click) thì kiểu Mapper MMC3 cho phép dung lượng PRG tối đa là 512 KB và CHR là 256 KB. Như vậy, ta hoàn toàn có thể nới rộng dung lượng của Rom gốc Ninja mèo. Khi mở rộng Rom, ta có hai lựa chọn là mở rộng PRG bank và mở rộng CHR bank. Nếu không có nhu cầu vẽ thêm hình ảnh vào bản dịch thì thông thường, chỉ cần mở rộng phần PRG bank là đủ không gian trống để chứa code mới và bản dịch.

VII. Mở rộng khung thoại

Sau khi mở rộng Rom, ta đã sẵn sàng để dịch từ ngôn ngữ nguồn sang ngôn ngữ đích. Tất cả mọi đoạn hội thoại trong game này đều được hiển thị trong một khung thoại. Quan sát kỹ thì sẽ thấy khung thoại được vẽ với chiều cao chiếm gần một nửa chiều cao của màn hình, bề ngang chiếm gần hết màn hình, trừ 2 khoảng đen như hình dưới đây.

Ta có thể mở rộng khung thoại này, để nó mở rộng về hai bên tả hữu, tận dụng được không gian viết chữ. Nhưng đầu tiên cần phải tìm hiểu về cách mà máy vẽ khung thoại này như thế nào. Đầu tiên, ta cần xác định cách mà một thành phần đồ họa bất kỳ thuộc khung thoại này được ghi vào PPU như thế nào. Muốn biết được cách mà tile đồ họa đó được ghi vào PPU như thế nào thì cần xác định vị trí (địa chỉ) trong PPU mà tile sẽ được ghi vào. Đối với các Debugger có chức năng cho xem Nametable thì việc xác định này cực kỳ đơn giản. Chẳng hạn như tile đồ họa ở góc trái trên cùng của khung thoại xuất hiện ở màn hình đầu tiên có địa chỉ PPU là $2201, số ID của tile đó là 0x77.

Việc tiếp theo là đặt break point trong chức năng Debugger. Break point được hiểu nôm na là "điểm dừng". Khi CPU đọc đến lệnh thỏa mãn điều kiện được đặt ra trong break point thì nó sẽ tạm dừng xử lý tại vị trí đó, cho ta biết mọi thông tin về địa chỉ lệnh, dữ liệu liên quan đến mệnh lệnh đó. Có 3 kiểu break point là: đọc, ghi, thi hành. Ở đây, ta cần xác định cách mà giá trị tile 0x77 được ghi vào địa chỉ PPU $2201 như thế nào, do đó ta cần đặt break point ghi vào PPU với địa chỉ $2201. Khi có bất kỳ giá trị nào được ghi vào địa chỉ $2201 trong PPU thì toàn bộ hệ thống sẽ tạm dừng hoạt động. Cách đặt break point như hình minh họa dưới đây.

Sau khi đặt break point như vậy, ta sẽ thấy CPU dừng xử lý mỗi khi có giá trị được ghi vào địa chỉ $2201 trong PPU, nhưng không phải tất cả chúng đều là thứ ta cần tìm. Chỉ khi giá trị 0x77 được ghi vào $2201 mới là lúc ta cần quan tâm.

Ta có thể đặt điều kiện khi đặt break point để CPU chỉ dừng khi có giá trị chỉ định được ghi vào địa chỉ chỉ định. Cách này sẽ giúp ta tìm ra được routine mong muốn nhanh hơn.

Khi CPU thi hành tới dòng lệnh thỏa mãn với điều kiện được đặt ra trong break point thì nó sẽ tạm dừng mọi hoạt động. Lúc này ta có thể thấy giá trị 0x77 được ghi vào PPU tại dòng lệnh STA $2007 ở địa chỉ $E1FA trong PRG Rom. Địa chỉ này tương ứng với $81FA trong CPU Memory. Do Ninja mèo là Rom với Mapper MMC3, kích thước mỗi bank là 0x2000 byte nên địa chỉ $E1FA tương ứng với bank $07 ($E1FA chia cho $2000 = $07). Bank $07 bắt đầu từ địa chỉ $E000 trong PRG Rom ($07 x $2000 = $E000), mà ta đã biết các bank trong PRG Rom được copy vào CPU map mỗi khi cần đến. Cụ thể, các PRG bank được chiếm vị trí từ $8000 đến $FFFF trong CPU Memory. Do vậy, địa chỉ $8000 trong CPU Memory sẽ là nơi bắt đầu của các PRG bank khi chúng được copy vào đây. Và như vậy, địa chỉ $E1FA trong PRG Rom sẽ tương đương với địa chỉ $81FA trong CPU Memory.

Nhìn lên một chút thì ta thấy routine này bắt đầu tại địa chỉ $E1EC trong PRG Rom, tương đương với $81EC trong CPU Memory tại thời điểm đó. Đặt break point thi hành tại địa chỉ $E1EC trong PRG Rom thì ta có được toàn bộ routine như dưới đây.

org $81EC

JSR $8F6D

LDA #$00

STA $00

_81f3:

JSR $820D

LDY #$00

-

LDA ($01),y //$9034

STA $2007

INY

CPY #$0E

BNE -

INC $00

LDA $00

CMP #$20

BNE _81f3

JMP $81BE

_820D:

LDA #$0C

STA $2000

LDA #$22

STA $2006

LDX $0000

STX $2006

LDA $900A,x

TAX

LDA $902A,x

STA $01

LDA $902B,x

STA $02

RTS

org $8F6D

SEI

LDA $1C

AND #$7F

STA $2000

LDA $1D

AND #$E6

STA $2001

RTS

Mở đầu routine này là một đoạn code tại subroutine tại $8F6D với nội dung ghi giá trị vào Register $2000 và $2001. Đây là các Register trực tiếp điều khiển các chức năng của màn hình, nhưng ở đây chúng không đóng vai trò quan trọng lắm. Tiếp theo, ta để mắt tới subroutine tại $820D. Giá trị 0x0C được ghi vào Register $2000. Giá trị này có bit 2 được set, cho thấy sau mỗi lần ghi vào PPU, địa chỉ PPU gia tăng thêm 32 tile, mà màn hình Famicom/NES gồm 32 tile (mỗi tile 8x8 pixel) nên điều này có nghĩa là sau mỗi lần ghi tile đồ họa thì tile đồ họa tiếp theo sẽ nằm ngay bên dưới tile liền trước. Điều này cho thấy khung thoại được vẽ lần lượt từ trên xuống dưới, từ trái qua phải.

Cũng tại subroutine này, ta thấy địa chỉ PPU, nơi bắt đầu được ghi hình ảnh là $2200. Đây là nơi bắt đầu được vẽ của cột khung thoại đầu tiên. Những cột tiếp theo được bắt đầu từ địa chỉ $2201, $2202,... và tất cả chúng đều được chỉ định ở địa chỉ $00 trong CPU Memory. Các ID của tile đồ họa cấu thành nên khung thoại được chỉ định ở địa chỉ ($01), cụ thể là $9304 trong CPU Memory. Các địa chỉ từ $900A trở đi là pointer chỉ đến các địa chỉ chứa ID đồ họa của khung thoại. Ta chỉ cần chỉnh sửa lại phần dữ liệu này là ta có thể thay đổi cách vẽ để mở rộng khung thoại như dưới đây.

VIII. Control code và MTE

Trong phần trước, ta đã lập được file table. Thực chất đây chỉ là file text đơn giản mà nội dung chính của nó là phép gán. Gán một giá trị hex nào đó cho một ký tự nào đó, khi đó thì những phầm mềm xem text như Windhex sẽ thể hiện text tương ứng với giá trị hex mà nó đọc được. Chẳng hạn, nếu trong file table của ta có phép gán 90=G thì khi phần mềm gặp giá trị hex 0x90 thì nó sẽ hiển thị chữ cái "G". 

Sau khi lập table, ta dễ dàng tìm thấy đoạn text mở đầu game trong Windhex. Tuy nhiên, vẫn còn một số chỗ chưa hiển thị được text như những vùng khoanh trắng dưới đây.

Điều này có nghĩa là trong file table mà ta vừa lập vẫn còn thiếu các giá trị gán. Nói cách khác là nó chưa hoàn chỉnh. Nhưng mà trong phần trước, ta đã ghi lại toàn bộ chữ cái trong Nametable vào file table này rồi, sao lại có chuyện thiếu được! Thật ra thì code table hiển thị text của một game bất kỳ không chỉ là mã hex của bảng chữ cái, mà còn bao gồm cả code điều khiển các chức năng đặc biệt, chẳng hạn như: code thông báo chấm hết câu thoại, CPU đừng đọc thêm nữa, đừng ghi ra màn hình nữa; code thông báo xuống dòng, dòng tiếp theo sẽ được viết ở vị trí thấp hơn so với dòng đầu; code thông báo thay đổi nhạc nền; code điều khiển màu chữ; code điều khiển tốc độ hiển thị chữ; vân vân. Người ta gọi chung những code chức năng này là "control code". Những code này không làm hiển thị chữ cái ra màn hình, nhưng trực tiếp điều khiển các chức năng liên quan tới văn bản như vừa kể. Control code xuất hiện trong tất cả game có hiển thị chữ ra màn hình, và Ninja mèo không phải là ngoại lệ.

Như vậy, nhiệm vụ tiếp theo của ta là bổ sung những control code này vào table để có được cái nhìn đầy đủ nhất về cách hiển thị chữ ra màn hình. Việc này khá đơn giản. Đầu tiên, cần xác định những mã hex chưa xuất hiện trong table. Sau đó, thay thế giá trị hex của một đoạn text bất kỳ mà ta đã biết trước bằng mã hex còn thiếu đó rồi quan sát sự biến đổi trên màn hình. Ví dụ, nếu thay một giá trị text bất kỳ bằng 0xF8 và kết quả là đoạn text ngay sau 0xF8 được viết thấp hơn dòng trước đó thì ta biết 0xF8 là code báo hiệu xuống dòng. Lặp lại quá trình trên cho những mã hex chưa biết khác thì ta được những kết quả sau.

F5=MTE

F7= tốc độ text

F8= xuống dòng

F9= xóa cả text

FA= đợi

FB= xóa một ký tự

FC=chuyển đổi bản chữ Kana

FD=cử động miệng cho nhân vật

FE=dừng cử động miệng cho nhân vật

FF= kết thúc text

Trong tất cả các control code này thì đáng chú ý nhất là F5. Đây là code thể hiện MTE (Multi Tile Encoding), một kỹ thuật nén text đơn giản, cho phép dùng một hoặc một số byte nhất định để thể hiện nhiều ký tự. Với cách hiển thị chữ thông thường, trên màn hình có bao nhiêu ký tự thì trong Rom chứa chừng đó mã hex tương ứng. Còn với MTE, một mã hex trong Rom có thể tương đương với nhiều ký tự trên màn hình, thậm chí là cả một đoạn văn bản. Do vậy, kỹ thuật này được vận dụng để tiết kiệm dung lượng Rom, nhưng thời gian xử lý của CPU sẽ kéo dài hơn. Mọi việc đều có cái giá của nó. Đây là một khía cạnh khác của định luật bảo toàn, một quy tắc bất di bất dịch của vũ trụ.

Thông thường, cú pháp của MTE sẽ là một byte báo hiệu MTE (trong game này là F5), nối tiếp sau đó có thể là một hoặc nhiều byte index tương đương với từng chuỗi text. Chẳng hạn:

F500=ABCDE, F501=FGHIJKLMNOPQRS, F502=TUV, vân vân.

Không khó để xác định các entry của MTE trong Ninja mèo. Ta chỉ cần làm theo cách mà đã xác định control code là được: thay thế lần lượt các giá trị F500, F501, F502,... vào vị trí text đã biết rồi quan sát kết quả thay đổi ra sao. Làm xong bước này là ta đã hoàn thiện toàn bộ table cho Ninja mèo. Kết quả như dưới đây.

IX. Hiểu về cách vẽ chữ

Ý nghĩa của việc lập table là để dễ dàng xác định được địa chỉ của text trong Rom mà không cần debug; và để dump (trích xuất) text từ Rom, chuẩn bị cho bản dịch. Tuy nhiên, để dịch được thì trước hết ta cần hiểu được cách vẽ chữ ra màn hình, để từ đó chỉnh sửa thành routine hiển thị đầy đủ dấu tiếng Việt cho bản dịch. Để xác định được routine vẽ chữ thì cần set break point tại vị trí mà chữ cái được ghi ra màn hình.

CPU dừng xử lý khi giá trị text được ghi vào PPU trong kỳ NMI. Routine NMI nằm ở bank $0F và được mapping cố định vào địa chỉ $E000~$FFFF trong CPU memory. Code ghi chữ thuộc một phần NMI như dưới đây.

org $1FCCD

PHA

TXA

PHA

TYA

PHA

LDA #$00

STA $2003 //oam adr

LDA #$02

STA $4014 //oam dma

LDA $1D

STA $2001 

LDA $1F

BEQ _fd27 //no write

LDY #$00

_fce7:

LDA $03A0,y

BEQ _fd16

TAX

BPL _fcf6

AND #$7F

TAX

LDA #$0C

BNE _fcc8

_fcf6:

LDA #$08

_fcc8:

STA $2000

INY

LDA $03A0,y

STA $2006

INY

LDA $03A0,y

STA $2006

INY

_fd0a:

LDA $03A0,y

STA $2007

INY

DEX

BNE _fd0a

BEQ  _fce7

_fd16:

STA $1F

STA $21

LDA $2002

LDA #$3F

STA $2006

LDA #$00

STA $2006

_fd27:

STA $2006

STA $2006

LDA $1C

STA $2000

LDA $2002

Đầu mỗi kỳ NMI, CPU sẽ check địa chỉ $1F, nếu giá trị tại địa chỉ này là 0x00 thì sẽ không ghi text ra màn hình. Chữ chỉ được hiển thị khi giá trị tại địa chỉ này khác 0x00. Tiếp đến, text được ghi vào PPU thông qua địa chỉ gián tiếp bắt đầu tại $03A0. Cú pháp byte tại địa chỉ này là:

$03A0: chỉ định số lượng ký tự được ghi vào PPU. Mặc định là 02. Nếu giá trị tại đây là số dương thì các giá trị text tại $03A3 và $03A4 được ghi liên tiếp theo hàng ngang (reset bit 2 của Register $2000). Nếu giá trị tại $03A0 là số âm thì các giá trị text tại $03A3 và $03A4 được ghi liên tiếp theo chiều dọc (set bit 2 của Register $2000).

$03A1-$03A2: địa chỉ PPU của text.

$03A3: dấu trọc âm/bán trọc âm.

$03A4: ký tự Kana.

$03A5: số byte được ghi của chuỗi tiếp theo.

Như vậy, các thông số cần thiết để ghi chữ ra màn hình, gồm giá trị của chữ cái, giá trị của dấu trọc âm/bán trọc âm hoặc không dấu, địa chỉ PPU của chữ cái đều được chứa tại $03A0 trong CPU memory. Tiếp theo, cần xác định những thông số này được ghi vào $03A0 tại thời điểm nào bằng cách đặt break point. Kết quả, ta được routine ghi giá trị text và địa chỉ PPU của text tại địa chỉ $8C92 của bank $07.

org $8C92

LDA #$06 //prg bank 6 mapped to $A000-$BFFF

JSR $8B9E //switch bank

LDA $043C

CMP #$F5

BCS _8d00

LDA $0438 //text speed

BNE _8cb0

LDA $0437 //pause

BEQ _8cb4

DEC $0437

LDA #$09

STA $0438

_8cb0:

DEC $0438

RTS

_8cb4:

TAY

LDA $0439

STA $0438

_8cbb:

JSR $8E50 //read text

CMP #$F5

BCS _8d00

TAY

LDA #$82

JSR $8E3E

TYA

CMP #$B0

BCC _8cee

CMP #$C0

BCS _8ce2

SEC

SBC #$B0

TAY

LDA #$5D

STA $03A3,x

LDA $8FF8,y

STA $03A4,x

BNE _8cf6

_8ce2:

AND #$7F

STA $03A4,x

LDA #$5C  //dakuten

STA $03A3,x

BNE _8cf6

_8cee:

STA $03A4,x

LDA #$20  //space

STA $03A3,x //upper letter

_8cf6:

TXA

CLC

ADC #$05

STA $21

INC $043A

RTS

_8d00:

SEC

SBC #$F5

TAY

BNE _8d22

JSR $8E50 //read txt

ASL

TAX

LDA $0C

STA $043F

LDA $A000,x //MTE PTR

STA $0C

LDA $0D

STA $0440

LDA $A001,x

STA $0D

JMP $8CBB

_8d22: //f6

DEY

BNE _8d32

LDA $043F

STA $0C

LDA $0440

STA $0D

JMP $8CBB

_8d32: //f7

DEY

BNE _8d3c

JSR $8E50 //read txt

STA $0439  //speed value

RTS

_8d3c: //f8

DEY

BNE _8d45

INC $043D //line

JMP $8E59

_8d45: //f9

DEY

BNE _8d5c

LDA #$FC

STA $043C

LDX $043D //line

LDA $9006,x

STA $0438

STY $043D

JMP $8E59

_8d5c: //fa

DEY

BNE _8d66

JSR $8E50 //read txt

STA $0437

RTS

_8d66: //fb

DEY

BEQ +

JMP $8DF7

+

STY $0438

LDX $21

JSR $8E50 //read txt

STA $03A0,x

STA $00

INX

JSR $8E50 //read txt

STA $03A0,x

STA $02

INX

JSR $8E50 //read txt

STA $03A0,x

STA $03

INX

_8d8c:

LDA ($0C),y

BPL _8da0

CMP #$B0

BCC _8da0

CMP #$C0

BCS _8d9c

LDA #$5D

BNE _8da2

_8d9c:

LDA #$5C

BNE _8da2

_8da0:

LDA #$20

_8da2:

STA $03A0,x

INX

INY

CPY $00

BNE _8d8c

LDA $00

STA $03A0,x

INX

LDA $03

CLC

ADC #$20

STA $03A1,x

BCC _8dbd

INC $02

_8dbd:

LDA $02

STA $03A0,x

INX #2

LDY #$00

_8dc6:

LDA ($0C),y

CMP #$A0

BCC _8de0

CMP #$C0

BCS _8dde

STY $04

SEC

SBC #$B0

TAY

LDA $8FF8,y

LDY $04

JMP $8DE0

_8dde:

AND #$7F

_8de0:

STA $03A0,x

INX

INY

CPY $00

BNE _8dc6

STX $21

LDA $00

CLC

ADC $0C

STA $0C

BCC _8df6

INC $0D

_8df6:

RTS

_8df7:

DEY

BNE _8e28

LDA $0438

BEQ _8e03

DEC $0438

RTS

_8e03:

LDA #$88

JSR $8E3E

INX #3

LDY #$07

LDA #$20

_8e0f:

STA $03A0,x

INX

DEY

BPL _8e0f

STX $21

INC $043A

LDA $043A

EOR #$3D

BNE _8df6

STA $043C

JMP $8E59

_8e28:

DEY

BNE _8e2f

INC $043E

RTS

_8e2f:

DEY

BNE _8e38

LDA #$80

STA $043E

RTS

_8e38:

LDA #$00

STA $0436

RTS

org $8B9E

LDX #$07

STX $36

STX $8000 //Select 8 KB PRG ROM bank at $A000-$BFFF

STA $8001  

STA $2A

RTS

org $8E3E

LDX $21

STA $03A0,x

LDA $043B

STA $03A1,x

LDA $043A

STA $03A2,x

RTS

org $8E50

LDA ($0C),y

INC $0C

BNE +

INC $0D

+

RTS

Routine này mở đầu bằng cách map PRG bank $06 vào địa chỉ từ $A000~$BFFF của CPU memory. Routine ghi giá trị text này vào $03A0 được thực thi bên ngoài kỳ Vblank, và chỉ được thực hiện khi giá trị tại $0438 là zero. Đây là địa chỉ điều chỉnh tốc độ text. Mỗi frame địa chỉ này giảm một giá trị, cho tới khi còn zero thì CPU sẽ đọc text. Nếu giá trị text nhỏ hơn 0xF5 thì sẽ ghi các giá trị chữ cái vào địa chỉ $03A3 và $03A4.

Đối với giá trị chữ cái nhỏ hơn 0xB0 thì $03A3 và $03A4 lần lượt được ghi giá trị khoảng trắng (0x20) và giá trị chữ Kana tương ứng. Đối với giá trị chữ cái từ 0xB0 trở đi và nhỏ hơn 0xC0 thì sẽ được ghi giá trị dấu trọc âm (0x5C) và giá trị chữ Kana tương ứng vào $03A3 và $03A4. Các giá trị từ 0xC0 trở đi sẽ được ghi giá trị dấu bán trọc âm (0x5D) và giá trị chữ Kana tương ứng.

Đối với giá trị từ 0xF5 trở đi sẽ là các control code đúng như ở phần trước ta đã tìm khi lập table.

X. Trích xuất text

Trước khi đi vào phần chính là hack routine hiện chữ thì ta cần trích xuất (dump) text. Mục đích của việc này là để xuất toàn bộ lời thoại trong game (tiếng Nhật) ra file text để thuận tiện cho việc dịch thuật. Với Windhex, ta dễ dàng xác định được địa chỉ của khối text trong game. Địa chỉ của những đoạn text này được chỉ định qua pointer ở địa chỉ $0C trong CPU memory. Routine đọc text ở $8E50 trong CPU memory.

org $8E50

LDA ($0C),y

INC $0C

BNE +

INC $0D

+

RTS

CPU sẽ đọc 1 byte tại địa chỉ được chỉ định ở $0C và $0D. Giả dụ, giá trị tại $0C là 53 và giá trị tạo $0D là A0 thì CPU sẽ đọc dữ liệu tại $A053 trong CPU. Do vậy, ta có thể dùng phần mềm Cartographer để trích xuất text theo giá trị pointer theo script và được kết quả như dưới đây.


 Tuy nhiên, cần lưu ý là trong Ninja mèo, không phải tất cả mọi câu thoại đều có pointer. Đây là một điểm khá lạ lùng, khác biệt với phần lớn game khác. Lời thoại trong Ninja mèo nằm trong chuỗi hoạt cảnh (anime), và người ta đặt timing cho mỗi câu thoại. Chỉ có những câu thoại mở đầu chuỗi hoạt cảnh là có pointer. Còn những câu thoại sau đó được đọc tại địa chỉ ($0C) bằng cách gia tăng giá trị tại địa chỉ này sau khi kết thúc câu thoại trước. Điều này có nghĩa rằng trong Rom không chứa pointer của những câu thoại nối tiếp những câu thoại mở đầu hoạt cảnh. Do vây, việc trích xuất text theo pointer như trên là chưa đủ, mà cần phải trích xuất theo địa chỉ tuyệt đối của text (kiểu RAW trong Cartographer).

Còn khi dịch thì cần dịch theo pointer cho câu thoại mở đầu hoạt cảnh, nối tiếp sau đó là những câu thoại sau không cần pointer. Dĩ nhiên những câu thoại phải được ngăn cách với nhau bằng Control code báo kết thúc câu (end) như ví dụ dưới đây. Đoạn text mở đầu hoạt cảnh đụng độ trùm ở màn 1 (hóa ra người ngoài Địa cầu...) được chỉ định ở pointer ($F9CA), nhưng đoạn text sau đó (Sàm xí đú....) không có pointer, mà được CPU đọc liền mạch sau khi chuỗi ở $F9CA kết thúc.

XI. Phần chính

Ta đã biết giá trị của ký tự được ghi lên màn hình trong kỳ Nmi, mỗi "ký tự" được ghi 2 lần, gồm phần trên (các dấu trọc âm, bán trọc âm) và phần dưới (chữ Kana). Do vậy, có thể dùng luôn chức năng này để ghi dấu tiếng Việt (phần trên) và chữ cái (phần dưới). Đoạn code dưới đây bổ sung thêm chức năng ghi dấu nặng ngay bên dưới chữ cái.


lda {text_trigger}

beq +

lda {sitaten}

sta $2007

lda #$00

sta {text_trigger}

lda #$4c

sta {sitaten}

+

jmp _fce7

Nguyên lý hoạt động ở đây là ngoài phần dấu bên trên, phần chữ cái thì còn ghi thêm giá trị của dấu nặng bằng cách đọc ram tại địa chỉ {sitaten}. Nếu đó là text không chứa dấu nặng thì địa chỉ này chứa giá trị của khoảng trắng. Giá trị này được ghi ngay bên dưới ký tự qua register $2007 mà không cần tính toán lại địa chỉ PPU nếu như bit 3 của register $2000 được set. Nếu trạng thái của bit này là reset thì cần phải tính toán lại địa chỉ PPU, nếu không thì dấu nặng sẽ xuất hiện theo hàng ngang, bên cạnh chữ cái thay vì ngay dưới chữ cái. Cần lưu ý trong đoạn code trên nó là một thành phần của routine đa mục đích. CPU dùng routine này để vẽ mọi thứ ra màn hình, gồm sprite, background và cả text (cũng là background), cho nên nếu mọi giá trị đều được ghi thêm một giá trị khác ngay bên dưới nó thì trên màn hình sẽ xuất hiện nhiều thành phần đồ họa không mong muốn. Do vậy ta cần giới hạn để đoạn code ghi dấu nặng trên chỉ hoạt động khi ghi text, bằng cách kiểm tra flag {text_trigger} trước khi ghi. Flag này được set tại routine đọc giá trị của chữ ngoài kỳ nmi.

Một vấn đề khác cần lưu ý khi ghi dấu nặng, là hiện tượng dấu nặng của dòng chữ bên trên bị che mất bởi dấu trên đầu (sắc, huyền, hỏi, ngã, móc, mũ,... hay không dấu) của hàng dưới. Chi tiết về khuyết điểm này cũng như cách khắc phục đã được nêu rõ trong bài viết về Contra (click).

Một điểm lưu ý khác là text gốc (tiếng Nhật) có dung lượng nằm gọn trong một bank (0x2000 byte), nhưng khi dịch thì dung lượng này sẽ phình ra rất nhiều. Đó là quy luật tất yếu của mọi bản dịch. Do vậy, một bản dịch đàng hoàng tử tế luôn chiếm dung lượng gấp vài lần so với bản gốc. Để khắc phục giới hạn này thì ta cần mở rộng Rom bằng phần mềm nflate rồi ghi dữ liệu text vào những bank trống, buộc CPU đọc dữ liệu text ở bank mới bằng cách chuyển đổi bank.

Ninja mèo là Rom với mapper MMC3, có cách chuyển đổi bank bằng cách ghi giá trị vào register $8000 và $8001. 

Nếu muốn map (bố trí) bank mới vào địa chỉ tử $A000 đến $BFFF của CPU memory thì cần set 3 bit đầu tiên của register $8000. Còn giá trị được ghi vào register $8001 chính là giá trị của bank mới. Đoạn code dưới đây map bank $13 vào địa chỉ $8000~9FFF trong CPU memory.

LDA #$06

STA $8000

LDA #$13

STA $8001

Ta có thể phân bổ text của mỗi màn chơi vào một bank khác nhau bằng cách check địa chỉ chỉ định ID màn chơi trước khi chuyển đổi bank và đọc text ở bank mới. ID màn chơi được chỉ định ở địa chỉ $5D trong CPU memory.

XII. Phần phụ

Ngoài phần ghi text thì bản dịch cần phải trải qua rất nhiều công đoạn chỉnh sửa để hoàn thiện. Chẳng hạn như chỉnh lại vị trí của con trỏ di chuyển, vì độ dài của text gốc và test sau dịch khác nhau; chỉnh lại vị trí khung thoại; sửa lại những đoạn text phụ như màn hình Game over, màn hình nhập Password; chỉnh sửa lại màn hình đầu tiên;... Những công đoạn này tốn nhiều thời gian và đôi khi là khó hơn cả phần text chỉnh. 

Bản dịch Việt ngữ của Ninja mèo này ở mức hoàn thiện 100% những thứ cần chỉnh sửa. Bạn có thể tìm thấy mọi thứ từ tập mã nguồn bản dịch ở phần bên dưới.

XIII. Tải bản dịch và mã nguồn

 Tải bản dịch và mã nguồn từ một trong các liên kết dưới đây.