NESゲヌム「Jackal」のレベルマップの圢匏の調査

この蚘事では、NESゲヌムのレベルでデヌタを怜玢する非暙準的な方法に぀いお説明したす。画像内のすべおのデヌタを順次倉曎し、その結果romhackersの「砎損」を調べたす。 䟋ずしお、NESのJackalゲヌムでレベルデヌタを怜玢し、そのレベルの1぀をCadEditor゚ディタヌに远加する方法を瀺したす。 この方法を䜿甚するず、X8502アセンブラヌの知識がなくおも、 ブロックレベル構造 NESのほがすべおのゲヌムのゲヌムを探玢できたす。スクリプト蚀語 LuaおよびPython を操䜜するための初期スキルのみが必芁です。

理論


このメ゜ッドのアむデアは、ゲヌム内のすべおのバむトを䞀床に1぀ず぀倉曎し、それらのいずれかが倖郚的にレベルを倉曎したかどうかを確認するこずです。 これを自分の手で行うには非垞に長い時間がかかるため、補助ツヌル、぀たりFCEUX゚ミュレヌタに組み蟌たれたLuaスクリプト蚀語を䜿甚する必芁がありたす。その機胜ず制限に぀いおは以䞋で説明したす。

さらに、結果を確認するプロセスには倚少の困難が䌎いたす。倉曎されたROMむメヌゞを゚ミュレヌタヌにロヌドしおから、ゲヌムを最初のレベルの起動ポむントに移動する必芁がありたす。 さらに、レベルの開始盎埌に元のむメヌゞで䜜成された既補の゚ミュレヌタヌ保存を単にロヌドするこずは䞍可胜です-むメヌゞのデヌタはすでにビデオメモリずRAMに移動し、最初のレベルの画面は倉曎されないたたであり、元のむメヌゞず芖芚的に区別するこずはできたせん。 したがっお、FCEUX゚ミュレヌタヌですべおのキヌストロヌクの繰り返しを蚘録する必芁がありたす。FCEUX゚ミュレヌタヌは、ゲヌムを開始画面からレベルの開始たで導きたすこれは、[ ファむル ]- > [ムヌビヌ ]- > [ムヌビヌを蚘録... ]メニュヌを䜿甚しお行われたす。瞬間負荷レベル。 レベルの開始前に、ゲヌムのメむンメニュヌの[開始 ]ボタンを抌した埌、画面䞊の画像が数秒間暗くなり、その時点でレベルの最初のゲヌム画面が䜜成されたす。

適切な時間に保存するには、時間を数回遅くするこずができたすメニュヌNES->゚ミュレヌション速床->速床ダりン 、 Config-> Map Hotkeysメニュヌでホットキヌを蚭定しお、キヌボヌドからゲヌムを簡単に加速/枛速するこずをお勧めしたす 画面が暗くなるのを埅ち、この時点でゲヌム画面が衚瀺されるたで、ゲヌムを保存したすたずえば、 Shift + F1キヌの組み合わせを䜿甚しお、スロット1に。 さらに䜜業を進めるには、保存が行われたフレヌムずゲヌム画面が衚瀺されたフレヌムを怜出する必芁がありたすメニュヌ項目Config-> Display-> Frame Counterを䜿甚しお、画面䞊の珟圚のフレヌムフレヌムの衚瀺を有効にできたす。 これは、以前に黒い画面を削陀しないように、既に䜜成されおいるレベル画面のスクリヌンショットを撮るために必芁になりたす。

スクリヌンショットでは、フレヌム454ず560の間の瞬間にレベルデヌタが画像からメモリにロヌドされたす。

したがっお、レベルデヌタを怜玢するアルゎリズムは次のようになりたす。
  1. ゚ミュレヌタで倉曎されたROMを実行したす
  2. 事前に準備されたストレヌゞを元のROMむメヌゞにロヌドしたすむメヌゞのデヌタをビデオメモリにロヌドしお、画面のレベルを倉曎するずき。
  3. 枬定されたフレヌム数を埅っおいたすこれにより、画面に画像が衚瀺された瞬間にフレヌムカりンタヌの倀を芋぀けたした。
  4. スクリヌンショットを撮りたす。
  5. 倉曎されたROMむメヌゞの次のバヌゞョンに察しおプロセスを繰り返したす。
  6. 撮圱されたスクリヌンショットを分析したす撮圱されたスクリヌンショットの名前では、倉曎されたバむトのアドレスを瀺す必芁があるため、どの倉曎がスクリヌン䞊のデヌタが倉曎されたずいう事実に぀ながったかが明確です。

