ファミコンの FF3 のメニューの描画を早くした

先日別の方が普通にプレイしている姿をみてて、スタートボタンを押してからメニューがでるまでに毎度1度秒以上かかるのが気になったので改修してみました.

ROM の CRC32 は 0x57E220D0 のものです.

遅い原因

  • 画面描画を止めずに vblank を待って限られた時間に最大で 64 byte 書いている
  • 移動画面でのセリフに使うウインドウのシステムを何故か流用している
    • FF2 からの仕様らしく FF3 でもそのまま使ってしまっている

客観的事実としまして、FF1 はメニュー描画は続編と比較して早く(ファミコンソフト一般として普通)で、FF2 と FF3 はものすごく遅いです.

プログラムをしらべる

対策は画面描画を止めて、vblank を待つことなく1度に VRAM へデータを書き込み、終わったら画面描画を再開することです.

直接の原因

PC:$edcd と PC:$f693 付近の命令が問題となっています.

EDCD: jsr $ff00 ;wait a vblank
EDD0: lda #$02  ;DMA to OAM
EDD2: sta $4014
EDD5: jsr $f6aa ;store to nametable from buffer
EDD8: jsr $c9a9
EDDB: jsr $ede1
EDDE: jmp $c750 ;sound task
F692: pha
F693: jsr $ff00
F696: inc $f0
F698: pla
F699: jsr $f6aa ;store to nametable from buffer
F69C: jsr $ede1
F69F: jsr $c750
F6A2: lda $93
F6A4: jsr $ff03 ;switch a bank for address $8000-$9fff
F6A7: jmp $f683

メニューのプログラム

スタートボタンを押すと画面暗転後に PC:0x3d:$a52f から始まります. MMC3 では CPU ROM bank は address $8000-$9fff, $a000-$bfff で分割していますが、このプログラムでは page は 0x3c と 0x3d をつなげて利用しています.

a52f: lda  #$00
a531: sta  $78f0 ;カーソルに関する変数
a534: lda  #$00
a536: sta  <$25
a538: sta  $2001 ;PPU 描画停止
a53b: sta  $79f0 ;カーソルに関する変数
a53e: sta  $7af0 ;同上
a541: jsr  $dd06 ;CPU の ROM からキャラデータの転送
a544: jsr  $f7bb ;画面外枠だけの nametable を data 0 で埋める
a547: jsr  $c486
a54a: jsr  $ff00 ;wait a VBLANK (いらない)
a54d: lda  #$02
a54f: sta  $4014 ;update OAM (いらない)
a555: lda  #$88
a557: sta  <$fd
a559: sta  <$ff
a55b: jsr  $959f ;PPU 描画再開

メニューのための VRAM 転送命令が続きますが中略します. その転送命令のなかでは直接の原因が利用されています.

下記は描画完了後のユーザーからのボタン入力待ちに関連するループです.

a5a9: jsr  $a7cd
a5ac: lda  #$00
a5ae: jsr  $8000 ;sprite 管理で OAM buffer を更新
a5b1: lda  #$00
a5b3: jsr  $80cb ;同上
a5b6: lda  #$04
a5b8: jsr  $91a3 ;同上
a5bb: lda  <$25
a5bd: beq  $a5c2 ;Bボタンが押されたか
a5bf: jmp  $a646 ;メニュー終了
a5c2: lda  <$24
a5c4: beq  $a5a9 ;Aボタンが押されたか

a646: jsr  $a685 ;画面全体の nametable を data 0 で埋める (いらない)
a649: lda  #$00
a64b: sta  $2001 ;PPU 描画停止
a64e: jsr  $a654
a651: jmp  $8f58 ;移動画面に戻る

PC:$a646 は本当に無駄な処理で単純にこの jsr のみをなくしても早く移動画面に戻ります.

PC:$a5c4 以降はボタンが押されたかで分岐が入り、アイテムや装備の画面切り替えに飛べます.

下記はアイテムのメニューのプログラムの抜粋です.

9ec2: jsr  $9592
9ec5: lda  #$80
9ec7: sta  <$b4
9ec9: jsr  $a685
9ecc: jsr  $956f
9ecf: jsr  $a328
(中略)
9efe: jsr  $a7cd
9f01: lda  #$08
9f03: jsr  $920d
9f06: lda  #$03
9f08: jsr  $8000
9f0b: lda  <$25
9f0d: bne  $9ebc
9f0f: lda  <$24
9f11: beq  $9efe

アイテム以外のメニュー画面も流れは大体同じで、 PC:$a685 で nametable を data 0 で埋めて、 PC:$956f で PPU を描画にして(すでに描画になってるのですが)、VRAM と変数の初期化命令が入り、 PC:$a7cd が呼ばれた後ボタン入力でループを回しています.

プログラムを直す

直接の原因

命令が増えることもあり、空き領域の確保が求められますがここでは省略します.

変数を追加して vblank 待ちを行わないフラグによって分岐するようにしました.

	org	$edcd
	jsr	edcd_new
	
	org	$f693
	jsr	f693_new

	org	xxxx ;PC: $c000-$ffff のどこか
;skip waiting vblank (jsr $ff00)
edcd_new:
	lda	<render_count
	bmi	edcd_skip
to_vblank_wait:
	jmp	$ff00
edcd_skip:
	pla
	pla
	jsr	$f6aa
	jmp	$c9a9

f693_new:
	lda	<render_count
	bpl	to_vblank_wait
 rept 3
	pla
 endm
	inc	<$f0
	jsr	$f6aa
	jsr	$ede1; update scroll register
	jmp	$f683

変数 render_count は address:$00dc 付近を使ってます. address: $00dc-$00e0 あたりは未使用の zeropage のようでした. この領域は reset のあと data 0 で埋めています. (address: $00f0-$00ff 付近は 0 クリアをやってない)

edcd_skip は無理やりやってるので rts 経由せずに pla x2 をいれています(あまりよくないです). PC:$ff00 以外にもサウンドドライバやいらなそうなのは削ってあります.

メニューのプログラム

