上に戻る
5.nesasmでプログラム改造 前編
概要
 いよいよ本番がやって参りました。nesasmを使ってプログラムを改造します。もとい、プログラムを新設し、それを実際に呼び出しゲームの挙動を大きく変更します。
根本的な考え方
 プログラムを新設するためには、ROM上に領域が必要です。もし、元のゲームに空き領域があったら、その部分を使ってプログラムを新設することができます。もし空き領域がない場合は、既存のプログラムを潰し、その上に新たにプログラムを新設することになります。
 後者の場合やや事故を起こしやすいのですが、いずれの場合にせよ、利用できる範囲をきちんと守り、サイズ内で抑える必要があります。
ニーモニック
 nesのプログラム改造をしたことが有る方なら、A9という値が神聖であることをよくご存知でしょう。大体これの後ろを書き換えれば色々おもしろいことが起こるというミラクルナンバーとなっています。これは、A9というオペコードにldaという命令が与えられており、その1バイト後ろの値を、「続く何らかの処理」のためにセットアップするため、色々と面白いことを引き起こせるためです。
 このldaといった表記のことをニーモニックというそうです。ニーモニックにより、命令の働きがだいたい決まります。(本当はニーモニック×オペランドで決まりますが省略)アセンブラですので、基本的にオペコードであるA9などは使わずに、ニーモニックを使ってコードを書いていくことになります。例えば、dbにより2バイトデータを配置するコード

    ROMADDR $3DBB2
    db $A9,$00
は、以下のコードと等価です。(たぶん完全に同じ扱いになると思います)

    ROMADDR $3DBB2
    lda #$00
 私にとって驚くべきことなのですが、nesの改造を行う方の中には、オペコードによる改造を自由自在に行えるにもかかわらず、ニーモニックを意識せずにそれを行なっている方がおられるようです。とある偉大な改造の作者も、オペコードをガリガリ打ち込んで作っているらしいというのは度肝を抜かれた……
 私はオペコードは殆ど覚えておらず、バイナリのダンプを見ても殆ど内容がわかりません。逆アセンブルされ、ニーモニックが出てこないとさっぱりわからないどころが、考え始めるに至らないというレベルです。
 この辺はどれが優れているということはなく、アプローチの違いとしか言い様がないのでしょう。しかし、せっかくですので、オペコード派の方も、ニーモニックを一通り覚えてしまいましょう!というのは無理があるので(逆に私がオペコード丸暗記とか……やだよ)ひとつ考えました。
 今からニーモニックを使って以下の様なプログラムを打ちます。

    ldx #$06
.Seek_loop
    lda $0420,x
    bmi $DBEC ;sec_rts
    dex
    cpx #$01
    bne .Seek_loop
    lda #$23
    jsr $C051 ;BGM
 オペコード派の人はさっぱりわからないかもしれませんが、これを以下のように書き換えたらどうなるでしょうか……?

    db $A2,$06
.Seek_loop
    db $BD,$20,$04
    bmi $DBEC ;sec_rts
    db $CA
    db $E0,$01
    bne .Seek_loop
    db $A9,$23
    db $20,$51,$C0
 分岐命令はニーモニックを使ってしまっていますが、それ以外は、オペコードをdb命令で打ち込んでいくことで、機械語直にアセンブラでプログラムを打ち込んでいくことができます。この方法ならオペコードに親しんだ方なら、アセンブラを利用する事ができるでしょう。
 ……いやまあ、若干いやかなり無理があるとは思っています。アセンブラを使うからには、ニーモニックを使って書いて行かないと、ラベル云々も活かしづらくなりますので、アセンブラでプログラムを打つには結局ニーモニックの暗記が必須です。こんな方法でも、覚えるまでの障壁が少なくなればなあと思います。
コメント:
ニーモニックをタダのアルファベットの羅列だと思うと覚えられません。アルファベット1文字毎に、必ず何らかの意味があります。ldaならLoaD Aです。staならSTore Aです。
まずはやってみる
 引き続き「ロックマン2」のロックマンの武器を改造してみましょう。ここでは「メタルブレード」を書き換えます。以下のようにコードを追加します。

    ROMADDR $3DBB2
    org $DBA2
