本文同步發布至 CCNS Weekly

如果各位有玩過 GBA/NDS,如果你玩的不是正版卡夾,或是買過攻略本就肯定有聽過/使用過作弊碼,也就是俗稱的金手指,只要輸入一串數字就可以讓玩家成為造物主,穿牆、跳關、無敵、無限道具…幾乎什麼都做得到

筆者在小學時是個神奇寶貝迷,GBA 上綠寶石重破了 5 次,所有路線跟劇情可以說是倒背如流,但是神奇寶貝的 NDS 卡夾一張動輒一兩千,實在沒那個羞恥心每次都請父母買給自己,於是買了一張 R4 燒錄卡,有幸體驗到 NDS 最輝煌的那個年代,也開啟了我的遊戲修改之旅,不過那時候的我還只是個 script boy(?),雖然想瞭解但無奈能力不足,於是就一直懷抱著疑問直到今天

所以接下來就讓我們抱著這懷舊的心情來看看,究竟金手指是怎麼運作的吧!

當然,有辦法負擔的話還是請各位支持正版

簡介

金手指是什麼,不就外掛?

「金手指」這個名稱對於有自組電腦經驗的朋友們來說應該不陌生,如果要將兩塊 PCB 版相互連結,一般我們都會用到由多組長條狀銅墊連續排列而成的邊接,因為其長相和手指有那麼一點點的相似因而得名「金手指」,例如 USB 接頭裡、顯卡、記憶體下方的那排銅墊都是

至於為甚麼會成為掌上遊戲修改器/作弊碼的代名詞,則是因為早期一些遊戲修改工具會需要在遊戲讀取完畢進入遊戲前或是在遊戲中將遊戲卡匣拔出,插入另一張電路版再進入遊戲,因為他沒有一般遊戲卡匣的外殼(有的甚至也沒有記憶體),而只有一些銅鍍電路和金手指在上面,所以這張修改遊戲專用的電路版就被暱稱為「金手指」,後來也就被延伸使用到作弊碼 (Cheat code) 上。參考

在 2020 年的今天,NDS/GBA 的模擬器早已多不勝數,比較知名的有 No$GBA、Visual Boy Advance、mGBA…等,甚至在手機 Google Play 上隨便找都有好幾款能夠順暢運行的模擬器,而在這些模擬器上金手指功能可以說是標準配備,畢竟現代人可能沒那麼多時間體驗遊戲 (唉)

比較細心一點的人可能會發現一些模擬器支援兩種不同的金手指,分別是 Action ReplayCode Breaker,實際上前者是由 Datel 這家英國廠商所製造的作弊卡帶 (Cheat cartridges),Action Replay 這個產品的歷史悠久,從 Commodore 64 開始、NES、GC、GBA、PS、XBOX、NDS,甚至也支援 3DS,後者則是他的競爭對手,由 Pelican Accessories 開發,不過支援的裝置相對就少了不少

打幾串數字就可以改遊戲?

不知道大家有沒有想過,為什麼很多金手指明明只有短短幾行卻可以改出一整排的道具?

答案其實意外的簡單,每一個金手指其實就是一個小小的程式,有自定義的指令集和對應的直譯器,基本上無論是硬體還是軟體模擬的金手指,運作的方式就是不斷地在背景執行那個小程式去修改記憶體內容,且由於在 GBA/NDS 中,每次遊戲從 ROM 載入到 RAM 時都會在相同的位置上,所以大多數的金手指中的寫入位置都是預先找好,然後寫死到裡面的

該教我怎用了吧

接下來文章中只會使用 Action Replay 的語法(Manual),且為了方便解釋,以[]代表對指標取值,取的 bit 數會在後方括號中註記

Action Replay 的指令以 8byte 為單位,書寫時分為左右各 4bytes,且任何值都是採 little-endian 編碼

符號含義

  • ????: 可為任意值
  • XXXX: Address
  • YYYY: Data
  • ZZZZ: Bit mask
  • NNNN: Counter
  • VVVV: Data
  • offset: Action Replay 內建的 32bit register 用以設定 address offset (下方任何 X 實際上皆為 X+offset,預設為 0)
  • stored: Action Replay 內建的 32bit register 用來暫存資料

記憶體讀寫

  • 0XXXXXXX YYYYYYYY
    • [X] = Y (32bit)
  • 1XXXXXXX ????YYYY
    • [X] = Y (16bit)
  • 2XXXXXXX ??????YY
    • [X] = Y (8bit)
  • EXXXXXXX NNNNNNNN VVVVVVVV VVVVVVVV * ((NNNNNNNN+7)/8)
    • memcpy(X, V, N) 從 X 記憶體位置開始寫入 V array,共寫入 N bytes
  • FXXXXXXX NNNNNNNN
    • memcpy(X, offset, N) (X 計算時不須加上 offset)

