此儲存庫包含 Atari 2600 經典遊戲《法櫃奇兵》已完全逆向工程並加上註解的原始程式碼。
專案已重新組織,以實現乾淨的開發工作流程:
DASM:dasm.exe 必須放在 bin/ 資料夾中。
Stella:Stella.exe 和 SDL2.dll 必須放在 bin/ 資料夾中(可選,用於執行)。
dasm:必須在系統上安裝 dasm。
stella:必須在系統上安裝 stella。
從根目錄執行建置腳本:
遊戲使用 2 個銀行 ROM(總計 8KB),透過 BANK0STROBE ( $FFF8 ) 和 BANK1STROBE ( $FFF9 ) 的 strobe 進行銀行切換。銀行切換是透過一種自我修改程式碼技術來完成的,其中指令被寫入零頁 RAM 變數並原地執行。
與所有 Atari 2600 遊戲一樣,程式圍繞 NTSC 電視訊號建構——迴圈完整通過一次產生一幀畫面(約 60 fps)。畫面分為四個階段:VSYNC、VBLANK、Kernel(可見畫面)和 Overscan。遊戲邏輯分散在 VBLANK 和 Overscan 中,以在每個階段的 CPU 時間預算內完成,並且每幀畫面都會在特定點切換兩個 ROM 銀行。
CPU 斷言 VSYNC 訊號恰好 3 個掃描線,期間執行輕量級的維護工作:
所有遊戲邏輯在 TIA 輸出空白畫面時執行。工作分散在兩個 ROM 銀行中,在此階段進行兩次銀行切換。
這是最大的遊戲程式碼區塊,每幀畫面按順序執行:
在 dispatchRoomHandler,程式碼將 selectRoomHandler 寫入為目標位址,並透過 jumpToBank1 trampoline 跳轉。
selectRoomHandler 透過 roomHandlerJmpTable 進行分派——每個房間都有自己的 handler,執行房間特定的 AI 和物理運算:
所有 handler 透過 jmpSetupNewRoom 退出,該函式將銀行切換回 Bank 0。
setupNewRoom 為 TIA 準備顯示:
然後 setThievesPosX 使用粗略/精確 HMOVE 技術定位所有 5 個 TIA 物件(P0、P1、M0、M1、Ball)。這會消耗 6 個掃描線(每個物件一個 WSYNC + 一個用於 HMOVE)。
最後,waitTime 在 INTIM 上旋轉,直到 VBLANK 計時器過期,然後切換到 Bank 1 執行 drawScreen。
最初可見的幾行設定了顯示狀態:
kernel index table 將每個房間映射到四個 kernel 中的一個:
staticSpriteKernel(Treasure Room、Marketplace、Entrance、Black Market、Map Room、Mesa Side):每迴圈兩個掃描線。掃描線 1:HMOVE,檢查蛇/球繪製範圍,透過與其 Y 位置比較來啟用飛彈 M0(網/群)和 M1(武器)。掃描線 2:繪製 P0 精靈——圖形資料的位元 7 編碼了內嵌的 TIA 暫存器寫入(顏色/HMOVE),而不是形狀資料,允許 P0 在畫面中間變更顏色或位置。透過掃描線比較啟用球體。
scrollingPlayfieldKernel(Temple Entrance、Spider Room、Shining Light、Mesa Field、Valley of Poison):每迴圈兩個掃描線,帶有畫面捲動。每次迭代,掃描線與 p0DrawStartLine 進行比較——在此邊界上方,kernel 索引(pf1GfxPtrLo)、y /(pf2GfxPtrLo)、y 加上捲動偏移量(roomObjectVar)來渲染捲動的牆壁。在此邊界下方,dynamicGfxData 渲染可破壞的地牢牆壁片段或清除的通道。兩條路徑都透過掃描線對比 Y 位置來繪製 P0 和 P1(Indy)。球體物件透過(kernelDataPtrLo)、y 驅動以實現蛇的擺動動畫。
multiplexedSpriteKernel(Thieves' Den、Well of Souls):使用單一 P0 硬體精靈顯示 5 個敵人盜賊,在每個盜賊區域(約 32 個掃描線)之間重新定位。透過 kernelRenderState 作為狀態機運作:定位階段(位元 7)使用粗略/精確延遲迴圈來放置 RESP0,繪製階段(位元 6)串流(p0GfxPtrLo)、y 和(kernelDataPtrLo)、y 以用於 16 行的精靈和顏色資料。在盜賊區域之間,P1(Indy)使用 PHP/堆疊技巧繪製——txs 重定向堆疊指標,以便 php 直接寫入 TIA 啟用暫存器(ENABL / ENAM1 / ENAM0 在 $1F / $1E / $1D)。
arkPedestalKernel(Ark Room —標題和結尾畫面):單一高度掃描線(每行 1 個 WSYNC)。第 0–14 行:繪製具有彩虹色循環(僅在獲勝狀態下)的 Ark 頂部/翅膀精靈。第 15–28 行:繪製具有交替金色圖案的 Ark 主體。第 29–143 行:根據 adventurePoints 繪製 Indy 在基座高度的站立精靈——Indy 上方是空的,下方是 diamond-pattern pedestalLiftSprite。第 144–159 行:繪製基座底部。
所有四個房間 kernel 在 drawInventoryKernel 處匯合,該函式繪製螢幕底部的庫存條:
庫存 kernel 之後,立即將 TIA 清空(VBLANK = $0F)並啟動 overscan 計時器:TIM64T = OVERSCAN_TIME (36)。這提供了 36 × 64 = 2,304 個週期 ≈ 30 個掃描線。堆疊指標在此處也重設為 $FF。
在 jmpObjHitHandeler,程式碼切換到 Bank 0 並進入碰撞分派鏈。這會讀取在 kernel 期間被鎖定的 TIA 碰撞暫存器:
碰撞鏈完成後,執行會落入 newFrame,該函式在 INTIM 上旋轉,等待 overscan 計時器過期——然後迴圈再次開始。
兩個銀行都包含一個對稱的 trampoline 例程(Bank 0 中的 jumpToBank1,Bank 1 中的 jumpToBank0)。trampoline 將自我修改程式碼寫入零頁 RAM(temp0 – temp5):
呼叫者在跳轉到 trampoline 之前將 temp4 / temp5 設定為目標位址。執行 jmp temp0 會讀取銀行 strobe(切換 ROM)並立即跳轉到新銀行中的目標標籤。
每幀畫面的銀行切換(按執行順序):
Bank 1 在其重設向量入口處還有一個安全存根(bank1Start)——如果 Bank 1 在開機時進入,它會立即讀取 BANK0STROBE 切換回 Bank 0。
死亡序列:設定 indyStatus 位元 7 會在 overscan 期間觸發死亡溶解效果。Indy 的精靈高度每 16 幀縮小一行。一旦只剩下帽子(高度 < 3),標誌就會旋轉,精靈消失 60 幀。在第 120 幀,Indy 會以完整高度重生,livesLeft 會遞減。如果生命值降至零以下,indyStatus 會被設定為 INDY_GAMEOVER — 在下一幀,VBLANK 溢位檢查會捕捉到這一點並轉換到 Ark Room。
房間轉換:由 handleIndyMove 中的邊界覆寫系統觸發。當 Indy 穿過房間邊緣時(透過 checkRoomOverrideCondition 與每個房間的邊界表進行檢查),currentRoomId 會被更改,initRoomState 會重新初始化所有房間特定狀態——精靈、畫面指標、物件位置和事件標誌。特殊轉換包括:Mesa Side fall into Valley of Poison(Y 位置檢查)、M0 碰撞 into Well of Souls、吹開的牆壁對齊 into the Temple,以及 Ankh 傳送直接到 Mesa Field。
遊戲使用 4 種不同的掃描線 kernel,透過 kernelJumpTableIndex 和 kernelJumpTable 選擇:
每個 kernel 以不同的方式處理 TIA 暫存器寫入,以適應這些房間的視覺需求——盜賊 kernel 管理掃描線上的多個 P0 物件,而畫面 kernel 處理捲動的地牢牆壁。
staticSpriteKernel 房間(0–5)使用巧妙的資料編碼來處理 Player 0,允許單一位元組串流同時包含像素資料和內嵌的 TIA 暫存器命令。這由 drawP0GraphicsStream 例程處理。
*playerGraphics 資料表(例如 marketplacePlayerGraphics、entranceRoomPlayerGraphics)中的每個位元組每掃描線讀取一次。該例程檢查位元 7 以確定位元組的含義:
當位元 7 設定時,該位元組同時編碼目標暫存器和載荷值:
兩個組合語言常數簡化了這些命令位元的編寫:
>> 1 是必需的,因為 ASL 在解碼過程中會將載荷向左移位。
一個關鍵細節:GRP0 在命令掃描線之間不會被清除。處理命令位元組時,TIA 暫存器會被更新,但 GRP0 會保留最後寫入的像素模式。這意味著相同的精靈形狀會在多個掃描線上持續顯示,同時其顏色或位置會發生變化。
這被大量用於多色精靈。例如,marketplac 的賣家使用單一 $7E(.XXXXXX.)像素模式,該模式在快速顏色變換中持續存在——黑色(帽子)→灰色(條紋)→粉紅色(臉部)→黑色(身體)——從單一重複的形狀中產生了多色角色的錯覺。
HMOVE 命令在資料串流中有兩個用途:
專案之間的重新定位:大型移動(例如 HMOVE_L7、HMOVE_R8)將 P0 移動到螢幕上的新位置以顯示下一個視覺元素。GRP0 通常在此類掃描線中設定為 $00,以便在移動期間不可見任何內容。
對角線形狀:持續的小型移動(例如每掃描線 HMOVE_R1)每行將精靈移動 1 個像素,產生傾斜的形狀。Entrance Room 的鞭子使用 HMOVE_R3 漂移,Marketplace 的長笛使用 HMOVE_R1 漂移來產生其對角線角度。
鋸齒邊緣:交替的 HMOVE_L1 / HMOVE_R1 命令與持續的 GRP0 模式結合,產生不規則、自然的剪影。Entrance Room 的洞口使用 15 個連續的 HMOVE jitter 命令來產生其粗糙的石牆邊緣。
每個 staticSpriteKernel 房間都有自己的 *playerGraphics 資料表:
每個房間的初始 P0 顏色在 kernel 前設置階段從 roomP0ColorTable 設定,但通常會被資料串流中的第一個 SET_PLAYER_0_COLOR 命令立即覆寫。
定義了 14 個房間作為常數(ID $00 – $0D):
房間轉換會呼叫 initRoomState,該函式從表中載入每個房間的資料:roomBGColorTable、roomPFColorTable、pfControlTable、objectPosXTable、roomObjPosYTable、畫面圖形指標和精靈資料。
每個房間都有一個 handler,從 Bank 1 的 roomHandlerJmpTable 分派,該 handler 每幀執行房間特定的邏輯。
玩家最多可以攜帶 6 個物品(MAX_INVENTORY_ITEMS)。每個槽位是一對零頁指標(invSlotLo / invSlotHi 到 invSlotLo6 / invSlotHi6),直接指向 inventorySprites 表中的 8 位元組精靈資料。
物品 ID 在組合時計算為相對於精靈表的偏移量:
選擇使用左類比搖桿。selectedItemSlot 追蹤游標位置(槽位陣列的位元組偏移量 0,2,4,6,8,10),selectedInventoryId 儲存當前物品的 ID。物品透過 placeItemInInventory 添加,透過 removeItem 移除。
遊戲使用兩個獨立的位元遮罩來追蹤已收集的物品:
itemIndexTable 確定哪個位元遮罩適用於每個物品:偶數索引使用 basketItemStatus,奇數索引使用 pickupItemStatus。helper 例程 showItemAsTaken、setItemAsNotTaken 和 isItemAlreadyTaken 透明地處理位元操作。
分數(adventurePoints)從 INIT_SCORE(100)開始,並在 getFinalScore 中修改。分數越低越好——減去獎勵並加上懲罰。
最終值決定了 Ark Room 中 Indy 的基座高度。與傳說不同,你無法獲得足夠的分數來「觸摸」Ark。
在 playerHitInWellOfSouls 中實現。必須同時滿足所有三個條件:
當所有三個條件都滿足時,arkRoomStateFlag 會被設定為正值,觸發 arkPedestalKernel 中的結尾序列,該序列將 Ark 顯示在 Indy 基座上方。
在 calculateMesaGrapple 中計算——將鉤子的像素位置轉換為網格 ID:
在 mapRoomHandler 中,當 Indy 拿起 Head of Ra 並且太陽(由 timeOfDay 控制)處於正確位置時,一道光束會利用 mapRoomArkLocX / mapRoomArkLocY 的資料揭示 Ark 的高原。
timeOfDay 每約 63 幀(大約每秒一次)遞增,由 VBLANK 部分驅動。它控制時鐘顯示、寶藏室旋轉、太陽位置和 Head of Ra 定時。
在 handleMesaScroll 中處理——當 Indy 接近螢幕邊緣時,相機偏移量 roomObjectVar 會移動所有物件的位置,邊界為 MESA_MAP_MAX_HEIGHT($50)。
兩個頻道帶有效果計時器(soundChan0Effect、soundChan1Effect)。當使用 RAIDERS_MARCH($9C)觸發時,Raiders March 從 raidersMarchFreqTable 播放。長笛旋律使用 snakeCharmFreqTable 和 SNAKE_CHARM_SONG($84)。
在 checkForArkRoom 中觸發——在 Flying Saucer Mesa 上找到 Yar(透過 checkMarketYar)會設定 yarFoundBonus。當 yarFoundBonus 設定且 arkRoomStateFlag 位元 7 清除時,Howard Scott Warshaw 的姓名縮寫(devInitialsGfx0 / devInitialsGfx1)會載入到庫存顯示槽中。
感謝 Dennis Debro 分享他的逆向工程嘗試,以便我能將其與我的結合。
逆向工程 Atari 2600 的《法櫃奇兵》