修正すべてをここには記載できませんが、下記の方針で該当部分を修正します.

  • 初期化
    • jsr/jmp $a646 と jsr $956f は消してかわりに jsr display_off をいれる
    • jsr $ff00 とか OAM への DMA request も消す
    • その他 address $2001 で描画を有効にする命令があれば消す
    • jmp $a646 で省いて VRAM で data 0 にすべきところがでてくるので直す
  • キー入力待ちのループ
    • jsr $a7cd を jsr a7cd_new にする
  • メニューの終了 (PC:$a646)
    • 変数 render_count = 0 にする
display_off:
	lda	#0
	sta	$2001
	lda	#$80
	sta	<render_count
	jmp	$c486 ;clear OAM buffer

a7cd_new:
	lda	<render_count
	lsr	a
	bcs	render_temp_0
;----1度目----
;nametable attribute を書いた後,一旦戻る
;subroutine の外で OAM buffer を書いてもらう
	lsr	a
	bcc	ppu_attr_fill_ff
	jsr	$95b2 ;draw nametable attribute
	jmp	a7cd_new_noattr
ppu_attr_fill_ff:
	ldx	#4
	jsr	ppuram_fill
a7cd_new_noattr:
	lda	#%0001 | $80
	sta	<render_count
	jmp	$A7D7
render_temp_0:
	lsr	a
	lsr	a
	bcs	render_temp_1
;----2度目----
;renderer on, OAM buffer -> OAM
	jsr	$ff00
	lda	#%0101
	sta	<render_count
	jsr	$959f
	lda	#0
	sta	$2003
	lda	#2
	sta	$4014
	jmp	$d308
;----3度目----
;通常処理
render_temp_1:
	jmp	$a7cd

a7cd_new について. jsr $a7cd は共通で呼ばれていることと、OAM の初期化が 描画開始から 1 frame ずれてしまう対策として作りました. メインループに入ってから 2 度は初期化が続きます.

  • 1度目はその他足りてない VRAM の初期化をいれて一度抜けて、 OAM buffer を更新してもらいます. VBLANK は待ちません.
  • 2度目は VBLANK を待ち, PPU を画面描画を再開、直後に OAM buffer を DMA で OAM に転送し、さらにパレットも更新します.
  • 3度目は通常通りです.

このため FF1 と違って nametable と object が同時にすべて出るようになります.

対策後

メニュー表示が FF1 並になりました. この動画は他にも修正入れまくってますがメニュー表示に関しては大体同じです.

www.youtube.com

PC:$a25f 開始から画面描画完了までは 8 frame でできるようになります. 8 frame のうち約半分の時間はキャラクタRAMへの転送(jsr $dd06)です. 残り半分はnametable への転送ですが、もともとの設計が 64 byte 程度のバッファ経由で少しずつ動かすようになっていてこれ以上の短縮は難しいです.

キャラクタRAMへの転送のループも 1 byte 転送で分岐命令が入っていたので 1 byte から 8 byte にかえて画面描画時間を 7 frame に一応は削れはしました. ただし ROM の空きの確保に苦労に対して効果は見合っていませんでした.

お仕事募集

自己紹介

筆者は個人事業主で、電子回路とそれに関わるファームウェア開発を主としております.

2021年になってから半導体の供給不足が問題となり状況は毎月悪化していて回復の見込みがなく、ソフトウェア開発の仕事を募集いたします.

得意分野

6502, 68000, Z80 採用の家庭用または業務用ゲームソフトの解析やソフトの改修を得意としております. 今回の TAS のような古いゲームのソースコードがない状態でのバグ修正や改善ができます.

実績

会社員時代の業務実績がございます. 詳細については個別にご回答いたします.

  • 激しい画面点滅演出の削減
  • 権利表記の修正, 社会情勢の変化に伴う記号や肖像の修正
  • 飛ばせないシーンを飛ばせる用対応もしくはその逆の対応
  • RPG での効率化 (移動速度高速化, 敵出現率低下)

基本、オリジナルのプログラムに最小限の修正を加えて対応しますので、エミュレータなど上位レイヤーで特別な対応が不要となります.

参考納期(画面点滅対応)

  • ハードウェア仕様を筆者が既知の場合は1週間.
  • ハードウェア資料があるが筆者が知らない場合それに加えて2週間.
  • ハードウェア資料もなく開発ツールの開発が必要な場合は1か月以上.

お問い合わせ方法

作業単価などの詳細のお問い合わせは個別に回答いたします.

お手数ですが、コメント欄にメールアドレス記載の上ご連絡ください. メールでご返信いたします.

ファイナルファンタジー3のTASの詳細

FF3 のゲーム開始前

ルールとして許可されているなら address 0x00f9 へ data 0xdd を仕込むことができます. 簡単な調査で見つかったのは address 0x00f9 を 1P のキー入力として使えるのは(superがない)魂斗羅と F1 センセーションの2本でした.

data 0xdd は上下右スタートBAをすべて押すため、ジョイカードを併用してください. ボタンをおしたままカセットを抜いて、電源をいれたまま FF3 をさしてリセットボタンを押してください.

address 0x00f9 を 2P のキー入力として使っているソフトはコナミの中ではかなり多いのですが, 1P は2本だけでした.

祭壇の洞窟 (1回目)

フロアその1

最初のゴブリン戦で3匹のゴブリンからポーションを2つ拾います. 理論上3つのポーションが落ちるようですが、最速プレイとしては利用できる条件ではありませんでした.

宝箱からポーションとかわのたてを拾います. かわのたては2個目のみです. もう1個のかわのたては購入したほうが早いです.

フロアその2

宝箱からポーションを拾います. 一見6歩無駄ですが、戦闘中のアイテム交換を 1回(177 frame)削減できます. 宝箱のための往復での6歩は 96 frame なのでとります.

なんきょくのかぜは遠く時間削減ができないので取りませんでした.

ランドタートル戦は TAS submission #2992 と同じ総和(詳細は後述)で倒しました. 手動操作では運が必要です.

祭壇の洞窟 (2回目)

