VitaでZAVASやりたくて(8)
ようやく五合目くらいには来ただろうか。
今回はついにエミュレータの改造。
さすがにソースコードを残してたので今回はわりとすんなり進みます。
おさらい
前回はZAVASにどんなプロテクトがかかってるか調査してみて、結局、実機でフロッピーがランダムに読める箇所がエミュレータだとランダムに読めないのがネックなのだと確認できたのでした。
実際に読み出してみてランダムになるものがあるのならそれも再現してこそエミュレータ。
ということで改造するぞ。
Windows版QUASI88をいじってみる
Vita版のPC88エミュレータがQUASI88なので(というかそれしか使ったことがないので)、手始めにWindows版で改造を試みて、それがうまくいったらVita版に移植することにします。
前回までの検証でWindows版QUASI88は既に手元にあるので改造ポイントを探ってちょちょいと改造してしまいましょう。
大まかな方針
ランダムに読める箇所はフロッピーごとに決まっているはずなので、d88ファイルのどの範囲がランダムになっているか指定する設定ファイルみたいなものを用意するのがよさげです。
iniファイルみたいなもので一元管理してもいいけどd88ファイルと1対1のファイルにした方が扱いもしやすそう。(CDで言うcueファイルみたいに)
で、d88ファイルを読み込んだ際その設定ファイルがあれば勝手に読み込むようにしておけば、通常の使用と違和感なく使えます。
というわけで出来そうかどうか確認していきます。
改造する個所を探す
どこかにd88ファイルを読み込む処理があるはずなのでまずはそれを探してみます。
d88でgrepしてみるとd88_read_headerという関数をdisk_insertから呼び出しているのが見つかります。いかにもそれっぽい。
switch( d88_read_header( drive[ drv ].fp, offset, c ) ){
fpというのはファイルポインタだろうか。少し手前も見てみるとこんなのが。
drive[ drv ].fp = osd_fopen( FTYPE_DISK, filename, "r+b" );
とにかく何やらオープンしている様子。
filenameに何が渡されているのか見てみたいのでdisk_insertの呼び出し元を遡ってみると、メニューでファイルを(d88とかを)開く処理からfilenameが渡されているのがわかります。
どうやらここでd88ファイルを開いているのはまず間違いなさそう。
あらためてdisk_insertを眺めてみると、d88_read_headerでヘッダを読んでdrive[ drv ]という構造体に色々と情報を格納している様子です。
ただし読み込みはそれだけで終わり。ここで全部読むのかと思ったら違った。
となるとフロッピーの中身自体は別で読んでいるはずなので、さらに処理を追ってみます。
すると「.fp」の参照箇所に怪しげなのが。
if( osd_fseek( drive[ drv ].fp,
drive[ drv ].sec_pos + SZ_DISK_ID, SEEK_SET )==0 ){
if( osd_fread( &data_buf[ptr], sizeof(Uchar),
size, drive[ drv ].fp )==(size_t)size ){
fdc_read_dataという如何にもそれっぽい関数の中である程度のサイズ分読み込んでいる。他にも読み込んでる箇所がいくつかありますが他は固定サイズでヘッダとかを読んでるだけです。本丸は恐らくここでビンゴ。
処理内容からは普通にシークして普通に読んでいるように見えますが、ちょっとはっきりしないのがdrive[ drv ].sec_pos。
これはいったい何なのだろうか。
値を設定している箇所を辿ってみるとこういう流れになっています。
最初:drive[ drv ].sec_pos = drive[ drv ].disk_top + track_top;
↓
更新:drive[ drv ].sec_pos += (sec_buf.size + SZ_DISK_ID);
disk_topはフロッピーの先頭(2枚結合してたりするのでそれぞれの先頭)、tack_topはD88形式フォーマットの各トラック情報を読み込んだ値です。(D88形式フォーマットは下記を参考)
最初はトラックの先頭、必要に応じて、セクタサイズ+セクタヘッダだけ増加しています。
セクタの中身にヘッダがくっついてセクタ1つを表している点に注意すると、これはつまり、sec_posがd88ファイル上の各セクタ情報の先頭を指している、という事になります。
あらためてfdc_read_dataに戻って何をしているのか考えてみると、要は、ごく普通に指定セクタの位置をd88ファイルからサーチしてセクタヘッダの後ろ=セクタの中身を読んでる、ということになるわけです。
ようやく全体が見えてきました。
d88ファイルを開くとdisk_insertが呼ばれてヘッダだけ読む
fdc_read_dataが呼ばれるたびに指定されたセクタの位置をd88ファイルからサーチして読む
ランダム機能もこれに倣って、
disk_insertのタイミングでランダム範囲の設定ファイルを読んでおく
fdc_read_dataで読み込みが終わったらランダム範囲に該当するか検索して必要に応じてランダムにする
これでいけそう!いざ実装!
実装してみる
飽きてきたのでここは結果だけ。
以下の2ファイルを変更します。
(「MOD BEGIN ~ MOD END」で挟んだところが変更箇所)
なお、いちおう動いてるけど保証は一切出来ませんのであしからず。
drive.h
先頭当たりのPC88_DRIVE_Tをいじる。
/* ---------- MOD BEGIN ---------- */
#define RND_RANGE_MAX 16
typedef struct {
long top;
long end;
} RANGE_T;
/* ---------- MOD END ---------- */
/**** ドライブ制御ワーク ****/
typedef struct{
OSD_FILE *fp; /* FILE ポインタ */
char read_only; /* リードオンリーでファイルを開いたら真 */
char over_image; /* イメージ数が多過ぎる時に真 */
char detect_broken_image; /* 壊れたイメージが見つかったら真 */
char empty; /* ドライブに空を指定しているなら真 */
int selected_image; /* 選択しているイメージ番号 (0~) */
int image_nr; /* ファイル内のイメージ数 (1~) */
struct{ /* ファイル内の全イメージの情報 */
char name[17]; /* イメージ名 */
char protect; /* プロテクト */
char type; /* ディスクタイプ */
long size; /* サイズ */
}image[ MAX_NR_IMAGE ];
/* ここから、選択中イメージのワーク */
int track; /* 現在のトラック番号 */
int sec_nr; /* トラック内のセクタ数 */
int sec; /* 現在のセクタ番号 */
long sec_pos; /* セクタ の現在位置 */
long track_top; /* トラックの先頭位置 */
long disk_top; /* ディスクの先頭位置 */
long disk_end; /* イメージの終端位置 */
char protect; /* ライトプロテクト */
char type; /* ディスクタイプ */
/* ファイル名 */
/* char filename[ QUASI88_MAX_FILENAME ];*/
/* ---------- MOD BEGIN ---------- */
// ランダム範囲
RANGE_T rnd_range[RND_RANGE_MAX];
/* ---------- MOD END ---------- */
} PC88_DRIVE_T;
fdc.c
①disk_insertの最後で新規関数load_rnd_fileを呼ぶ。
②fdc_read_dataのデータ読み込み後にランダム加工を追加。
/* ---------- MOD BEGIN ---------- */
//***********************************************************************
// <機能>
// rndファイルを読み込む
//
// <パラメータ>
// drv ... ドライブ
// filename ... d88ファイル名
//
// <戻り値>
// 0 ... 成功
// 1 ... 失敗
//
// <rndファイル名について>
// 「d88ファイル名.rnd」で決め打ち。つまり「filename.d88.rnd」みたいになります。
//
// <書式>
// ランダムに読める範囲を16進表記で「XXXX(開始) - YYYY(終了)」の形で指定する。
// 最大15個まで指定可能。(RND_RANGE_MAX-1個)
// 単純にファイル先頭からのオフセット値を指定するだけなので(つまりバイナリエディタ等で読めるアドレスそのまま)、
// d88ファイルの構造とかは知らなくてOK。
// 範囲指定なので「YYYY(終了)の値を含む」点に注意すること。
// 上記の書式以外は無視するので適当に何か書けば事実上コメント代わりにもなるよ。
// 余談だが、シードすら与えずにrand()で上書きするだけなのでひどくなんちゃってランダムである。
// シード与えても意味がない気がするしデバッグ時に再現性ある方が便利かと思ってね。敢えてね。
//
// <例>
// # 1st range
// 0002C0 - 0003BF
//
// # 2nd range
// 008000 - 0081FF
//***********************************************************************
int load_rnd_file(int drv, const char* filename) {
char rnd_filename[QUASI88_MAX_FILENAME];
FILE* fp;
char buf[256];
int n;
long top, end;
sprintf(rnd_filename, "%s.rnd", filename);
fp = fopen(rnd_filename, "r");
if (fp == NULL) {
return 1;
}
n = 0;
// 読み込む
while (fgets(buf, sizeof(buf), fp)) {
// 「XXXX(開始) - YYYY(終了)」の形で読めたものだけ処理
if (sscanf(buf, "%lx - %lx", &top, &end) == 2) {
drive[drv].rnd_range[n].top = top;
drive[drv].rnd_range[n].end = end;
n++;
if (n == (RND_RANGE_MAX - 1)) break;
}
}
drive[drv].rnd_range[n].top = 0;
drive[drv].rnd_range[n].end = 0;
fclose(fp);
return 0;
}
/* ---------- MOD END ---------- */
int disk_insert( int drv, const char *filename, int img, int readonly )
{
int exit_flag;
Uchar c[32];
long offset;
int num;
int open_as_readonly = readonly;
/* 現在のディスクはイジェクト */
disk_eject( drv );
/* ファイル名を覚えておく */
/*
if( strlen(filename) >= QUASI88_MAX_FILENAME ){
DISK_ERROR( "filename too long", drv );
return 1;
}
strcpy( drive[ drv ].filename, filename );
*/
/* "r+b" でファイルを開く。だめなら "rb" でファイルを開く */
if( open_as_readonly == FALSE ){
drive[ drv ].fp = osd_fopen( FTYPE_DISK, filename, "r+b" );
}
if( drive[ drv ].fp == NULL ){
drive[ drv ].fp = osd_fopen( FTYPE_DISK, filename, "rb" );
open_as_readonly = TRUE;
}
if( drive[ drv ].fp == NULL ){
DISK_ERROR( "Open failed", drv );
return 1;
}
/* 反対側のドライブのファイルと同じファイルだった場合は */
/* 反対側ドライブのワークをコピー */
/* そうでない場合は ドライブのワークを初期化 */
if( drive[ drv ].fp == drive[ drv^1 ].fp ){ /* 反対ドライブと同じ場合 */
/* drive[ drv ].file_size = drive[ drv^1 ].file_size;*/
drive[ drv ].read_only = drive[ drv^1 ].read_only;
drive[ drv ].over_image = drive[ drv^1 ].over_image;
drive[ drv ].detect_broken_image = drive[ drv^1 ].detect_broken_image;
drive[ drv ].image_nr = drive[ drv^1 ].image_nr;
memcpy( &drive[ drv ].image,
&drive[ drv^1 ].image, sizeof(drive[ drv ].image) );
if( drv==0 ){
DISK_WARNING( " (( %s : Set in drive %d: <- 2: ))\n", filename, drv+1 );
}else{
DISK_WARNING( " (( %s : Set in drive 1: -> %d: ))\n", filename, drv+1 );
}
}else{ /* 反対ドライブと違う場合 */
if( open_as_readonly ){
drive[ drv ].read_only = TRUE;
DISK_WARNING( " (( %s : Set in drive %d: as read only ))\n",
filename, drv+1 );
}else{
drive[ drv ].read_only = FALSE;
DISK_WARNING( " (( %s : Set in drive %d: ))\n", filename, drv+1 );
}
/*
drive[ drv ].file_size = osd_file_size( drive[ drv ].filename );
if( drive[ drv ].file_size == -1 ){
DISK_ERROR( "Access Error(size)", drv );
return 1;
}
*/
drive[ drv ].over_image = FALSE;
drive[ drv ].detect_broken_image = FALSE;
num = 0; offset = 0;
exit_flag = FALSE;
/* 各イメージのヘッダ情報を全て取得 */
while( !exit_flag ){
switch( d88_read_header( drive[ drv ].fp, offset, c ) ){
case D88_SUCCESS: /* イメージ情報取得成功 */
memcpy( &drive[ drv ].image[ num ].name[0], &c[0], 16 );
drive[ drv ].image[ num ].name[16] = '\0';
drive[ drv ].image[ num ].protect = c[DISK_PROTECT];
drive[ drv ].image[ num ].type = c[DISK_TYPE];
drive[ drv ].image[ num ].size = READ_SIZE_IN_HEADER( c );
/* 次のイメージの情報取得へ */
offset += drive[ drv ].image[ num ].size;
num ++;
if( num >= MAX_NR_IMAGE ){ /* イメージ数が多い時は中断 */
DISK_WARNING( " (( %s : Too many images [>=%d] ))\n",
filename, MAX_NR_IMAGE );
drive[ drv ].over_image = TRUE;
exit_flag = TRUE;
}
else if( offset < 0 ){ /* イメージサイズが大きすぎ? */
DISK_WARNING( " (( %s : Too big image? [%d] ))\n", filename, num+1 );
drive[ drv ].detect_broken_image = TRUE;
exit_flag = TRUE;
}
break;
case D88_NO_IMAGE: /* これ以上イメージがない */
exit_flag = TRUE;
break;
case D88_BAD_IMAGE: /* このイメージは壊れている */
DISK_WARNING( " (( %s : Image No. %d Broken? ))\n", filename, num+1 );
drive[ drv ].detect_broken_image = TRUE;
exit_flag = TRUE;
break;
default: /* ??? */
DISK_WARNING( " (( %s : Image No. %d Error? ))\n", filename, num+1 );
drive[ drv ].detect_broken_image = TRUE;
exit_flag = TRUE;
break;
}
}
if( num==0 ){
DISK_ERROR( "Image not found", drv );
return 1;
}
drive[ drv ].image_nr = num;
}
/* disk_top をimg 枚目のディスクイメージの先頭に設定 */
if( img < 0 || img >= drive[ drv ].image_nr ){
DISK_WARNING( " (( %s : Image No. %d Not exist ))\n", filename, img+1 );
drive_set_empty( drv );
}else{
disk_change_image( drv, img );
}
/* ---------- MOD BEGIN ---------- */
// rndファイルを読む
// 毎回読み直しで若干効率は悪いが気にしない気にしない
load_rnd_file(drv, filename);
/* ---------- MOD END ---------- */
return 0;
}
/*===========================================================================
* READ系コマンドで、DATAをバッファに読み込む
* 結果は ST0~ST2 とバッファセットされる
* (READ ID の場合、この関数は呼ばないこと)
*===========================================================================*/
static int fdc_read_data( void )
{
int drv = (fdc.us);
int read_size, size, ptr, error;
print_fdc_status(( (fdc.command==READ_DIAGNOSTIC) ? BP_DIAG : BP_READ ),
drv, drive[drv].track, drive[drv].sec);
/* STATUS を再設定 (READ(DELETED)DATAの場合は DATA CRCエラーのみ) */
if( sec_buf.status == STATUS_DE ){ /* ID CRC err */
fdc.st0 = ST0_IC_AT | (fdc.hd<<2) | fdc.us;
fdc.st1 = ST1_DE;
fdc.st2 = ( (sec_buf.deleted==DISK_DELETED_TRUE)? ST2_CM: 0 );
}
else
if( sec_buf.status == STATUS_DE_DD ){ /* DATA CRC err */
fdc.st0 = ST0_IC_AT | (fdc.hd<<2) | fdc.us;
fdc.st1 = ST1_DE;
fdc.st2 = ST2_DD | ( (sec_buf.deleted==DISK_DELETED_TRUE)? ST2_CM: 0 );
}
else{ /* CRC OK */
fdc.st0 = ST0_IC_NT | (fdc.hd<<2) | fdc.us;
fdc.st1 = 0;
fdc.st2 = ( (sec_buf.deleted==DISK_DELETED_TRUE)? ST2_CM: 0 );
}
if( ! idr_match() ){ /* IDR不一致 */
fdc.st0 |= ST0_IC_AT;
fdc.st1 |= ST1_ND;
}
/* DATA 部分を読む */
read_size = 128 << (fdc.n & 7); /* 読み込みサイズ */
ptr = 0; /* 書き込み位置 */
if( fdc.command == READ_DIAGNOSTIC ){
if((sec_buf.size==0x80 && read_size!=0x80) || /* セクタの Nと */
(sec_buf.size & 0xff00) != read_size ){ /* IDRの Nが違う時は */
fdc.st0 |= ST0_IC_AT; /* DATA CRC err */
fdc.st1 |= ST1_DE;
fdc.st2 |= ST2_DD;
}
}
while( read_size > 0 ){ /* -------------------指定サイズ分読み続ける */
size = MIN( read_size, sec_buf.size );
if( osd_fseek( drive[ drv ].fp,
drive[ drv ].sec_pos + SZ_DISK_ID, SEEK_SET )==0 ){
if( osd_fread( &data_buf[ptr], sizeof(Uchar),
size, drive[ drv ].fp )==(size_t)size ){
/* ---------- MOD BEGIN ---------- */
// 読めた範囲にランダム範囲があればランダム加工する
// 関数に切り出した方が綺麗ではあるのだがパラメータが多くてかえって煩雑なのでこれで妥協
RANGE_T fread_range;
fread_range.top = drive[drv].sec_pos + SZ_DISK_ID;
fread_range.end = fread_range.top + size - 1;
// ランダム範囲の一覧からその範囲内に読めた位置があるか見ていく
for (int n = 0; drive[drv].rnd_range[n].top != 0; n++) {
// 読めた範囲とランダム範囲の位置関係には以下のパターンがある。
// 要は手前か後ろかを判断すればよい。
//
// |rnd_range|
// ①|fread_range|...| | -> 範囲外
// ② |fread_range| |
// ③ |fread_range|
// ④ | |fread_range|
// ⑤ | |...|fread_range| -> 範囲外
//
// ③’ ||frd_rng|| ※読めた範囲が狭い場合③はこのパターン
//
if (fread_range.end < drive[drv].rnd_range[n].top) continue;
if (drive[drv].rnd_range[n].end < fread_range.top) continue;
// 範囲内だーッ!
logfdc("[READ:%08lX - %08lX] [RND:%08lX - %08lX] ",
fread_range.top, fread_range.end, drive[drv].rnd_range[n].top, drive[drv].rnd_range[n].end);
// ランダム範囲を総舐めして読めた範囲の該当位置をランダム加工していく
// ループ条件を細かく決めると無駄なく処理することも出来るが割とめんどくさいのでシンプルさを優先した
for (int offset = drive[drv].rnd_range[n].top; offset <= drive[drv].rnd_range[n].end; offset++) {
if ((fread_range.top <= offset) && (offset <= fread_range.end)) {
// 「ptr + 0」の位置とfread.range.topが対応している
// 従ってfread.range.topとの差分を加えたものが書き込み位置になる
data_buf[ptr + (offset - fread_range.top)] = rand();
// ※もっと簡単に計算出来そうな気がするけど思いつかなかった。
// fread_range側でループを回せば若干の効率悪化と引き換えに少しだけ意味が分かりやすくなるかなあ。
}
}
}
/* ---------- MOD END ---------- */
error = 0;
}
else error = 1;
} else error = 2;
if( error ){ /* OSレベルのエラー発生 */
printf_system_error( error ); /* DATA CRC err にする*/
status_message( 1, STATUS_WARN_TIME, "DiskI/O Read Error" );
fdc.st0 |= ST0_IC_AT;
fdc.st1 |= ST1_DE;
fdc.st2 |= ST2_DD;
break;
}
ptr += size;
read_size -= size;
if( read_size <= 0 ) break;
fdc.st0 |= ST0_IC_AT; /* 次のセクタに跨った */
fdc.st1 |= ST1_DE; /* DATA CRC err にする */
fdc.st2 |= ST2_DD;
/* セクタ間を埋める (DATA-CRC,GAP3,ID-SYNC,IAM,ID,ID-CRC,GAP2など) */
#if 0 /* セクタ間のデータ作成なし */
/* CRC GAP3 SYNC AM ID CRC GAP2 */
if( fdc.mf ) size = 2 + 0x36 + 12 + (3+1) + 4 + 2 + 22;
else size = 2 + 0x2a + 6 + (1+1) + 4 + 2 + 11;
ptr += size;
read_size -= size;
disk_next_sec( drv );
#else /* peach氏より、セクタ間データ生成処理が提供されました */
size = fill_sector_gap(ptr, drv, fdc.mf);
if (size < -1) goto FDC_READ_DATA_RETURN;
ptr += size;
read_size -= size;
#endif
} /* ----------------------------------------- */
/* 読み込み終わったら、次セクタへ進めておく */
disk_next_sec( drv );
FDC_READ_DATA_RETURN:
/* READ DIAGNOSTIC の場合、CRC err と IDR不一致は正常としてみる */
/* (ST1, ST2のビットは、そのまま残す) */
if( fdc.command == READ_DIAGNOSTIC ){
fdc.st0 &= ~ST0_IC; /* == ST0_IC_NT */
}
return 1;
}
ビルドして動かす
ビルドは普通にビルドすればOK。
ランダム範囲の指定は「d88ファイル名.rnd」というファイルで行います。
ランダムに読める範囲を16進表記で「XXXX(開始) - YYYY(終了)」の形で指定してください。詳細はソース内のコメント参照。
僕の場合はプログラムディスクを「ZAVAS PROGRAM DISK DRIVE 1.d88」というファイル名で吸い出しているので(長い?フロッピーにそう書いてあったからね!)、「ZAVAS PROGRAM DISK DRIVE 1.d88.rnd」という設定ファイルを用意してランダム範囲を指定し…
いざZAVAS発進!
オープニングでエンターキーを華麗にたんたかたーん!
……(待つ)……


うおおおおおお!こいつうごくぞ!
次回予告
ついにエミュレータを超絶パワーアップさせることに成功したのでいよいよ次はVitaで動かします。
時は来たそれだけだ!
しかしまだ全然来てなかったとはこの時まだ知る由もなかったのである…
次回はVitaで動かしてみる冒険の旅。