MetalBlade_Fire:
    ldx #$06
.Seek_loop
    lda $0420,x
    bmi $DBEC ;sec_rts
    dex
    cpx #$01
    bne .Seek_loop
    lda #$23
    jsr $C051 ;BGM
    dec <$A2 ;メタルブレードのエネルギー
    ldx #$06
.Place_loop
    ldy #$07
    jsr $D3DD ;Obj[x]武器yを作成
    lda MetalBlade_Tbl+5*0-2,x
    sta $0660,x ;Vylo
    lda MetalBlade_Tbl+5*1-2,x
    sta $0640,x ;Vyhi
    lda MetalBlade_Tbl+5*2-2,x
    sta $0620,x ;Vxlo
    lda MetalBlade_Tbl+5*3-2,x
    sta $0600,x ;Vxhi
    dex
    cpx #$01
    bne .Place_loop
    beq $DBE9

    ROMADDR $3DBFE
    org $DBEE
MetalBlade_Tbl:
.V = $0700
.c0 = 65536
.c1 = 60547 ;(22.5o*65536+0.5);=
.c2 = 46341 ;(45o*65536+0.5);=
.s0 = 0
.s1 = 25080 ;(22.5s*65536+0.5);=
.s2 = 46341 ;(45s*65536+0.5);=
    db LOW (.V*.s0/65536)
    db LOW (.V*.s1/65536)
    db LOW (.V*.s2/65536)
    db LOW (.V*.s1/65536*-1)
    db LOW (.V*.s2/65536*-1)

    db HIGH(.V*.s0/65536)
    db HIGH(.V*.s1/65536)
    db HIGH(.V*.s2/65536)
    db HIGH(.V*.s1/65536*-1)
    db HIGH(.V*.s2/65536*-1)

    db LOW (.V*.c0/65536)
    db LOW (.V*.c1/65536)
    db LOW (.V*.c2/65536)
    db LOW (.V*.c1/65536)
    db LOW (.V*.c2/65536)

    db HIGH(.V*.c0/65536)
    db HIGH(.V*.c1/65536)
    db HIGH(.V*.c2/65536)
    db HIGH(.V*.c1/65536)
    db HIGH(.V*.c2/65536)
 これでメタルブレードが、方向指示こそは出来ないものの、向いている方向に5Wayに撃つことが出来るようになりました。それほど長くないコードですが、挙動がオリジナルと比べ全く別物に変わりました。この様に、プログラム改造は色んな意味で強烈な結果を生みます。
 このコードを追加したasmをここの[5-10]に置いておきます
コメント:
ところで、もうDataHack.asmというasmファイル名はプログラム改造に入ったので不適切かもしれませんが変更するのも面倒なのでこのまま最後まで行きます。
5Wayメタルの解説
 今のコードに、上から下まで解説を入れていきます。「アセンブラの使い方」というメインテーマとは若干反するのですが、ある程度プログラムについても解説したほうが、円滑な解説になるでしょう。たぶん。

    ROMADDR $3DBB2
    org $DBA2
MetalBlade_Fire:
 この場所では、PC位置を指定しています。このアドレスにあるプログラムは、「メタルブレードを装備した状態で、Bボタンを押した時」に来る部分です。それはともかく「org $DBA2」とはなんぞやとツッコミを入れたくなった方がいたら偉い。後述しますがそれを入れておかないとこのコードは正しく動作しません。

    ldx #$06