ウルの村で大量の物資を購入するため 2000 ギルを拾得します.

ウルの村

スクリプトポインタの初期化

前回の TAS ではピアノを弾いてスクリプトポインタを初期化していたのですが、その代わりにそこらへんの人と会話します. ボタン操作の簡略化のため、買い物前に会話するほうがいいです.

買い物

基本方針は下記です.

  • 戦闘中の武器交換回数をへらすためにアイテム欄はできるだけ埋める.
  • アイテムの名称より数量とXの有無のほうが重要.

道具屋は X がつかないアイテムが3種類売ってるのですが、往復に 177 x2 frame 以上かかるので採用していません. 防具屋も実はいかなくても条件を満たせるのですが、アイテム欄を埋める時間の節約と消費金額が少なさ(装備できるかわのたてが安い)の2つの理由で行きます.

TAS では歩数の都合でウルの村で敵と遭う都合とアイテム整理の操作削減のため「防具屋->魔法屋->武器屋->村内の敵が出る場所」の順番に行きます. 歩数の制限と村民たちの妨害がなければ、「武器屋->防具屋->魔法屋->祭壇の洞窟(3回目)」のほうがアイテム整理は楽です.

アイテム整理

TAS では戦闘前に下記の並びにしています.

 右手:かわのたて: 1
 左手:かわのたて: 1
  0:ダガー          :12  1:かわのたて      :11
  2:(空欄)          : 0  3:(空欄)          : 0
  4:(空欄)          : 0  5:(空欄)          : 0
  6:(空欄)          : 0  7:ロングソード    : 1
  8:(空欄)          : 0  9:(空欄)          : 0
 10:ポーション      : 1 11:(空欄)          : 0
 12:(空欄)          : 0 13:(空欄)          : 0
 14:(空欄)          : 0 15:(空欄)          : 0
 16:ふく            : 3 17:かわよろい      : 1
 18:(空欄)          : 0 19:どうのうでわ    : 4
 20:かわのぼうし    : 5 21:(空欄)          : 0
 22:ヌンチャク      : 2 23:ポイゾナ        : 4
 24:つえ            : 7 25:(空欄)          : 0
 (以下すべて空欄)

戦闘中にアイテム欄を下記にすると必要な変数をうまく操作できます. 条件を守れば上記の並びと同じにしなくていいです.

 右手:Y     : 1 
 左手:Y     : 1
 0から15: Y : 1
 16:X       :3+ 17:N       : 1
 18:N       : 1 19:N       :3+
 20:N       :3+ 21:N       : 1
 22:N       : 2 23:ポイゾナ:3+
 24:N       : 7 25:N       : 1
 26:W       : 1 27:N       : 1
 28から31:N : 1
アイテム種類の見方
  • Y: X がつかないもの
    • 装備できる手につける装備品, または消耗品
  • X: X がつくもの
    • 装備できない手につける装備品, 手につけない装備品, 魔法のオーブ
  • N: どれでも可能
  • E: ID の bit7 == 0 && bit1 == 1 のもの
    • 序盤で手に入るものは下記.
    • ヌンチャク,つえ,ナイフ,ダガー,かわのぼうし,ふく,かわよろい
    • 戦闘強制終了フラグを書き込む
  • W: ID で 0x20 から 0x5f ではないもの. (文字として濁音か半濁音ではないもの)
    • 序盤で手に入るものはかわのたてをいれてはいけないので注意
    • FF3 のゲーム前に RAM を初期化しているのなら条件外
  • ポイゾナ: ポイゾナのみ可
数量の見方
  • 0,1,2: 指定通りの数量をいれてください.
  • 3+: 3個以上. 4個だと早く買えます.
  • 7: TAS では 7 個. FF3 のゲーム前に RAM を初期化しているのなら 8 個.
    • ポインタの上位バイトを書き込む
TAS での並びの解説
  • ダガーとかわのたてはアイテム交換を早くすませるためです.
  • 右手左手両方に装備をしてください. (数量は1が必須)
  • X がつかないアイテムは位置の制限が緩いので、ポーションとロングソードは位置交換の過程でそのままにしてます. もしほかに消耗品があれば同様の処置で構いません.
  • 16番のふくはかならず X がつくものをおいてください.
  • 17番以降は X の有無は関係ありません.
    • 17番では E の条件をみたすかわよろいをおいていますが、アイテム交換の過程でうまくいくならダガーで構いません.
  • ダガーとかわのたては12個ずつ買います. アイテム購入操作回数をへらすために 4 個単位で3回買っています. そのため残金がわずかです.
  • かわのたてのかわりにロングソードを交換品にすることも一応可能です. 10単位での買い物を組み合わせた上に、複数人から装備品を外す必要があり、早くありませんでした.

敵に遭う

今回の方法は外では成功しません. 村の中かダンジョンで遭ってください.

FF3 のゲーム前に RAM を初期化していない場合は "上下右スタートBA" を押して敵に遭ってください. この組み合わせで移動できる方向は右だけです. ウル内部の敵がでてくる場所はせまく、右に移動しづらいです.

FF3 のゲーム前に RAM を初期化している場合は移動前のボタン入力の制限はありません.

戦闘では下記の行動です.

  • 1人目(1度目): アイテムでナイフを使う
  • 2人目:
    • アイテム欄をすべて埋めるために武器を交換し続ける
    • 終わったら B ボタンで戻る
  • 1人目(2度目): B ボタンで一旦キャンセルし、コマンド欄に不正な文字列を出す
  • 1人目(3度目): 4番目のコマンドを選び戦闘強制終了
    • (前回のTASでは2度不正な文字列を出してましたが、今回は1度でいいです)

戦闘後

FF3 のゲーム前に RAM を初期化していない場合は 30 秒程度待つとエンディングが始まります. これは pointer が 0x07f8 から 0x0821 まで順番に村内の人をゆっくり動かしているのが原因です.

FF3 のゲーム前に RAM を初期化している場合はすぐにエンディングが始まります. こちらは pointer が 0x08f8 から 0x08f9 で読み込み数が少ないためです.