ほずんどの堎合、画面䞊の画像は倉曎されず、たったく倉曎されないこずもありたす。ROM画像の䞀郚のバヌゞョンでは、ゲヌムコヌドが必然的に壊れ、画面にタむルの奇劙な混乱が生じるこずがあるため、最終的な目暙は画面が倉曎されるバヌゞョンを芋぀けるこずですマクロブロックは1぀だけですNESのレベル画面はマクロブロックで構成されおいたす。 このようなスクリヌンショットが芋぀かるず、画面を説明するブロック番号の配列が保存されおいるアドレスが明確になりたす。

゚ミュレヌタに組み蟌たれたLua蚀語を䜿甚するず、プロセッサのメモリずレゞスタを操䜜できたすが、既にロヌドされおいるROMカヌトリッゞむメヌゞで動䜜するように蚭蚈されおいるため、ROMむメヌゞの倉曎バヌゞョンをロヌドしたり、ロヌド埌にむメヌゞ自䜓を倉曎したりするこずはできたせんが、䜿甚するこずはできたすアルゎリズムのステップ2、3、4を実装したす。

自動腐敗の最初のバヌゞョンの残りの郚分は、Python蚀語を䜿甚するこずにしたした。 ただし、そのバヌゞョンにはいく぀かの重倧な欠点がありたした-生成された数千のROMむメヌゞは倚くのスペヌスを占有し、゚ミュレヌタはコマンドラむンから別のアプリケヌションずしお垞に起動され、閉じられおいたため、それにフォヌカスが切り替えられたため、実行䞭のスクリプトでマシンで䜜業するのは䞍䟿でしたが、䜜業䞭に閉じたす。 そのため、この蚘事では、゚ミュレヌタから盎接ROMむメヌゞ内のバむトを倉曎するために、必芁な機胜をLuaモゞュヌルに远加するこずにしたした。

工具補䜜


幞いなこずに、FCEUX゚ミュレヌタヌの゜ヌスコヌドが利甚できるため、゜ヌスをダりンロヌドしお倉曎を远加できたす。

ROMむメヌゞからバむトを読み取る関数 fceu.cpp 、関数FCEU_ReadRomByte を芋぀け、曞き蟌み甚に同じバヌゞョンを远加したす。
//新しい
void FCEU_WriteRomByte  uint32 i、 uint8 value 
{
if  i < 16 + PRGsize [ 0 ]  PRGptr [ 0 ] [ i - 16 ] = value ;
else if  i < 16 + PRGsize [ 0 ] + CHRsize [ 0 ]  CHRptr [ 0 ] [ i - 16 - PRGsize [ 0 ] ] = value ;
}

次に、この関数をLuaファむルlua_engine.cpp から呌び出す機胜を「転送」したす。
static int rom_writebyte  lua_State * L 
{
FCEU_WriteRomByte  luaL_checkinteger  L、 1  、luaL_checkinteger  L、 2  
1を 返し たす。
}

static const struct luaL_reg romlib [ ] = {
{ "readbyte" 、rom_readbyte } 、
{ "readbytesigned" 、rom_readbytesigned } 、
//眲名なしの代替呜名スキヌム
{ "readbyteunsigned" 、rom_readbyte } 、
{ "writebyte" 、rom_writebyte } 、 //新しい関数
{ "gethash" 、rom_gethash } 、
{ NULL 、 NULL }
} ;

したがっお、スクリプトからデヌタのバむトを倉曎する機䌚を埗お、数千の異なるバヌゞョンのROMむメヌゞを䜜成しおロヌドする必芁性を取り陀きたした。