.Seek_loop
    lda $0420,x
    bmi $DBEC ;sec_rts
    dex
    cpx #$01
    bne .Seek_loop
 この部分で、メタルブレードが既に存在しているかどうかを調べています。$0420,xの値が負なら存在しています。この5Wayメタルブレードは、2,3,4,5,6番目のオブジェクトを利用します。そのいずれのオブジェクト一つでも存在したら、メタルブレードは不発になります。このループは、まずはx=6とし、6番目のオブジェクトが存在しているかを調べます。そして、順次、5,4,3,2と存在しているかを調べます。もしオブジェクトが存在している場合処理を中断し、射出処理は行いません。
 もし、メタルブレードは一つも存在していない場合は続く処理へ移行します。

    lda #$23
    jsr $C051 ;BGM
    dec <$A2 ;メタルブレードのエネルギー
 これらの処理は、単に音を鳴らし、エネルギーを消費させるだけです。
 続いて、メタルブレードを5個配置します。

    ldx #$06
.Place_loop
    ldy #$07
    jsr $D3DD ;Obj[x]武器yを作成
    lda MetalBlade_Tbl+5*0-2,x
    sta $0660,x ;Vylo
    lda MetalBlade_Tbl+5*1-2,x
    sta $0640,x ;Vyhi
    lda MetalBlade_Tbl+5*2-2,x
    sta $0620,x ;Vxlo
    lda MetalBlade_Tbl+5*3-2,x
    sta $0600,x ;Vxhi
    dex
    cpx #$01
    bne .Place_loop
    beq $DBE9
 まず6番目のオブジェクトとしてメタルブレードを配置します。続けて、5,4,3,2番目のオブジェクトとして配置します。ここで、

    lda MetalBlade_Tbl+5*0-2,x
 この表現が小難しいかもしれません。まず、MetalBlade_Tblというのは、後で作るテーブル(値の表)のアドレスです。5Wayなので、5バイトずつデータを持ちます。また、X速度とY速度の、整数部と小数部の4つのデータをブレード一つに付き持ちます。
コメント:
・Y速度小数部(ブレード5個分)
・Y速度整数部(ブレード5個分)
・X速度小数部(ブレード5個分)
・X速度整数部(ブレード5個分)
の順番にテーブルになっています。通常の感覚では理解しがたい順番で入っていると思いますが、このようにするのが効率がよいです。
 「5*0」「5*1」……の部分は、5バイトずつずれた位置から、読み込んでいるんだと理解しやすいと思いますが、-2とは何でしょうか。これは、xが6,5,4,3,2と順番に動くことが考慮されています。最低値が2で、最大値が6なので、-2したアドレスでxインデックスアクセスすれば、テーブルの先頭から5バイト分のデータが順次読みこむことができます。
コメント:
 更に小難しいかもしれませんが「MetalBlade_Tbl+5*1-2」といった値は、アセンブル時に「定数」で有ることに注意が必要です。アセンブルすることによりMetalBlade_Tblのアドレスが解決されれば、この式全体は、ただある値になるに過ぎません。
例えばプログラム上で

    dex
    dex
    lda MetalBlade_Tbl+5*0,x
    inx
    inx
    sta $0660,x ;Vylo
と書いても動くはずです……が、こちらは実際にnesの処理としてxを変化させているので処理時間がかかります。要するに2バイトずれた場所にアクセスしたいのだから、最初からずらしたアドレスを指定しておけば、速度的・容量的なペナルティ無く読み込むことが出来るわけです。
 メタルブレードを配置するプログラムの最後

    beq $DBE9
 は、「投擲ポーズに変更して処理を終了」です。beqとなっていますが、これは必ずジャンプします。直前の処理は、bneであるため、このbeqは確実に成立し、分岐が行われます。この書き方をすると、容量が1バイト省略できるので、実際のソフトの中でも頻繁に利用されています。なお、処理速度は(たぶん)beqを使ってもjmpを使っても同じです。
 残るプログラムは、速度を指定するためのテーブルです。

    ROMADDR $3DBFE
    org $DBEE