戦闘時の乱数

基本的に FF2 での説明と同じです.

Zeropage 総和

移動画面から戦闘画面に切り替わるときにプログラムは address $0000-$00ff の zeropage の carry 込みの総和を保存します. 総和は戦闘時の乱数 table への index となり、敵の数や行動の結果が決まります.

総和とパーティのパラメータが同じであれば、経過時間は関係なしに容易に結果が確定できます. 乱数 table の中身は総和の bit1 で 2 通り用意されています.

ゴブリン戦とランドタートル戦は戦闘直前にボタンを複数押すのは総和の調整のためです. (キラービー戦は別の理由)

乱数 index

乱数 index は memory address $0015, $0016, $0017 に利用され、初期値は総和で、乱数計算後に都度加算されていきます. FF3 では変数が3つに増えましたが、基本は $0016 を利用しているようです.

ゴブリン3匹では総和 bit1 = 0 で、戦闘終了時に address $0016 が data 0xfd の場合3個のポーションが落ちることがわかっています. 戦闘中の行動次第で調整できるはずですが、最速ではその実現は不可能でした.

その他見つかったが実用化されなかったもの

会話の省略

ウルの東西の細い道にある程度入った状態で敵を出せれば会話は省略できました. 歩数調整や徒歩時間の都合で使えませんでした.

別のアイテムの並び

未記載分は X 以前は Y:1, X 以後は N:1

 14:EY      : 1 15:Y       : 1
 16:Y       : 1 17:X       : 1
 18:N       :3+ 19:N       :3+
 20:N       : 1 21:N       : 2
 22:ポイゾナ:3+ 23:N       : 7
 24:N       : 1 25:W       : 1
  • EY は E と Y を同時に満たすもの. 実質ダガーのみ.
  • 13:Y:3+ がロングソードのみで必要金額が厳しい.
  • アイテム欄の並びが1つ手前に来るのが特徴. 実際に使ってみたところは手数が多いらしく早くなかった.
  6:Y       : 1  7:Y       :3+
  8:EY      : 1  9:Y       : 1
 16:Y       :3+ 17:Y       :3+
 18:Y       : 1 19:X       : 2
 20:ポイゾナ:3+ 21:N       : 7
 22:N       : 1 23:W       : 1
  • アイテム欄がかなり手前に来るのだが、 Y:3+ が3種類いるので道具屋に行くことが必須となったので不採用.
  • おそらく金額の制限にもひっかかる.

バグの詳細

続きは TASvideos に説明を書きましたのでご覧ください.
http://tasvideos.org/7197S.html

FF2 の script を調べた

他の作業がスランプのためファミコンソフトの解析をしてごまかしてます...

script data の解析

エンディング呼び出しだけ調べてましたが他のも調べました.

data 0x00 から 0xbf (1 byte)

キャラの移動と方向転換、あとは初期化ができるかもしれません.

databit
7:4 chara ID
3   ?
2   move flag (0:move, 1:方向転換のみ)
1:0 direction (0:right, 1:left, 2:down, 3:up)

bit 3 = 1 は初期化に使ってるようですが情報量が少なすぎるのでスクリプトだけではできない気がします.

data 0xc0 から 0xcf (1 byte)
databit
7:4 chara ID 0xc
3   mode
(bit 3 == 0 のときは data 0x00 から 0xbf と同じ)
(bit 3 == 1 のとき)
2:0 button を押したことにするためのコード

chara ID 0xc はプレイヤーを操作しますが、bit 3 == 1 のときは address $0020 を操作してます. 最初のヒルダに会うところとか定期航路での操舵とか聖堂での蘇生後に出る配置用メニューで使われています.

0xc8 は A ボタンを押したことにする操作でしたが、これで会話相手がいるといと event ID #1 が強制実行されたり、会話相手が各種店員だと想定外のことを言ったりしました. このバグの利用はできなさそうです.

data 0xd0 から 0xdf (1 byte)

address $6012-$6013 の data を 1 bit 単位で反転します.
エンディングが呼び出せるので相対的に重要ではないと感じています.

databit
7:4 0xd
3   byte offset
2:0 bit number

address $62bb (宿屋のイベントID 兼 ブリンクの本の武器としての熟練度経験値), data 0x5c の場合、スクリプトが address $6011 (セーブした外のY座標)から実行できますので、任意の 3 byte を生成するにはこれが利用できます. しかし address $6011 の操作にはその場所まで行くのがかなりの手間です.
さらに address $6012-$6013 のゲームの進行中のフラグの管理で操作するとアルテアの町が破壊されて宿屋が減ることを確認しています.

data 0xe0 から 0xef (1 byte)
e0 shake the screen
e1 run battle ship
e2 break battle ship
e3 light off screen?
e4 dance
e5 ???
e6 flash the screen
e7 go to epilogue
e8 sleep; wait key input
e9 tsunami
ea-ec swap backup chara status and chara #3
ed copy chara #3 to backup chara and add Lyla
ee add Leon to chara #3
ef remove chara #3?
  • e1-e3 はスクリプトが続行できないような動きでした.
  • ea-ec は実行アドレスが同じで違いはないみたいです.
  • ea を2度実行するとフラグだけで消えてる初期型レオンにいさんを追加
  • ed は#3にいるキャラを一旦離脱用に退避してレイラを追加
  • ee は後期型レオンにいさんを追加(上書き)
  • 他のキャラはスクリプトから呼べない
data 0xf0 から 0xf8 (2 byte)

引数が 1 byte あります. 各引数は想定値が大きくないものもあるので、変な値をいれるとちょっとおかしくなりますが、致命的なものはなさそうです.

f0 xx show message
f1 xx teleport to outside
f2 xx teleport to inside
f3 xx start battle
f4 xx same command f2
f5 xx set music
f6 xx bit flag set for address $6040-$6043
f7 xx bit flag clear for address $6040-$6043
f8 xx wait xx frames
data 0xf9 から 0xfc (2 byte)

1つ前に実行した移動系コマンドを引数の数繰り返します.
移動系以外をいれると無限ループになってしまうようです.