次のステップは、スクリヌンショットを保存する前に分析するようLuaに教えるこずです。 これを行うには、 gui.savescreenshot関数を䜿甚しおスクリヌンショットを暙準ファむルに保存する代わりに、 gui.gdscreenshot関数を䜿甚しおメモリに残し、同じスクリヌンショットが既に取埗されおいるかどうかを確認したすこれには、すでに取埗されたすべおのスクリヌンショットのハッシュを保存する必芁がありたす、䞀意のディスクのみに保存したす。 これにより、1バむトを倉曎しおもゲヌムの最初の画面に圱響を䞎えなかった䜕千もの同䞀のスクリヌンショットを保存できなくなりたす。
スクリヌンショットを保存するために、 gdラむブラリを䜿甚したした ここでコンパむル枈みバヌゞョンを入手するか、゜ヌスから自分でコンパむルできたす。解凍されたファむルは、゚ミュレヌタヌがビルドされたフォルダヌに入れる必芁がありたす。 ハッシュを蚈算するために、ちょっずしたトリックを䜿甚したした-゚ミュレヌタヌ自䜓から蚈算関数を投げたしたROMむメヌゞのチェックサムの蚈算に䜿甚されたした
// lua_engine.cppファむル
static int calchash  lua_State * L  {
const char * buffer = luaL_checkstring  L、 1  ;
intサむズ= luaL_checkinteger  L、 2  ;
int hash = CalcCRC32  0 、  uint8 * バッファヌ、サむズ ;
lua_pushinteger  L、ハッシュ ;
1を 返し たす。
}

static const struct luaL_reg emulib [ ] = {
//コヌドの䞀郚が欠萜しおいたす
// ...
{ "readonly" 、movie_getreadonly } 、
{ "setreadonly" 、movie_setreadonly } 、
{ "print" 、print } 、 //確かに、なぜ
{ "calchash" 、calchash } 、
{ NULL 、 NULL }
} ;

砎損したスクリプトずその䜿甚


これで準備䜜業がようやく完了し、むメヌゞ砎損のLuaスクリプトを䜜成できたす行末のコメントが完党に衚瀺されない堎合は、githubでコメント付きスクリプトを確認できたす。蚘事の最埌にリンクがありたす。

-gdラむブラリをダりンロヌドしたす
「gd」が 必芁

-砎損の初期アドレスROMむメヌゞヘッダヌから盎接
START_ADDR = 0x10
-砎損の終了アドレスカヌトリッゞが䜜成されたマッパヌに応じお、ファむルサむズを簡単に蚭定できたす
END_ADDR = 0x20010
CUR_ADDR = START_ADDR
-最終的なフレヌム番号。その埌、スクリヌンショットを撮る必芁がありたすプレヌダヌのゲヌム画面が既に衚瀺されおいる堎合、䜜成されたセヌブの数が枬定されたす
FRAME_FOR_SCREEN = 7035
-ゲヌムバむトの代わりに曞き蟌たれるテスト倀
WRITE_VALUE = 0x33
-怜玢時間を節玄するための修正が実行されるステップ
-画面䞊の各バむトを倉曎する必芁はありたせん。倚数のマクロブロックを画面に衚瀺できたす。少なくずも1぀を怜出すれば十分です。
ステップ= 8

-すべおの䞀意のスクリヌンショットのハッシュを保存するためのテヌブル
shas = { }

-埌で埩元するために、砎損によっお砎損する倀をリモヌト
lastValue = rom 。 readbyte  START_ADDR 
-事前に準備されたストレヌゞを最初のスロットからロヌドしたす゚ミュレヌタヌのスロットの番号は0から始たり、Luaでは1から始たりたす。
s = savestate 。 䜜成 2 
savestate 。 負荷 s 

while  true  do
-画面がロヌドされ、すでに衚瀺されおいる堎合
if  emu。framecount   > FRAME_FOR_SCREEN  then
-スクリヌンショットをメモリに保存
ロヌカル gdStr = gui 。 gdscreenshot   ;
-ハッシュを蚈算する
ロヌカルハッシュ= emu calchash  gdStr 、 string.len  gdStr   ;
-スクリヌンショットがただない堎合
if  shas [ hash ] で は ない 堎合
print  "Found new screen" .. tostring  hash   ;
local fname = string.format  "05X" 、 CUR_ADDR  .. ".png" ;
ロヌカル gdImg = gd createFromGdStr  gdStr  ;
-倉曎されたバむトの名前にアドレスを付けおスクリヌンショットをディスクに保存したす
gdImg  png  fname 
shas [ハッシュ] = true ;
終わり ;
-前のバむト倀を埩元する
rom 。 writebyte  CUR_ADDR 、 lastValue  ;
CUR_ADDR = CUR_ADDR + STEP ;
-次のバむトの砎損
lastValue = rom 。 readbyte  CUR_ADDR  ;
rom 。 writebyte  CUR_ADDR 、 WRITE_VALUE  ;
-再びセヌブをロヌドしお、ゲヌムが新しいむメヌゞからメモリにデヌタをロヌドできるようにしたす
s = savestate 。 䜜成 2 
-進行状況の衚瀺
savestate 。 負荷 s 
gui 。 text  20、20 、 string.format  "05X" 、 CUR_ADDR   ;
-すべおのアドレスが凊理されたら、゚ミュレヌタヌを停止したす
if  CUR_ADDR > END_ADDR  then
゚ミュヌ 䞀時停止  ;
終わり
終わり ;
-次のフレヌムたで゚ミュレヌションをラップする
゚ミュヌ frameadvance   ;
終わり ;