MetalBlade_Tbl:
.V = $0700
.c0 = 65536
.c1 = 60547 ;(22.5o*65536+0.5);=
.c2 = 46341 ;(45o*65536+0.5);=
.s0 = 0
.s1 = 25080 ;(22.5s*65536+0.5);=
.s2 = 46341 ;(45s*65536+0.5);=
 ここにも謎のorgがありますが後述します。MetalBlade_Tblラベルを設定し、テーブルの先頭の目印にしています。また、一時的に使う値を、ローカルラベルとして定義しています。これらについて少し見てみましょう。
 .Vは、メタルブレードの速さを表します。1フレームあたりに進む速さを256倍した値です。この値を弄るだけで好きなように速さを変えることができます。
 .c0,.c1,.c2は、コサインです。それぞれ、0°、22.5°、45°の値を65536倍したものです。.s0,.s1,.s2も同様にサインです。このような定数を準備しておくと、自然な5Wayを作ることができます。
 次からは実際のテーブルです。

    db LOW (.V*.s0/65536)
    db LOW (.V*.s1/65536)
    db LOW (.V*.s2/65536)
    db LOW (.V*.s1/65536*-1)
    db LOW (.V*.s2/65536*-1)
 わかりづらいかもしれませんが、速さにサインをかけることで、速さのY成分を取り出しています。ここで、.Vは256倍、.s?は65536倍の値を取っていることを思い出して下さい。掛け算をした後、65536で割る(小数部切り捨て)ことで、()の中は256倍になっています。そしてその値のLOW()を見るので、Y速度の小数部を取り出し、それらをテーブルにしています。
コメント:
この計算をする時に、掛け算をした後に割り算をすることに注意して下さい。除算時に小数点以下は切り捨てられるので、除算は最後に行うと近似値を求めることができます。より精密にやるなら、四捨五入するため、+$8000してから65536で割ると、より良いです。あと大きめの数を扱うのでオーバーフローには注意。ちなみにnesasmは小数をサポートしていないっぽいです。
 続く5バイトは、今計算したデータの上位バイトを見るために、HIGH()を利用しているだけです。
 その後に更に続く5バイト、5バイトは、X速度についても同様にテーブルを作ったにほかなりません。但し、X速度は、絶対値のみを計算します。向きが考慮され、自動的に射出される方向が決定されます。便利。
なんか気に入らないんだよなあ……
 上の様なコードを書きましたが、個人的には気に入らない点が多いです。列挙します。

 ・変数はなるべくラベルを使うべき
 ・サブルーチンのアドレスなどもよく使うものはラベルを使うべき
 ・アドレスの指定方法が何より許せない、なんだorgって
 ・この書き方だとうっかりコードを書きすぎて、確保した以上の容量を使っても気づかない
 どれも、せっかくアセンブラを使うのだから改善した方がいい点です。
ラベルを使う
 汎用以外の変数にはラベルを付けると良いでしょう。尚、私は面倒になり、よく直値を使います(あれ?)私は最近、オブジェクトに関する変数(配列)は、先頭にoをつけるようにしています。

aWeaponEnergy = $9C
oType    = $0400
oFlag    = $0420
oXhe     = $0440
oXhi     = $0460
oXlo     = $0480
oYhi     = $04A0
oYlo     = $04C0
oVal0    = $04E0
oVxhi    = $0600
oVxlo    = $0620
oVyhi    = $0640
oVylo    = $0660
oSprTimer = $0680
oSprNo    = $06A0
oHP      = $06C0
以上のように、予めラベルを定義しておき、以下のように利用します。

    ldx #$06
.Seek_loop
    lda oFlag,x
    bmi $DBEC ;sec_rts
    (中略)
.Place_loop
    ldy #$07
    jsr $D3DD ;Obj[x]武器yを作成
    lda MetalBlade_Tbl+5*0-2,x
    sta oVylo,x
    lda MetalBlade_Tbl+5*1-2,x
    sta oVyhi,x
    (略)
コメント:
通常、グローバルラベルを短い文字数にすべきではありません。しかし、使う頻度・重要性を考えれば、これだけ短い名前のグローバルラベルで定義しても問題ないでしょう。
また、ゼロページメモリのうちの幾つかは、大抵汎用(サブルーチンの引数・返り値などに利用)になっているものですが、こういうものは直値を使った方がわかりやすいです。
 これで、アドレスを覚えなくてもプログラムが書けるようになります……と思いきや、ラベル名を覚えないといけなくて、結局覚えられないなんてことも多くあったりして……
 変数名だけでなく、ルーチンのアドレスラベルに出来ます。というかどちらの場合にしてもただの数値にほかなりません。「BGM(音)を鳴らす」「オブジェクトを配置する」だの、頻度が高いものは、ラベルにしておいたほうが良いでしょう。私は、ルーチンのアドレスのラベルを付ける時は、先頭にrをつけます。