jump table で実行する PC がすべて同じで違いはないようです(自信なし).

data 0xfd (1 byte)

場所のスタックを pop します. デジョンレベル1と同じです.

data 0xfe (1 byte)

無限ループしてしまうようです. (data 0xff と同じ処理に見えるのですが...)

data 0xff (1 byte)

スクリプト終了で通常操作に戻ります.

TAS の説明の補足

宿屋で「はい」を選ぶときに address 0x0020 の data を 0xe7 にするとエンディングにいけるのは事実なのですが、動作の詳細が私の想定外でしたので記載します.

現状では script system は address 0x0020 を読み、その次の address 0x0021 の data が 0xe7 だったのでエンディングにいけます. address 0x0020 は「いいゆめを...」の反応のときに押すキーで、ここで引数をもつ script data をいれると address 0x0021 はコマンドとしては無効になります.

「いいゆめを...」のところは一旦ボタンを全部離してからなにか押すと動作が進みます. ボタンを1こだけ押すとなにかの移動コマンドになるので重要ではないのですが、ここに data 0xe4 をいれると宿屋のスタッフが踊ったあとエンディングにいけます. ただし指定したボタンを1度に(16.6ms 単位の精度で)全部押す必要があるので手動ではかなり厳しいです.

address 0x0021 は START, SELECT, B,A に関してはOFF->ON のトリガ検出のためのバッファで、方向は上下や左右の同時押しを解消しているプログラムになっているのは確認しましたがちょっと意味がわかりませんでした. そのために入力値にはいくつか制限があります.

bit7 は A ボタンのため 1, bit6:4 は自由, bit3:0 はよくわからないのですが入力可能値は 5 6 7 9 A B D E F の 9 通りです. 引数にあまり自由をもたせられないことになります.

つまり TAS の最後のボタン入力に何かしら演出をいれるとエンディングの発生が伸びますが、TAS としては終わっているので入力時間は変わらないことになります.

mruby のコンパイルがうまくいかない

楽しかった TAS は終わりで mruby の構築をはじめます.
いままでは pacman -S mruby でとってこれるバイナリパッケージを使ってましたが、 require が使えないことが大変によろしくないのでコンパイルをすることにしました.

一連の流れ

git clone https://github.com/mruby/mruby
cd mruby
cp build_config/default.rb build_config/myconfig.rb
(編集後)
rake -v MRUBY_CONFIG=myconfig

build_config/myconfig.rb は必要に応じて機能を追加削除できます.

自分は rake を使ったことがなかったので勝手を覚えるのに手間取りました. というのもこのビルドシステムは結構な規模がある上、並列処理もあるのでしょうけど、エラーメッセージが流れて追いかけるのが大変でした.

MRUBY_CONFIG=default なら msys2 であってもエラーがでなくて助かりました.

iij/mruby-require と fts.h

default ならバイナリパッケージでいいのでここからです. build_config/myconfig.rb に追加します.

  conf.gem :github => "iij/mruby-require"

そうすると自動的にソースを取ってきてビルドとなりますが、エラーが流れてしまって当初は気づきませんでした.

fts.h がないというのは、私の msys2 の中では /usr/include にはあって、 /mingw64/include にはないということです. さらに rake が /usr/include と書いても認識しないらしく、Windows として本当の絶対パスの d:/dev/msys64/usr/include と書いて認識しました.

次の問題が d:/dev/msys64/usr/include の path を build_config/myconfig.rb に書いてはならず、 build/repos/host/mruby-tempfile/mrbgem.rake に書き足せばコンパイルが通ります.

たぶんこれは msys と mingw を混ぜてはいけない注意が示す実例のようで、 /usr/include と /mingw64/include には重複するファイルもあり、混乱を抑えるために mruby-tempfile だけにしたらコンパイルが通っただけだと思われます.

さらに下記の msys ライブラリ (cygwin でいう POSIX 互換レイヤーだと思う)を足すとリンクも通ります.

    spec.linker.library_paths += ["d:/dev/msys64/usr/lib"]
    spec.linker.libraries << 'msys-2.0'

そしてリンクが通ったところで実行ファイル (mirb) は動かないです. require を抜けば動きます. ここどうしていいかわからないです.

仮に動いても msys 互換レイヤ込でバイナリ配布はあまり気が乗らず. fts って何のためにあるのか、どうせならなくしたい、そんな気持ちです.

正規表現ライブラリ

正規表現もバイナリパッケージに入っていません...

  conf.gem :github => "mattn/mruby-onig-regexp" #うごいた
  #conf.gem :github => "mattn/mruby-pcre-regexp" #ダメ
  #conf.gem :github => "pbosetti/mruby-hs-regexp", :branch => "master" #ダメ
  #conf.gem :github => "masamitsu-murase/mruby-hs-regexp", :branch => "master" #ダメ

たくさんあるみたいですが今でも使えるのは mattn/mruby-onig-regexp だけみたいでした. ビルドが通らない、ビルドが通っても落ちる. .... 楽しくないです.

不安定な mirb

基本機能はこれだけにしてます.

  conf.gembox "stdlib"
  conf.gembox "stdlib-ext"
  conf.gem :core => "mruby-math"
  conf.gem :core => "mruby-rational"
  conf.gem :core => "mruby-io"
  conf.gem :core => "mruby-print"

これと mattn/mruby-onig-regexp で mirb をためしたところ画像の感じです.
f:id:na6ko:20210610214849p:plain

特に未使用変数書いただけで Segmentation fault は勘弁して欲しいです.

FF2 の TAS を作った

先週土曜日に完成しました.
https://www.nicovideo.jp/watch/sm38840650

詳細はこっち.
http://tasvideos.org/7136S.html

今回の TAS を通じてわかったのは最適化作業はつまらなく Lua 言語がマジに作れば作るほど貧弱の2点です.

