スクリプトファイルを解析する当たってどうしても欠かせなかった資料があります。それはmglvns (Leaf Visual Novel System for MGL2)と呼ばれるプログラムで、元々はhandheld PC(MGL2環境)で動作することを目的にしています。個人的に多くのソフトウェア設計方法を学ばせてもらい勉強させていただきました(^^;。ソースコードも公開されていますので、ぜひ一度読んでみてください。
スクリプト構文解析の処理部分を以下に表します。基本的にはデータ構造に沿って順番に実行しているだけです。ScriptParserScn関数はイベントデータ、ScriptParserTxt関数はメッセージデータを担当しています。このときはスクリプトの全体像を把握したかった為、動作処理については省略して作りました。
//--------------------------------------------------------------------------- //「雫」シナリオパーサ本体 #define c s->pScnCur EWRAM_CODE void ScriptParserScn() { ST_SCRIPT* s = &Script; for(;;) { switch(c[0]) { case 0x00: TRACEOUT("[ブロック終了]\n"); c++; return; case 0x01: //特殊効果 switch(c[1]) { case 0x01: TRACEOUT("[ぐにゃり→暗]-[メッセージ: %d]\n", c[2]); ScriptParserTxt(c[2], TRUE); ScreenFontDrawCls(); c += 3; break; case 0x02: TRACEOUT("[暗→ぐにゃり]-[メッセージ: %d]\n", c[2]); ScriptParserTxt(c[2], TRUE); ScreenFontDrawCls(); c += 3; break; case 0x03: TRACEOUT("[涙の雫: %02x]\n", c[2]); c += 3; break; case 0x04: TRACEOUT("[ぐにゃり2(異次元)]-[メッセージ:%d]\n", c[2]); ScriptParserTxt(c[2], TRUE); ScreenFontDrawCls(); c += 3; break; default: TRACEOUT("異常な0x01コマンドです(%02x,%02x)\n", c[1], c[2]); return; } break; case 0x03: TRACEOUT("[謎: %02x]\n", c[0]); c += 2; break; case 0x04: TRACEOUT("[ジャンプ: SCN%03d.dat Block %d]\n", c[1], c[2]); ScriptLoadScenario(c[1], c[2]); break; case 0x05: TRACEOUT("[選択肢: %d][メッセージ: %d]\n", c[2], c[1]); c = ScriptWaitSelect(c); break; case 0x06: TRACEOUT("[謎: %02x]\n", c[0]); c++; break; case 0x07: TRACEOUT("[前の選択肢に戻るマーク位置]\n"); c++; break; case 0x0a: TRACEOUT("[背景ロード: MAX_S%02d.img]\n", c[1]); c += 2; break; case 0x14: TRACEOUT("[画面クリア? %02d]\n", c[1]); c += 2; break; case 0x16: TRACEOUT("[Hシーンロード: MAX_H%02d.img]\n", c[1]); c += 2; break; case 0x22: TRACEOUT("[キャラクタロード: MAX_C%02x.LFG %02x]\n", c[1], c[2]); c += 3; break; case 0x24: TRACEOUT("[キャラクタロード2?(center?): MAX_C%02x.LFG]\n", c[1]); c += 3; break; case 0x28: TRACEOUT("[選択肢の前に存在するデータ]\n"); c++; break; case 0x38: TRACEOUT("[表\示処理: %02x]\n", c[1]); c += 2; break; case 0x3d: TRACEOUT("[if文 flg:%02x == 0x%02x pc += %02x]\n", c[1], c[2], c[3]); if( SizukuGetSysFlag(c[1]) == c[2] ) { c += c[3]; } c += 4; break; case 0x3e: TRACEOUT("[if文(否定) flg:%02x != 0x%02x pc += %02x]\n", c[1], c[2], c[3]); if( SizukuGetSysFlag(c[1]) != c[2] ) { c += c[3]; } c += 4; break; case 0x47: TRACEOUT("[フラグの値設定: %02x = 0x%02x]\n", c[1], c[2]); SizukuSetSysFlag(c[1], c[2]); c += 3; break; case 0x48: TRACEOUT("[フラグ加算: %02x += 0x%02x\n", c[1], c[2]); SizukuAddSysFlag(c[1], c[2]); c += 3; break; case 0x54: TRACEOUT("[テキストメッセージ: %d]\n", c[1]); ScriptParserTxt(c[1], TRUE); ScreenFontDrawCls(); c += 2; break; case 0x5a: TRACEOUT("[謎: %02x]\n", c[0]); c++; break; case 0x5c: TRACEOUT("[謎: %02x %02x]\n", c[0], c[1]); c += 2; break; case 0x61: TRACEOUT("[謎: %02x %02x %02x]\n", c[0], c[1], c[2]); c += 3; break; case 0x62: TRACEOUT("[謎:%02x(%02x)]\n", c[0], c[1]); c += 2; break; case 0x60: case 0x63: case 0x64: case 0x65: case 0x66: TRACEOUT("[謎: %02x]\n", c[0]); c++; break; case 0x6e: TRACEOUT("[BGM再生: %02x]\n", c[1]); ScriptMusicStart(SizukuGetBgmNo(c[1]), TRUE); c += 2; break; case 0x6f: case 0x73: TRACEOUT("[謎: %02x]\n", c[0]); break; case 0x7e: TRACEOUT("[エンディング番号設定: %02x]\n"); /* 0 12 卒業式 1 12 瑞穂 BAD 2 12 破壊 3 12 トースター 4 11 佐織 HAPPY 5 12 佐織 BAD 6 11 瑞穂 HAPPY 7 12 瑞穂 BAD 8 10 True 9 11 瑠璃子 HAPPY a 01 大田さん b 14 異次元 c 12 異次元 BAD */ //瑠璃子 HAPPY を見ているかチェックをします if( SizukuGetSysFlag(0x46) == 1) { SizukuSetSysFlag(0, 3); } else { if( SizukuGetSysFlag(0) == 0 ) { SizukuSetSysFlag(0, 2); } else { SizukuSetSysFlag(0, 1); } } c += 2; break; case 0x7c: TRACEOUT("[エンディング関係 謎:%02x()]\n", c[0]); c++; break; case 0x7d: TRACEOUT("[エンディングBGM設定 & 起動:%02x %d]\n", c[0], c[1]); c += 2; break; case 0xff: TRACEOUT("[本来アクセスし得ない場所にアクセスした %02x]\n", c[0]); for(;;){} break; default: TRACEOUT("[ScriptParserScn unknown = %02x][no: %02x]\n", c[0], s->scnNo); for(;;){} break; } // switch(c[0]) } // for(;;) } #undef c //--------------------------------------------------------------------------- //「雫」テキストパーサ本体 #define c s->pTxtCur EWRAM_CODE void ScriptParserTxt(u8 no, bool isHistoryAdd) { ST_SCRIPT* s = &Script; u8 c1, c2, c3, c4; //シナリオデータのロード c = s->pTxt + GET_SHORT(s->pTxt + (no+1) * 2); //履歴に登録するかチェックをします if(isHistoryAdd == TRUE) { HistoryAdd(c); } for(;;) { //文字の表示(リーフフォントコード) if(c[0] & 0x80 || c[0] == 'r') { c = ScriptDrawCode(c); continue; } switch(c[0]) { case '$': TRACEOUT("[メッセージ終了]\n"); c++; return; #if 0 //※ 前処理に移動しました。 case 'r': TRACEOUT("[改行]\n"); c++; break; #endif case 'p': TRACEOUT("[ページ更新待ち]\n"); ScriptWaitPage(); c++; break; case 'k': case 'K': TRACEOUT("[キー入力待ち]\n"); ScriptWaitKey(); c++; break; case '0': TRACEOUT("[謎: %02x]\n", c[0]); c++; break; case 'C': TRACEOUT("[キャラクタ交換: %c, MAX_C%c%c]\n", c[1], c[2], c[3]); c += 4; break; case 'B': TRACEOUT("[背景ロード: %c%c, %d, %d]\n", c[1], c[2], Dig(c[3],c[4]), Dig(c[5],c[6]) ); c += 7; break; case 'S': TRACEOUT("[背景付きキャラ表\示: %c, MAX_C%c%c, MAX_S%c%c, %d, %d]\n", c[1], c[2],c[ 3],c[4],c[5], Dig(c[6],c[7]), Dig(c[8], c[9])); c += 10; break; case 'D': TRACEOUT("[キャラ全消去後表\示: %c, MAX_C%c%c]\n", c[1], c[2], c[3]); c += 4; break; case 'A': case 'a': TRACEOUT("[キャラ3人表\示: %c, %c%c, %c, %c%c, %c, %c%c]\n", c[1],c[2],c[3], c[4], c[5],c[6], c[7],c[8],c[9]); c += 10; break; case 'Q': TRACEOUT("[画面を揺らす: %02x]\n", c[0]); c++; break; case 'E': TRACEOUT("[背景ロード(2)?: MAX_S%c%c.LFG, %d, %d]\n", c[1], c[2], Dig(c[3], c[4]), Dig(c[5], c[6])); c += 7; break; case 'F': TRACEOUT("[フラッシュ: %c]\n", c[0]); c++; break; case 'V': TRACEOUT("[ビジュアル: VIS%c%c, %d, %d]\n", c[1], c[2], Dig(c[3], c[4]), Dig(c[5], c[6])); c += 7; break; case 'H': TRACEOUT("[Hシーン(HVS%c%c, %d, %d)]\n", c[1], c[2], Dig(c[3], c[4]), Dig(c[5], c[6 ])); c += 7; break; case 'M': //BGM 関連 c1 = c[1]; c += 2; if(c1 == 'f') { TRACEOUT("[BGM フェードアウト]\n"); } else if (c1 == 'n') { c2 = *c++; c3 = *c++; TRACEOUT("[BGM 再生(next): M_%c%c\n", c2, c3); } else if (c1 == 'w') { TRACEOUT("[BGM FADE WAIT]\n"); } else if (c1 >= '0' && c1 <= '2') { c2 = *c++; TRACEOUT("[BGM 再生: M_%c%c]\n", c1, c2); } else if (c1 == 's') { TRACEOUT("[BGM 停止]\n"); } else { TRACEOUT("[cmd:4d][cmd:%x]\n", c1); } break; case 'P': //PCM 関連 c1 = c[1]; c += 2; if(c1 == 'l') { c2 = *c++; c3 = *c++; TRACEOUT("[PCMロード(%c%c)]\n", c2, c3); } else if (c1 >= '0' && c1 <= '9') { c2 = *c++; c3 = *c++; c4 = *c++; TRACEOUT("[PCM再生指定(%c%c, %c%c)\n]", c1, c2, c3, c4); } else if(c1 == 'f') { TRACEOUT("[PCMフェードアウト?]\n"); } else if(c1 == 'w') { TRACEOUT("[PCM wait?]\n"); } else if(c1 == 's') { TRACEOUT("[PCM停止]\n"); } else { TRACEOUT("[cmd:50][cmd:%x]\n"); } break; case 'X': TRACEOUT("[表\示オフセット指定: %x]\n", c[1]); c += 2; break; case 's': TRACEOUT("[表\示速度指定?: %x]\n", c[1]); c += 2; break; default: TRACEOUT("[ScriptParserTxt unknown = %02x][no: %02x]\n", c[0], s->scnNo); for(;;){} break; } // switch(c[0]) } // for(;;) } #undef c T
プログラムの暴走を避ける為、たくさんのデバッグログを用意しておいて外枠をしっかり固めておきます。そうしておけば不具合が出たときに追跡しやすくなります。余計な処理で動きが鈍くなると思われますが、以下のようにリリース時にdefine定義を変えてあげれば問題はないです。デバッグについて知りたい場合はDoc.10 エミュレータのデバッグコンソールを参照してください。
//デバック #define TRACEOUT _Printf //リリース #define TRACEOUT(...)
文字を画面に表示するとき、文字のコードはEUCやShift-JISではなく、リーフフォントコードと呼ばれる順番で格納されています。具体的には以下のとおりです。
コード | 文字 |
0x0000 | |
0x0001 | ■ |
0x0002 | あ |
0x0003 | い |
0x0004 | う |
.... | .. |
コードは全部で1853個あります。順番はEUCやShift-JISのインデックスと完全に一致しない為、変換テーブルを用意しなくてはなりません。ただし「リーフフォントコード→変換テーブル→文字表示の関数(Shift-JIS)」という処理の流れでは、1文字毎に負荷がかかりすぎます。そこでフォントデータ自体をコードの順番に並べてしまい、「リーフフォントコード→文字表示の関数(Leaf)」という作りにしました。詳しいことは旧サイトのNo.78を参照してください。
#ref(): File not found: "clip_1.png" at page "Ex.22"
それぞれのプロトタイプは機能別に作られた為、お互いが干渉することはありません。音を鳴らす、画面を表示する関数等は、まるでC言語の標準関数のように機能を提供することができます。以下にNO.79のソースコードを表します。
IWRAM_CODE void AdStart(u8* pData, u32 len, bool isLoop); IWRAM_CODE void AdReStart(); IWRAM_CODE void AdStop();
EWRAM_CODE void FontLeafDrawChr(u16 x, u16 y, u16 code); EWRAM_CODE void ScreenImgClear(); EWRAM_CODE void ScreenImgUpdate(); EWRAM_CODE void ScreenImgLoadBg(u8* pBg); EWRAM_CODE void ScreenImgLoadChr(u8* pChr, u8 pos); EWRAM_CODE void ScreenImgFlash(); EWRAM_CODE void ScreenImgFadeIn(u16 mode, u16 min, u16 max, u16 wait); EWRAM_CODE void ScreenImgFadeOut(u16 wait); EWRAM_CODE void ScreenCursorDrawBlink(); EWRAM_CODE void ScreenCursorMoveIn(); EWRAM_CODE void ScreenCursorMoveOut();
機能を提供してほしいのはスクリプトを実行しているScriptParserScn、ScriptParserTxt関数です。NO.78までは骨組みだけ整えていましたが、先の関数をうまく利用して中身を作っていきます。
case 'F': TRACEOUT("[フラッシュ: %c]\n", c[0]); ScreenFontOut(); ScreenImgFlash(); ScreenFontIn(); c++; break; (中略...) case 'S': TRACEOUT("[背景付きキャラ表\示: %c, MAX_C%c%c, MAX_S%c%c, %d, %d]\n", c[1], c[2],c[3],c[4],c[5], Dig(c[6],c[7]), Dig(c[8], c[9])); ScreenFontOut(); ScriptClear(); ScriptLoadBg(Dig(c[4],c[5])); ScriptLoadChr(HexToDig2(c[2], c[3]), c[1]); ScriptUpdate(); ScreenFontIn(); c += 10; break;