rAudio = $C051
rNewWeapon = $D3DD
 そして、以下のように利用します。

    bne .Seek_loop
    lda #$23
    jsr rAudio
    dec <aWeaponEnergy+6 ;メタルブレードのエネルギー
    ldx #$06
.Place_loop
    ldy #$07
    jsr rNewWeapon
    lda MetalBlade_Tbl+5*0-2,x
    sta oVylo,x
    lda MetalBlade_Tbl+5*1-2,x
 ラベルを使え的な節でしたが、直値は一切使うなみたいな極端な事は言いません。そんなことをするとむしろ作業能率が低下してしまうでしょう。
 利用する頻度の高いものはラベルにしてはどうでしょうか。尤も、最もまずいのは、ラベル表記と直値表記が混在することなのですが……
アドレスの指定方法の再考
 ここまでROMADDRPC位置を指定するために使って来ました。ここまでそれでも全く問題が起きてこなかったように見えますが……私は、この時に利用する、「ROM上のアドレス」は何ら意味を持たないと思っています。大事なのは実行時に「割り当てられる」アドレスです。「割り当てられる」アドレスとはどういうことでしょうか。
 まず、nesファイルは、10hバイトのヘッダを持ちます。その分「ROM上のアドレス」と実行される時のアドレスはずれることになります。しかし問題はそれだけではありません。
 元々nesの仕様では、プログラムは8000h(約32Kバイト)までしか利用できませんでした。この、8000h(4000hしかないROMもあるが……)のプログラムを、8000-FFFFに「割り当てて」利用します。このため、

    lda $8000
 とすると、ROMの先頭のバイトをロードすることができます。こういった「割り当てられた時の」アドレスを、PC位置として指定すべきです。「なぜそんなことをするのか、ROM上のアドレスで十分ではないか」と、疑問に思われるかもしれませんが、まともにアセンブラでプログラムを打ち込んでいくには避けて通れません。
bankとorg
 本来、PC位置を変更するには、nesasmのディレクティブである、bankorgを使います。これがなかなか厄介な挙動をします。orgの後ろには16ビット値を与えます。例えば、以下のようになります

 org $0000
 これで、ヘッダを除いたromの先頭をPC位置とすることができます。当然、

 org $0001
 と書けば、ヘッダを除いた先頭から2バイト目(オフセット1)をPC位置とします。この調子で、延々と続くかと思いきや、ある値で話は変わります。

 org $1FFF
 までは、同様に処理されます。ですが、

 org $2000
 これだと、なぜか、org $0000と同じく、書き換わる位置が、ヘッダを除いたromの先頭になります……どういうことでしょうか。実は、orgの後ろに与える値は、下位13ビットのみが有効です。残りの3ビットは(とりあえずは)効果がありません。ではどうするかというと、ここでbankを利用します。まず、

 bank $00
 org $0000
これが先頭です。で、+1FFFした位置は以下の表現で表せます。

 bank $00
 org $1FFF
そして、これに更に+1した位置は以下のようになります。

 bank $01
 org $0000
 bankの値1につき、2000h相当ということです。
 ROM上のあるアドレスをPC位置としたいときは、まずそのROM上のアドレスからヘッダ分の10hを引き、2000hで割った値がbank、その剰余がorgとすると、「とりあえず」目標を達成することができます。
orgの上位3ビットの役割
 orgの上位3ビットは無意味なのでしょうか?そんなことはありません。以下のように、ラベルを定義するときに、=以下を書かないと、その場所のアドレスを取得できるのでした。

    bank $00
    org $0000