前回ここにかいた 67 回の AB キャンセルは減ることなく、大きな変更はなく投稿してしまいました. 実は昨日、ブリンクのほんを2冊(2刀流で)持つと AB キャンセルの回数は減るのかを調べ忘れていていまさら調べました. その回数は減りはしたんですが熟練度経験値への書き込みが偶数のみということで、各種手続きのほうが時間がかかるので(運良く)作り直しはありませんでした.

はじめての TAS は Lua で全部操作する仕組みでやりました. これは順番のいれかえや乱数調整がやりやすいのでよかったんですが、 Lua の言語仕様が悪い意味で簡単すぎて作り込みたくても作れなくて Squirrel ならできるのにーとかエラーで止まってくれるのに思ってました. 後者は相当きつくちょっとした記述ミスもだまって通ってしまうので修正作業が大変に苦痛でした.

いままで Cheap さんが見つけたものを解析して応用できたのが初めてだったのでちょっと無理をして自分が突っ走ってしまったのはあまりよくなかったかなと思ってます.

それにつきあってくれたピロ彦さんありがとうございました. ただ、彼のためにオーガメイジからのドロップという更新候補をプレゼントしましたし、どうも彼はその問題を解決できてしまったようです. つまり 1 週間ぐらいで TAS は更新されてしまうことになりそうです.

FF2 の皇帝呼び出しを調べたらさらに短縮できた

前回のイベント番号で CPU address $0072 の script (scenario) pointer へ設定される値は下記です. イベント番号@ pointer の table は CPU address $0d:$bfc0 にあり正規の値は 0 から 0x19 でそれを超えると MMC1 の bank 境界をまたぎます.

ROM

FF3 同様エンディングに直行できる script data は決まっていまして 2013 年に自分が調査しています.
https://na6ko.hatenadiary.jp/entry/20130806/p2
正規の手順でしたらイベント番号 0x0f の皇帝登場がまさにそれです. イベント番号 0x40 はエンディングの途中ですが初期化していない変数があるのか止まります.

0x00@ $8000: 0xc2 0xf9 0x07 0xc0 | 0x45@ $8d02: 0xb1 0xa2 0xa4 0x3c
0x01@ $8088: 0xc0 0xf9 0x18 0xc3 | 0x3b@ $8eff: 0xa2 0x46 0xc3 0xc3
0x02@ $8098: 0xc0 0xc0 0xc2 0xf9 | 0x2f@ $9c0c: 0xe5 0xc8 0xf0 0x0b
0x03@ $80a7: 0xf8 0x40 0x13 0x22 | 0x3a@ $a220: 0x1c 0xea 0x9c 0xf2
0x69@ $80a9: 0x13 0x22 0x10 0x21 | 0x26@ $a24c: 0xf3 0x1c 0xe1 0x9c
0x78@ $80bd: 0x12 0x12 0x11 0x12 | 0x65@ $a261: 0xe0 0x43 0xf2 0xb5
0x04@ $811b: 0xf8 0x10 0xe0 0xe0 | 0x6b@ $a262: 0x43 0xf2 0xb5 0x2a
0x05@ $8164: 0xf8 0x10 0xf0 0x11 | 0x48@ $a5dc: 0xbb 0xcb 0xf1 0xbb
0x06@ $81b7: 0xf8 0x10 0xe0 0x2a | 0x66@ $a900: 0xcb 0xfd 0x15 0xa9
0x07@ $8208: 0xc3 0xf9 0x04 0xf0 | 0x6c@ $a900: 0xcb 0xfd 0x15 0xa9
0x08@ $827f: 0xf8 0x10 0x03 0xf9 | 0x4b@ $a90f: 0x1e 0x2e 0x3e 0xfc
0x09@ $82c6: 0xf7 0x39 0x0f 0x1f | 0x3d@ $a99a: 0xa8 0xa1 0xa9 0x9a
0x0a@ $830b: 0xf8 0x40 0x03 0xf9 | 0x32@ $a9fe: 0xd8 0x18 0xed 0xf2
0x0b@ $839b: 0xf8 0x40 0x3a 0x32 | 0x44@ $a9fe: 0xd8 0x18 0xed 0xf2
0x0c@ $83e3: 0x0f 0x1f 0xf8 0x20 | 0x5b@ $ad27: 0x25 0x65 0x46 0xcb
0x0d@ $844d: 0xf8 0x80 0xc2 0xf9 | 0x56@ $b642: 0xf1 0xb8 0xf2 0x6b
0x0e@ $850f: 0xe6 0xe6 0xe6 0x28 | 0x52@ $b890: 0x3b 0xcb 0x3b 0xcb
0x0f@ $8531: 0xf8 0x40 0xf0 0x37 | 0x64@ $c08d: 0xa9 0x7f 0x20 0x00
0x60@ $8546: 0x1b 0x2b 0xc7 0x3a | 0x42@ $c46e: 0xa2 0x3f 0xa9 0xf0
0x5a@ $8560: 0x23 0xc3 0x03 0x13 | 0x58@ $c486: 0xa2 0xef 0xa9 0x00
0x5f@ $8560: 0x23 0xc3 0x03 0x13 | 0x3f@ $c4a0: 0xa9 0x30 0x8d 0x00
0x4c@ $8577: 0x13 0xc3 0x03 0x13 | 0x25@ $c74f: 0xa9 0x0d 0x20 0x03
0x79@ $85b4: 0xf9 0x06 0x5f 0x6c | 0x7f@ $c832: 0xa5 0xff 0x8d 0x00
0x40@ $8620: 0x44 0x26 0xf0 0x45 | 0x49@ $c9fa: 0xad 0x47 0x60 0x29
0x10@ $8690: 0xe6 0xe0 0xe6 0xe0 | 0x7b@ $ca41: 0x20 0x6f 0xd0 0x20
0x11@ $86b4: 0xe6 0xc4 0xe6 0xc6 | 0x28@ $dc30: 0xad 0x02 0x20 0xa9
0x12@ $86eb: 0xf8 0x40 0xc1 0xc1 | 0x22@ $dce3: 0xa9 0x05 0x85 0x61
0x13@ $8719: 0xf8 0x20 0xc4 0xc6 | 0x4a@ $f077: 0x20 0xe8 0xe4 0x91
0x14@ $875e: 0xc2 0xc1 0xf9 0x04 | 0x2b@ $f2c8: 0xad 0xf5 0x62 0x10
0x15@ $876f: 0xe6 0xe6 0x3a 0x32 | 0x4e@ $f476: 0xa9 0x88 0x85 0xff
0x16@ $877e: 0xe9 0xf4 0x17 0xf8 | 0x6a@ $f58d: 0xa5 0x61 0x29 0xe0
0x17@ $8789: 0xf3 0x78 0xf0 0x4d | 0x68@ $fa00: 0x4c 0x9e 0xfa 0x48
0x18@ $8790: 0x05 0xf0 0x4f 0xc1 | 0x38@ $fd85: 0x20 0xa5 0x00 0x8d
0x19@ $87c5: 0x2c 0x22 0x22 0xf9 | 0x6e@ $fe03: 0x4c 0x1a 0xfe 0x8d
0x36@ $88a9: 0x9c 0x91 0xaf 0xc4 | 0x35@ $fe85: 0x10 0x40 0xa9 0xc0
0x33@ $8d00: 0x7f 0x8c 0xb1 0xa2 | 0x37@ $ff85: 0x00 0x00 0x00 0x00