流程控制

  • 3XXXXXXX YYYYYYYY
    • if [X] < Y (32bit)
  • 4XXXXXXX YYYYYYYY
    • if [X] > Y (32bit)
  • 5XXXXXXX YYYYYYYY
    • if [X] == Y (32bit)
  • 6XXXXXXX YYYYYYYY
    • if [X] != Y (32bit)
  • 7XXXXXXX ZZZZYYYY
    • if ([X] & ~Z) < Y (16bit)
  • 8XXXXXXX ZZZZYYYY
    • if ([X] & ~Z) > Y (16bit)
  • 9XXXXXXX ZZZZYYYY
    • if ([X] & ~Z) == Y (16bit)
  • AXXXXXXX ZZZZYYYY
    • if ([X] & ~Z) != Y (16bit)
  • D0?????? ????????
    • endif 標註 if block 結尾
  • C??????? NNNNNNNN
    • do N times 重複執行 code block N 次
  • D1?????? ????????
    • endrepeat 標註 repeat block 結尾
  • D2?????? ????????
    • endcode 結束所有 if/repeat block,並重置 offset/stored register

core register 操作

  • BXXXXXXX ????????
    • offset = [X] (32bit)
  • D3?????? YYYYYYYY
    • offset = Y
  • D4?????? YYYYYYYY
    • stored += Y
  • D5?????? YYYYYYYY
    • stored = Y
  • D6?????? XXXXXXXX
    • [X] = stored; offset += 4 (32bit)
  • D7?????? XXXXXXXX
    • [X] = stored & 0xFFFF; offset += 2 (16bit)
  • D8?????? XXXXXXXX
    • [X] = stored & 0xFF; offset += 1 (8bit)
  • D9?????? XXXXXXXX
    • stored = [X] (32bit)
  • DA?????? XXXXXXXX
    • stored = [X] (16bit)
  • DB?????? XXXXXXXX
    • stored = [X] (8bit)

分析

要熟悉金手指該怎麼用,最快的方式當然就是抄別人的來看,
接下來我以神奇寶貝白金版(2641 - Pokemon Platinum (J))為例進行分析

按下 SELECT 修改金錢至 999999

94000130 FFFB0000
B2101140 00000000
00000090 000F423F
D2000000 00000000

將其轉成 pseudo-code

if ([0x4000130] & 0x4) == 0:
    offset = [0x2101140]
    [offset + 0x90] = 0xF423F
    endcode

這樣就可以很清楚的瞭解到,這個小程式會透過檢查 0x4000130 (KEYINPUT),判斷是否 SELECT 鍵有被按下後,才會將角色的金錢改為 999999

至於這個 offset 該怎麼取得的問題,最簡單的做法就是動態分析,透過觀察程式修改了那些記憶體地址去反推,另外當然也可以試試看靜態分析,不過相對更考驗玩家的逆向技巧了

有關 NDS 的記憶體配置可以參考這裡

按住 R+B 穿牆

94000130 FEFD0000
12060458 0000E00A
120604BE 0000E001
1206055C 0000E008

轉成 pseudo-code

if ([0x4000130] & 0x12) == 0:
    [0x2060458] = 0xE00A
    [0x20604BE] = 0xE001
    [0x206055C] = 0xE008

穿牆的金手指就不只是改改數值而已,它是透過將部分檢查碰撞的程式碼 patch 掉來達成,很大可能是透過逆向分析得出的。

可是開穿牆還要按住兩顆按鈕也太麻煩,尤其如果是使用模擬器還要看鍵盤臉色…
所以讓我們直接把第一行拿掉,這樣隨時都會處於穿牆狀態了XD

[0x2060458] = 0xE00A
[0x20604BE] = 0xE001
[0x206055C] = 0xE008

瞭解金手指語法的其中一個優勢就是,就算不懂修改細節也還能夠自定義按鍵行為

變更遭遇野生神奇寶貝數值

yy 為等級,xxx 為圖鑑編號

9224012E 0000AA15
02240130 47184B00
02240134 02000021
E2000020 00000028
4E071C3B 60161C40
809626yy 80D63108
280C3208 46C0DBF5
47004800 02240149
00000xxx 46C046C0
D0000000 00000000

轉成 pseudo-code

V = [
    0x4E071C3B 0x60161C40
    0x809626yy 0x80D63108
    0x280C3208 0x46C0DBF5
    0x47004800 0x02240149
    0x00000xxx 0x46C046C0
]

if ([0x224012E] & 0xFFFF) == 0xAA15:
    [0x2240130] = 0x47184B00
    [0x2240134] = 0x02000021
    memcpy(0x2000020, V, 0x28)
endif

這個的原理就作為練習題,交給有興趣的讀者分析了

寫在後面

如果有讀者想自己寫神奇寶貝的金手指,或是對 Pokemon ROM hack 有興趣的話,上個月剛好有一波從 Gen1-Gen4 的 leak,有興趣的朋友們可以去找找看