TestLabel:
    db HIGH(TestLabel)
    db LOW(TestLabel)
 このように書くと、TestLabelは値0000hになります。そして、続く2つのdbにより、00,00という値が書き込まれます。では、+2000したアドレスを与えるとどうなるでしょうか。

    bank $00
    org $2000
TestLabel:
    db HIGH(TestLabel)
    db LOW(TestLabel)
 この場合は、TestLabelは値2000hになります。そして、dbにより、20,00という値が書き込まれます。一つ前の場合と書き込まれる場所自体は変わりませんが値は変わります。
 先ほどからorgとして低い値を与えていますが、実際には8000以上の値を通常与えます。これは、nesはプログラムを8000-FFFFに割り当てるためです。この、割り当てられたときのアドレスと、orgの上位3ビットを常時正しくあわせて行く必要があります。
 8000hバイトしかないROMですと、これが素直に8000-FFFFに割り当てられるため、

    bank $00
    org $8000
    ……(各種処理)……
    bank $01
    org $A000
    ……(各種処理)……
    bank $02
    org $C000
    ……(各種処理)……
    bank $03
    org $E000
    ……(各種処理)……
 の様にすれば、割り当てられた時のアドレスとPC位置の対応が正しくなります。
限界突破
 初期は8000hのプログラムでも良かったのですが、その後、カートリッジの方を拡張し、メモリコントローラ(この辺テキトーだけど)を積むことで、より大容量のプログラムを、その8000hの場所に「割り当てて」利用できるようになりました。
 本ウェブサイトの別のコンテンツからの再喝となりますが、以下はその様子を図示したものです。

 この図にも書いてありますが、多くのnesのカートリッジ(のメモリコントローラ)は、プログラムは2000h毎に分割して管理しており、それを、4箇所に割り当てながら使う事が多いように思います。(この辺りは、カートリッジにどのハードウェアが積んであるかに応じて全く変わってくる)
 上図の一つずつの四角が「bank $00」「bank $01」……の一つずつです。それぞれ2000hの大きさを持ちます。問題は、どのバンクがどこに割り当てられるかは、調べてみないとわからないということです。例えばbank $00は、8000-9FFFに割り当てられるかもしれないし、A000-BFFFに割り当てられるかもしれないし、C000-DFFF,E000-FFFFかもしれません。
 どう割り当てて利用しているかは、カートリッジに搭載されたハードウェアや、ゲームの作りや、当時の開発者の意向次第です。作り方次第では、bank $00は時に8000-9FFF、時にA000-BFFFに割り当てて使う、といった作りにすることも可能です。
 あるバンクのプログラムを書き換える時は、そのバンクが、今、4箇所の何処に割り当てられているのかという意識が大事になってくることもあります。
ROMADDRはなるべく使わない
 これまで当たり前のように使ってきた、ROMADDRはいぜん説明しましたが、私の定義したマクロです。マクロについてこれまで突っ込んでは触れていませんが、ここでさっと説明します。まず、ROMADDRマクロは以下のように定義されていました。

ROMADDR .macro
    bank ((\1-$10)/$2000)
    org (((\1-$10)&$1FFF)|$8000)
    .endm
 .macroと.endmの間の2行がマクロROMADDRの定義です。ROMADDRも、bankとorgの2つの命令を組み合わせたものに過ぎなかったのです。そして、利用する時は以下のように書きました。

    ROMADDR $1010
 この時、この行を見つけたアセンブラはどのような処理を行うでしょうか。マクロは定型文のようなものと説明しましたが、この行は、定義した通りに置き換えられて解釈されます。また、ここでは$1010となっている値は、マクロの定義で「\1」となっている部分に代入されます。以上の処理により、上の一行は、アセンブラには以下のように解釈されます。

    bank (($1010-$10)/$2000)
    org ((($1010-$10)&$1FFF)|$8000)
 算術演算を始末しましょう。

    bank $00
    org $9000
 こうして、これまでROM上のアドレスを利用して、PC位置を指定してきました。
 さて、ここで問題があるのですが、ROMADDRは必ず8000-9FFFにそのバンクが割り当てられたとして処理をします。しかし、実際にどこに割り当てられるかはその時々に応じます。もし、正しくない割り当てをした状態でアドレスにラベルをつけると、各不具合を招くこととなります。
 まあ、A000-BFFFに割り当てるような「ROMADDR_A」マクロをまた別に定義する……という方法でもいいには良いのですが、もうROM上のアドレスを使うこと自体をやめてしまいましょう。
 今回の例のメタルブレードのテストでは、この問題にモロに直撃しました。そのため、ROMADDRで指定しながらも、慌ててorgで指定しなおすという無様極まりないコーディングとなっております。こんな書き方はしないように。