RAM + その他

この中でわかりやすいものは 0x43@ $0020 (ボタン入力)と 0x5c@ $6011 (セーブした外のY座標)です. 最初に気づいたのは CPU address $6011 のほうで徒歩やチョコボで移動してセーブし、エンディングにいけるデータを書き込み、宿屋からエンディングにいけることを確認しました. 目的の場所がかなり遠かったので最速プレイにはなりませんでした.

次に気づいたのは CPU address $0020 で宿屋で宿泊する際にボタン入力から必要なデータをいれると宿屋からエンディングにいけました. これはネリーを助けなくていいし、皇帝を倒さなくていいのでかなり早いです.

熟練度経験値を 0x43 にするために 67 回もたたかうを選びキャンセルすることになるのでまだ縮められるのではと考えています.

0x43@ $0020: RAM             |0x3e@ $2000: undef
0x29@ $184c: RAM mirror $004c|0x6d@ $2000: undef
0x39@ $008d: RAM             |0x34@ $2001: undef
0x2c@ $00a9: RAM             |0x57@ $2008: undef
0x63@ $08a9: RAM mirror $00a9|0x7e@ $2040: undef
0x7c@ $00a9: RAM             |0x7a@ $2048: undef
0x59@ $10ad: RAM mirror $00ad|0x67@ $207f: undef
0x5e@ $18ad: RAM mirror $00ad|0x41@ $20c4: undef
0x6f@ $00bd: RAM             |0x4d@ $20fa: undef
0x1a@ $0200: RAM             |0x2e@ $20fe: undef
0x1b@ $0200: RAM             |0x51@ $20fe: undef
0x1c@ $0200: RAM             |0x55@ $20fe: undef
0x1d@ $0200: RAM             |0x20@ $254c: undef
0x1e@ $0200: RAM             |0x61@ $2842: undef
0x1f@ $0200: RAM             |0x5d@ $2885: undef
0x2d@ $0320: RAM             |0x73@ $2985: undef
0x50@ $0320: RAM             |0x77@ $2a85: undef
0x54@ $0320: RAM             |0x47@ $3020: undef
0x31@ $034c: RAM             |0x62@ $32b0: undef
0x7d@ $158d: RAM mirror $058d|0x70@ $38b4: undef
0x30@ $0ea9: RAM mirror $06a9|0x75@ $38b4: undef
0x4f@ $0ea9: RAM mirror $06a9|0x72@ $3f29: undef
0x53@ $0ea9: RAM mirror $06a9|0x46@ $4014: undef
0x71@ $07e9: RAM             |0x74@ $40bd: undef
0x76@ $07e9: RAM             |0x21@ $4cc0: undef
0x5c@ $6011: RAM             |0x2a@ $4cc0: undef
0x3c@ $6f25: RAM             |0x27@ $4cdb: undef
                             |0x24@ $4cf3: undef
                             |0x23@ $4d4c: undef

FF2 の皇帝呼び出しを調べた

原理説明 (簡素)

  • 2人目がブリンクの本を武器として熟練度を上げたときに CPU address $62BA に data 0x0f (10 進数で 15) が代入される.
  • CPU address $62BA は聖堂で復活するときに起きるイベント番号で 0x0f は皇帝が出てきて最終戦となる.

イベント番号が代入される理由

  • CPU address $6200-$62ff は大半はプレイヤーキャラのパラメータ置き場.
  • その中で 0x40 byte ごとにキャラ #0, #1, #2, #3 (以降p) となる.
  • 武器としての熟練度は $6200-$620f + p * 0x40 にある.
  • 更新アドレスの計算は $6200 + p * 0x40 + index * 2. index の正常な値は 0 から 7 だが、魔法の本の武器としての index は不正な値で不連続で最小値 0 で最大値 0x64.
  • ブリンクのほんの index は 0x3d でキャラ #1 (2人目) だと $62ba が算出される.
  • CPU address $62ba にはもともと data 0x15 が入っているが熟練度レベルとしての最大値は数値で 0x0f (画面表示の数字は 16) なので 0x0f が代入される
  • CPU address $62bb も熟練度経験値として計算する. この変数は宿屋で宿泊したときに起きるイベント番号. 番号の振り方は CPU address $62ba と同じ. cheap 氏の 2021/2/23 での記述が参考になる.

変数領域の発掘

  • キャラのパラメータは 0x40 byte ごとと書いたが厳密には offset 0x34-0x3f の末尾の方は関係のないパラメータで CPU address $62ba がその例.
  • 自分が見つけたところでは CPU address $62f4 が 4 人目がいるかというフラグだった. ほかは用途が不明.

不正な武器での熟練度更新 address

下記2条件で総当りで調べた.