スクリプトで゚ミュレヌタを実行するには、次のコンテンツのバッチファむルを䜿甚できたす。
fceux -turbo 1 -lua砎損.lua「ゞャッカルU[]。nes」
 タヌボキヌを䜿甚するず、゚ミュレヌタを最も高速なモヌドで起動できたす。

私のマシンのスクリプトは、すべおのデヌタを8分で凊理したす。 動䜜時間が長すぎる堎合は、 STEPステップを画面ごずに最倧64たで倧きくしお、ゲヌムをより正確にするこずができたす。この堎合、起動フレヌムずスクリヌンショットを撮りたいフレヌムの間の時間は最小限になりたす。
さたざたなゲヌム甚のスクリプトを蚭定するためのいく぀かの掚奚事項画面䞊のデヌタは、倚くの堎合、銀行の先頭 0x2000たたは0x4000の倍数のアドレスから始たるため、これらのゟヌンを詳现に調べるこずができたす。 ROMむメヌゞにビデオバンク CHR-ROM がある堎合、ビデオメモリのみを保存するため、調査するこずはできたせん。 ビデオバンクは垞にROMむメヌゞの最埌にあり、その番号はヘッダヌROMむメヌゞの最初の16バむトでも確認できたす。

ゲヌム "Jackal"の堎合、スクリプトは235のナニヌクなスクリヌンショットを芋぀けたす。これらのスクリヌンショットは、さたざたなグラフィックの䞍具合を幅広く瀺しおいたす。 ただし、アドレス0x105C8、0x105D8、0x105E8で倉曎されたバむトを持぀画像から取埗したスクリヌンショットは興味深いものです。

それらから、それは明らかです


受信したデヌタで䜕ができたすか たずえば、レベルのすべおのマクロブロックの写真を準備し、 CadEditorブロック゚ディタヌでそれらを䜿甚しおレベルマップ゚ディタヌを䜜成したす。

これを行うには、スクリプトスクリプトを少し曞き盎しお、可胜性のあるすべおのバむト倀をアドレスに曞き蟌みたす。これにより、画面䞊のブロックたずえば、 0x105C8 が倉曎され、すべおのブロックのスクリヌンショットが撮られたす。 スクリプトの党文は提䟛したせん。蚘事の最埌にあるサンプルアヌカむブ corrupt_byte.lu a にありたす。 残念ながら、 gdラむブラリは画像の䞀郚を䟿利に凊理するこずを目的ずしおいないため、スクリヌンショットからマクロブロック画像を「噛んで」、1぀の長い「テヌプ」にたずめるために、別のPythonスクリプトを䜜成する必芁がありたした PILラむブラリを画像凊理甚にむンストヌルしたす
-*-コヌディングutf-8-*-
むンポヌト画像
def cutBlock  pp  
im =むメヌゞ。 オヌプン  pp 
任意の画像゚ディタヌにスクリヌンショットをアップロヌドしお、ブロックの先頭の座暙を確認したす
X = 96
Y = 96
スクリヌンショットから指定された座暙でブロックを切り取りたす
imCut = im。 crop   X 、 Y 、 X + 32 、 Y + 32  
imCut。 保存  "_" + pp 

xrange 内の x  256  
cutBlock  r "03d.png" x 

BLOCK_COUNT = 102
MAX_BLOCK_COUNT = 256
imBig =むメヌゞ。 新芏  "RGB" 、  32 * MAX_BLOCK_COUNT 、 32  
xrange 内の x  BLOCK_COUNT  
im =むメヌゞ。 オヌプン  "_03d.png" x 
imBig。 貌り付け  im 、  32 * x 、 0、32 * x + 32、32  
CadEditorで䜿甚するための芁件であるマクロブロックサむズを64x64に増加
imBig64 = imBig。 サむズ倉曎   MAX_BLOCK_COUNT * 64、64  
imBig64。 保存  "outStrip.png" 

レベル゚ディタヌにゲヌムを远加する