BBAAAA形式のアドレス
 では、どのようなアドレスを使うことをオススメするかと申しますと、「バンク番号」と「割り当てられたアドレス」をそのまま繋げて6桁にした、BBAAAA(BB:バンク番号 AAAA:割り当てられたアドレス)という形式を推奨します。結局、まともにプログラムを読み、改造していくなら、割り当てられたアドレスを念頭に入れざるを得ません。それならば、そのままそのアドレスを使えるように、このような形式にしてしまうのが良いでしょう。
 BBAAAA形式のアドレスと、ROM上のアドレスの変換は以下のとおりになります。

 BB*2000h + AAAA&1FFFh + 10h = ROM上のアドレス

 (ROM上のアドレス-10h)/2000h(切捨て) = BB
 ((ROM上のアドレス-10h)&1FFFh) | (8000かA000かC000かE000) = AAAA
 この変換を見ても分かりますが、「BBAAAA形式」と「ROM上のアドレス」は1対1にはなっていません。それは当たり前の話で、ROM上のある1点を「何処に割り当てられているか」も考慮して表現するのがBBAAAA形式だからです。
 このアドレスを用いて、PC位置を指定するには、例えば以下のように指定します。

    ;アドレス$1289AB(バンク$12の$89AB)を指定したい時は……
    bank $12
    org $89AB
 要するに、上2桁をbankとして与え、下4桁をorgとして与えるだけです。便利なのでマクロにしましょう。
コメント:
もっと言うと、「ROM上のアドレス」よりずっと自然なものが、このBBAAAA形式ということです。自然な形式なので、単に分割しただけでアセンブラの指定に使えるわけです。

BANKORG .macro
    bank BANK(\1)
    org (\1)
    .endm
BANKORG_D .macro
    bank ((\1)>>16)
    org ((\1)&$FFFF)
    .endm
 このマクロを利用する時は以下のようになります。

    BANKORG_D $12ABCD
    db 0,1,2,3,4
TestLabel:
    db 5,6,7,8,9
    BANKORG TestLabel
 BANKORG_Dの方はそのまま6桁のアドレスを与えます。_Dがつかない方は、ラベルを引数に与える時に使います。
コメント:
BANK()というディレクティブが出て来ました。これは、()内のラベルのバンク番号を取り出すものです。上の例ではTestLabelは16ビットの値ですが、実は、バンクの値を内包しています。その、「実は含んでいるバンクの値」を取り出すことが出来るのです。c言語で言う「左辺値」「lvalue」的なものでしょうかねえ。
 尚、以下のように、値を指定されたラベルにBANK()を使うとアセンブルエラーが出ます。

TestValue = $1234
    db BANK(TestValue) ;←これは不可能
BANKORG_Dを実際に使う
 5wayメタルのソースをBANKORG_Dで書き直すとこうなります

    BANKORG_D $1EDBA2
MetalBlade_Fire:
    ldx #$06
.Seek_loop
    lda $0420,x
    bmi $DBEC ;sec_rts
    (省略)
    bne .Place_loop
    beq $DBE9

    BANKORG_D $1EDBEE
MetalBlade_Tbl:
.V = $0700
.c0 = 65536
.c1 = 60547 ;(22.5o*65536+0.5);=
    (省略)
 PC位置を指定する時に、同時に正しく、割り当てられたアドレスも指定するため、謎のorgを利用しなくてよくなりました。これだけだとなぜこんな書き換えが必要だったのか理解できない方も居るかもしれません。しかし、逆アセンブルを自分で行い、解析からやったことが有る方なら、この記述の利便性が理解できると思います。