0 0xac 0x6234 | 2 0xba 0x6314
1 0xac 0x6274 | 3 0xb6 0x6332
0 0xb1 0x627a | 3 0xb1 0x633a
2 0xac 0x62b4 | 2 0x9b 0x6348
1 0xb1 0x62ba | 2 0xaa 0x6348
3 0xac 0x62f4 | 2 0xb4 0x6348
2 0xb1 0x62fa | 2 0xb9 0x6348
1 0x9b 0x6308 | 3 0xaf 0x634c
1 0xaa 0x6308 | 3 0xba 0x6354
1 0xb4 0x6308 | 3 0x9b 0x6388
1 0xb9 0x6308 | 3 0xaa 0x6388
2 0xaf 0x630c | 3 0xb4 0x6388
3 0x98 0x6310 | 3 0xb9 0x6388
3 0xa0 0x6310
3 0xa5 0x6310

アドレスの定義

  • CPU address $6044 ($6344) data bit2 = 1 とするとネリーが救出されたことになるが上記の方法では更新できない.
  • CPU address $6300-$631f は大雑把には位置、お金あたりが入っている.
  • CPU address $6020-$6047 ($6310-$6347) 辺りは大雑把にはゲーム進行のフラグになっている.
  • CPU address $6080-$60ff ($6380-$63ff) 辺りは大雑把には宝箱を開けたかのフラグになっている.

考察

  • 熟練度レベル最大値が 15 であることと皇帝が出てくる数値が 15 であることは偶然の一致で奇跡.
  • ほかの数値は進行上初期化している値があるような前提なので無理やり選んでも止まってしまうのに、皇帝が出てくる場面は必要条件がない. これも奇跡.
  • ブリンクの本を装備したときに更新するアドレスがイベント番号であることも偶然の一致.
  • バグの挙動に詳しい cheap さんの進め方は説得力がある. 長期間丹念に調べていることは驚異で彼の功績のおかげで発展できている.

パラレル 5V の flash memory の datasheet をみた

anago 相当の flash programmer を mruby で作ってみようとしました. anago で最近気になったことを直してみようと調べてみました.

  • GUI でデバイスを動かすときにスレッドをまともに扱っていない → 適切な直し方は知っているが C++ でつくるのめんどい
  • flash programming で ROM image が 2 の n 乗ではないとかけない → ROM としてありえない数値なので解釈に困るので手動で補正するしかない
  • flash device ID を拾えないか → 後述
  • chip erase ではなく sector erase を使えないか → 後述

ROM dump はそんなに難しくないのですが flash programming で昔見てた AM29F040B, W29C040 などのデバイスを確認してます. flash の command 関連は当時のわたしが面倒そうだから実装しなかったと推測していたですが、デバイスによって仕様がかなりバラバラで実装が不可能なのが事実でした...

SPI ではなくパラレルの flash は +12V がいるのは除外して +5V だけのものでも西暦 2000年以前に設計された古いものがあり、コマンド体型が統一されていないので難しいです. 当時(2009年頃)、ファミカセに flash memory を載せる実験をしたきっかけはその5年前にバイト先で廃棄するだったパソコンのマザーボードBIOS 用に載っていた W29C020 を拝借したことから始まります*1.

実験に成功した後 AM29F040B が新品で手に入ることを知り、秋月電子でも EN29F002 と MX29F040 が売っていることを知り...そんな流れです.

flash device ID を拾えないか

1998年設計の W29C040 には command で flash device ID を得る方法がありません. device ID 取得には 12V が必要の上、アドレスバスを全ビットを適切に入力する必要があります.

sector erase を使えないか

AM29F040B と MX29F040C は sector size が 0x10000 bytes 均一です.

W29C040 には sector erase はありません.

EN29F002 は sector の並びが均一ではない上、上位ビットはファミカセ基板で楽に配線できる方をつなげと当時指示を書いたので入力したアドレスバスから sector を指定/把握する方法がありません.

SST39SF040 は sector size が 0x1000 bytes 均一のようです. データシートにあまり細かく書いてありません. このデバイスがなぜか一番供給が安定しているみたいなのですが、コマンドアドレス幅が広いとかで癖があります.

導ける答え

  • command を打たずに address: device ID の data を取得する
  • command を打って同じ address を取得して値が変わったら command から device ID を取得する
  • device ID を取得不能な場合は手動入力の上 chip erase をする
    • W29C040 は chip erase がいらない
  • device ID でも EN29F002 は chip erase
  • AM29F040B 系と SST39SF040 系は sector size を見ながら sector erase + program

こんなところでしょうか...?

*1:許可をもらって拝借しました

mruby - mpsse 経由で SPI Flash/ I2C EEPROM programmer を作るその2

mruby でエラーの行番号を出す方法を下記を参考にいれました. API ドキュメントは読んでないので本当にいいのか保証がありません.
https://tyfkda.github.io/blog/2013/09/21/mruby-backtrace.html

ソースコードは整備ができましたので公開いたします. プロジェクト名は ezp201x という怪しい programmer をもらったけどソフトを使いたくなかったのでケースだけを流用しようと思ったからです.
gitlab.com

ruby script を書いて感じたのはやはり Squirrel より便利なのでこちらを乗り換えようと思います. もう 10 年以上放置している anago もこれで作り直して、libusb を触るところだけ C, 基本的な処理はコンパイル済みの mruby, カートリッジハード個別のドライバは rb ファイル直接とすると 楽しくプログラムをかけそうです.

anago の場合はユーザー数がかなり多いので GUI をつけろと要求が来るのは面倒なんですよね... 自分は GUI 制作に向いてない*1し、wxWidgets 使うにしろ C++ を書くのが大変に苦痛(言語として難しすぎる)なので気軽にかければ本当にそれがいいのですけど.

これは個人的な信仰で、 C++ は一応書けるけど難しく手間が多いので書きたくない、 Python はインデントが慣れないので*2やろうと思ったことさえない、結果として Ruby がいいということです.

anago を作り直すことに GUI で協力してくれる人がいればコメントなりで連絡ください.

*1:twitter で upergrafx control panel が使いにくいと言われて気にしてます

*2:C では {} を絶対に省略しない人です