最埌の郚分は残りたす-CadEditor゚ディタヌの構成ファむルを䜜成する必芁がありたす。これは、結果のむメヌゞを䜿甚したす。 Cをスクリプト蚀語ずしお䜿甚したす CSScriptラむブラリを䜿甚。
スクリヌンショットによれば、マクロブロックマップの行の先頭を蚈算したす。アドレス0x105C8が行の4番目のマクロブロックを倉曎する堎合、アドレス0x105C5が最初のを担圓したす。 次に、テンプレヌト構成を䜜成したす。
CadEditor を䜿甚し たす 。
System を䜿甚し たす 。
System.Collections.Generic を䜿甚し たす。
System.Drawing を䜿甚し たす。

パブリック クラスデヌタ
{
/ *行の境界を䞊䞋に亀互にシフトしお、正しいアドレスを蚈算したす
「りィンドり」に正しい「レベルマップ」が衚瀺されるたで。
開始アドレス0x10625から、96行䞊に埌退したす。
1-レベルごずのゲヌム画面の数、
16 * 96-1ゲヌム画面のバむト単䜍のサむズ
* /
public OffsetRec getScreensOffset   { 新しい OffsetRec  0x10625-16 * 96、1、16 * 96 を 返し たす。 }
public int getScreenWidth   { 16を 返す ; } //画面の幅を蚭定する
public int getScreenHeight   { return 96 ; } //画面の高さを蚭定する
public int getBigBlocksCount   { 256を 返す ; }
//ストリップをマクロブロック画像に接続したす
public string getBlocksFilename   { return "jackal_1.png" ; }

//このゲヌムに実装されおいないマクロブロックず敵のサブ゚ディタヌをオフにしたす
public bool isBigBlockEditorEnabled   { falseを返す ; }
public bool isBlockEditorEnabled   { falseを返す ; }
public bool isEnemyEditorEnabled   { falseを返す ; }
}

このような構成で゚ディタヌにロヌドされたレベルマップは奇劙に芋えたすが、実際のレベルマップに䌌おいたす。


結果を調べた埌、16x8マクロブロックのサむズの画面の行は䞋から䞊に順番に栌玍されたすが、画面自䜓は䞊から䞋に栌玍されたす。぀たり、画面の8行ごずに再配眮されたす。 幞いなこずに、゚ディタヌには、むメヌゞからROMレベルをロヌドする方法を指定できる倚数のメ゜ッドがありたす。 この堎合、マクロブロック番号がカヌドから正確に読み取られる方法ず、それに応じお゚ディタヌによっお曞き戻される方法を制埡する2぀の特別な機胜を蚭定する必芁がありたす。
//特別な関数を䜿甚しおマクロブロック番号を取埗するよう゚ディタヌに指瀺したす
//マップしお蚘録したす
public GetBigTileNoFromScreenFunc getBigTileNoFromScreenFunc   { return getBigTileNoFromScreen ; }
public SetBigTileToScreenFunc setBigTileToScreenFunc   { setBigTileToScreenを返す; }

public int getBigTileNoFromScreen  int [ ] screenData、 int index 
{
int w = getScreenWidth   ;
int noY = index / w ;
noY =  noY / 8  * 8 + 7-  noY  8  ; //マクロブロックのY座暙の倉換
int noX =むンデックス w ;
return screenData [ noY * w + noX ] ;
}

public void setBigTileToScreen  int [ ] screenData、 int index、 int value 
{
int w = getScreenWidth   ;
int noY = index / w ;
noY =  noY / 8  * 8 + 7-  noY  8  ; //マクロブロックのY座暙の倉換
int noX =むンデックス w ;
screenData [ noY * w + noX ] = value ;
}

これでマップが正しく衚瀺され、レベルゞオメトリを再描画できたす。


怜玢方法は、ほがすべおのNESゲヌムに適甚できたす。アヌカむブのスクリプトを䜿甚しお、お気に入りのゲヌムを孊習するこずができたす。

さらに、いく぀かの倉曎を加えるず、この方法は「Sega Mega Drive」および「SNES」プラットフォヌムにも適甚できたす倉曎する必芁があるのはROMむメヌゞ自䜓ではなく、コン゜ヌルのRAM、倚くの堎合、展開されおいないレベルカヌドが栌玍されるこずです

次の蚘事では、ゲヌムの䟋ずしお、ゲヌムロゞックデバむスず、ゲヌムオブゞェクトを怜玢しおマップ䞊に衚瀺および配眮する方法を瀺したす。

参照
䟋付きのアヌカむブ
コメント付きスクリプト

Source: https://habr.com/ru/post/J259483/


All Articles