利用可能なスペースを上回らないようにする
 改造はその性質上、プログラム上に設けたせっまい空き容量にひしめくようにコードを打ち込んでいく必要があります。もしプログラムを打ち過ぎて、空き容量を超えてコーディングしてしまうと、触れていけない部分までコードが達して、それはもう大変なことになります。
コメント:
そんなふうになった時でもフォローが効きやすいのがアセンブラだが
 先ほどの5wayメタルも、隙間にコーディングしているにもかかわらず、コードを打ちすぎるトラブルへの対処が成されていません。どのように対応できるでしょうか。
 答えをさっさと言うと、nesasmのディレクティブのif-endiffailを使うと、コードを打ち過ぎた時にエラーを出すことができます。使い方は、雑にこんな感じです。

    if (条件)
    ;条件を満たした時にここがアセンブルされる
    endif

    fail ;ここがアセンブルされるとアセンブラはアセンブルを中止する
コメント:
実のところ、ifの条件にどのようなものが使えるのかは、私ははっきりと把握していません。条件式の中には、どうも、「条件を評価する段階で、既にアドレスが解決されているラベル」を利用することは出来るようなのですが……?(つまり、そのifディレクティブより上に書いてあるラベルのみ利用できる)
 今回のメタルブレードの射出処理では、元の射出ルーチンが1EDBEBまでが利用しているので、そこまでは利用できます。次の、1EDBECに達したらアウトです。
コメント:
どこまでが利用しているか判断するなんてのは慣れしかありません。一見してある場所まで潰して利用できるように見せかけて、同じルーチンが他の場所から参照されていたりするんですよねえ。あるルーチンの終端のrtsだけ、次のルーチンから利用されているなんてこともよくあります。
 以下のようにコードを書いておけば、そこまでにコードを書きすぎた時エラーを出してアセンブルを止めることができます。

MetalBlade_end:
    if MetalBlade_end>$DBEC
    fail
    endif
 1行目でラベルを定義し、2行目でそのラベルがある一定の値より大きいかを確認、もし大きければ3行目のfailがアセンブルされ、アセンブラはエラーメッセージを出し、アセンブルが終了されます。毎回ラベル名を考えていたら面倒なので、これもマクロにしてしまいましょう。
コメント:
この場合、ラベルMetalBlade_endは、$DBECを「指す」まではギリギリセーフです。「次のルーチンと同じアドレスはセーフ」です。指しているだけで、まだ書き込んでいませんので。
また、バンクの境界(2000h毎)を踏み越えてアセンブルした時は、nesasmが自動でエラーを出してくれるので、バンクの終端の部分にプログラムを書き足す場合はこういった配慮は必要ありません。

END_BOUNDARY_TEST    .macro
CurrentPosition\@:
    if ((CurrentPosition\@)&$FFFF)>((\1)&$FFFF)
    fail ;END_BOUNDARY_TEST \1
    endif
    .endm
 \@はマクロ内で利用できる表記です。マクロが使われる毎に固有の数値に置き換えられます。詳しくはUsage.txtを。5Wayメタルブレードでは、射出処理と、速度のテーブルの終端がオーバーしていないかどうかを確かめます。END_BOUNDARY_TESTを利用して書き換えたasmがここの[5-20]です
 これでようやく私としては納得行く作りになりました。
オチ
 この章で言いたかったことは、何より「ROM上のアドレス」なんて忘れてほしい事です。尤も、利用したほうが早く行いたいことを処理できそうな時は、敢えて避ける必要はありません。また、5wayメタルの例を通して、どうすれば事故らないように円滑にコードを書けるかという点を説明しました。
 次回は最終回として、もっと大規模にロックマン2の武器を作ります。新たに解説するnesasmに関する事自体は殆ど無く、こんなふうに作ると良いのではないかという私の行った例と、手法の解説がメインとなります。
(246)2013年5月25日 プレさ兵衛
inserted by FC2 system