1 文字ずつ読んでいって構文解析していく、有限オートマトンの話。大学 1 年のときにやったはず。
……と思っていたが、Node.js に実行させてsp
などを TSV などで出力することでも代用できそう。(終)
Node.js をインストールして、
console.log("hello, world.");
とだけ記述した first.js というテキストファイルを作り、node first.js
で実行したとき、コンソールに「hello, world.」と表示されたらインストール成功。
javascript部分の抽出
HTML ファイルは、
<html><head><META http-equiv="Content-Type" content="text/html; charset=shift_jis"> <title>"EPIC TRANCE" ABSOLUTE</title> <script type="text/javascript" src="../bms2jsh.js"></script> </head><body><script type="text/javascript"><!-- genre="EPIC TRANCE";title="ABSOLUTE"; // (中略) dp=[ /* 中略 */ ,"#ODAAY","80"];} } hd();w("<table><tr>");for(x=0;x<measure;x+=4){w("<td valign=bottom>"); if(x==36){b(36,37);x-=2;}else b(x,x+3);w("<\/td>");}ft(); //--></script></body></html>
のような記述をしている。
譜面データ定義部
そのため、主となる譜面データ定義部は<!--
から-->
で囲まれた箇所として、hd();
呼び出し以降を読み捨てれば良さそう。この javascript 部分を「譜面データ定義部」と呼ぶ。
変数初期化部
「譜面データ定義部」より前に、bms2jsh.js ファイルの変数初期化部分である
ln=[],sp=[],dp=[],tc=[],c1=[],c2=[],cn=[]; genre=title=artist=bpm=opt=lnse=lnhs="",key=ky=back=7,hs=gap=ty=k=1;cncnt=bsscnt=legacy=prt=pty=0; soflan=level=notes=measure=a=l=m=g=db=p1o=hps=flp=off=lnln=lnst=lned=alls=hids=sran=kuro=sftkey=os=hcn=ttl=0;
を挿入する。この javascript を「変数初期化部」と呼ぶ。
譜面種別の分岐
「変数初期化部」と「譜面データ定義部」の間に、どの譜面種別を読み込みたいかを決定する。DP 譜面に限れば、l
, hps
, a
, kuro
の 4 変数に下記表のような0
, 1
の値が設定されていればよい。
- | l | hps | a | kuro |
NORMAL | 1 | 1 | 0 | 0 |
HYPER | 0 | 1 | 0 | 0 |
ANOTHER | 0 | 0 | 1 | 0 |
LEGGENDARIA | 0 | 0 | 1 | 1 |
const SCORE_FLAGS = {'N': [1, 1, 0, 0], 'H': [0, 1, 0, 0], 'A': [0, 0, 1, 0], 'L': [0, 0, 1, 1]}; for (const SCORE_TYPE in SCORE_FLAGS) { // 「変数初期化部」 l = SCORE_FLAGS[SCORE_TYPE][0]; hps = SCORE_FLAGS[SCORE_TYPE][1]; a = SCORE_FLAGS[SCORE_TYPE][2]; kuro = SCORE_FLAGS[SCORE_TYPE][3]; // 「譜面データ定義部」 // 「TSV 出力部」(後述) }
のようなループ処理を行えば node の実行 1 回で、1 ファイルの DP 全パターンを TSV ファイルに出力できるはず。
TSV 出力部
譜面種別が決定されて「譜面データ定義部」を通過すると、sp
, dp
, c1
, c2
, tc
, ln
が確定する。楽曲によっては、チャージノーツがなかったり BPM 変動がなかったりするため、その場合はc1
, tc
などは空配列になるだろう。
ただ、tc
が未定義だと BPM の初期情報が出力できないので、ちょっと工夫が必要そうだ。
TSV ファイルは以下のような項目で構成する。
楽曲ID | プレースタイル | 譜面種別 | 出力配列 | 小節番号 | データ |
それぞれ、
楽曲ID | "absolute" や"pperplex" 、"_sakura" など |
プレースタイル | "SP" または"DP" |
譜面種別 | "B" , "N" , "H" , "A" , "L" |
出力配列 | "sp" , "dp" , "c1" , "c2" , "tc" , "ln" |
小節番号 | 0 以上の整数 |
データ | 「出力配列」の「小節番号」要素に格納された文字列や配列の文字列表現。undefined の場合は空文字列 |
を想定する。データ部は、"c1"
や"tc"
のときに配列なので、文字列表現された配列を元に戻すときに少し処理が必要だろう。
楽曲 ID の決定
Node.js で実行する js ファイル名を、楽曲 ID とする(例: absolute.js )。このとき、Node.js は__filename
という予約語で実行する js ファイルのフルパスが獲得できる。
const FILENAME_PATHS = __filename.split('\\'); const FILENAME = FILENAME_PATHS[FILENAME_PATHS.length - 1].replace('\.js', ''); const MUSIC_ID = FILENAME;
Windows で実行しているときは、ディレクトリ区切り文字が\
であるため、このようにしてファイル名の拡張子部分を除いた文字列が、楽曲 ID 文字列としてMUSIC_ID
に格納できる。
譜面の有無
すべての楽曲に DP-NORMAL 譜面および DP-HYPER 譜面が用意されているが、DP-ANOTHER 譜面や DP-LEGGENDARIA 譜面は一部の楽曲に実装されていない。実装されていない譜面種別については、処理をスキップさせたほうがよい。
譜面種別ごとのレベル数値は、actbl.js に定義されている。この actbl.js をrequire()
で参照できるよう、
// 変更前 actbl = { // 変更後 exports.actbl={
actbl
オブジェクトをexports
オブジェクトのメンバとするように修正する。これで
const ACTBL = require('./include/actbl.js');
のようにrequire
して、
l = SCORE_FLAGS[SCORE_TYPE][0]; hps = SCORE_FLAGS[SCORE_TYPE][1]; a = SCORE_FLAGS[SCORE_TYPE][2]; kuro = SCORE_FLAGS[SCORE_TYPE][3]; k = 0; if (SCORE_TYPE == 'A' && ACTBL.actbl[MUSIC_ID][19] < 1) { continue; } if (SCORE_TYPE == 'L' && ACTBL.actbl[MUSIC_ID][21] < 1) { continue; }
[19]
は DP-ANOTHER 譜面のレベル、[21]
は DP-LEGGENDARIA 譜面のレベルで、この値が0
のときは譜面が実装されていないことを意味する。
二次元配列の文字列表現化
Node.js が二次元配列をconsole.log()
で出力するとき、複数行で出力されることがある。TSV ファイルに出力するときは、1 小節 1 行で出力されてほしいので、
// TSV 出力部 let OUTPUT_ARRAYS = {'sp': sp, 'dp': dp, 'c1': c1, 'c2': c2, 'tc': tc, 'ln': ln} for (const ARRAY_NAME in OUTPUT_ARRAYS) { let ARRAY = OUTPUT_ARRAYS[ARRAY_NAME]; if (ARRAY) { for (let ix = 0; ix < ARRAY.length; ix++) { let DATA = ARRAY[ix]; if (DATA === undefined) { DATA = ''; } let DATALINE = ''; if (Array.isArray(DATA)) { DATALINE += '['; for (let jx = 0; jx < DATA.length; jx++) { if (jx > 0) { DATALINE += ', '; } let element = DATA[jx]; if (Array.isArray(element)) { DATALINE += ("[" + element.toString(element) + "]"); } else { DATALINE += element; } } DATALINE += ']'; } else { DATALINE = DATA; } console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, ix, HTAB, DATALINE); } } }
DATA
の代わりにDATALINE
を構築することで、複数行出力を回避する。
HTML ファイルから「譜面データ定義部」を抽出
Python スクリプトで、読み込む HTML ファイルをargv
で取得し、標準出力に書き出す。
import sys args = sys.argv if len(args) > 1: html_file = args[1] fp_html = open(html_file, encoding='MS932') text_html = fp_html.read() part1_file = 'source/part_1_init.txt' fp_part1 = open(part1_file) text_part1 = fp_part1.read() part2_file = 'source/part_2_output.txt' fp_part2 = open(part2_file) text_part2 = fp_part2.read() comment_start = text_html.find('<!--') comment_end = text_html.rfind('-->') func_hd_call = text_html.find('hd();') if comment_start > 0 and comment_end > 0 and func_hd_call > 0 and func_hd_call < comment_end: text_notes = text_html[comment_start + 4 : func_hd_call] print(text_part1) print(text_notes) print(text_part2)
open(path, encoding='MS932')
は、ジャンル名、楽曲名、アーティスト名に ASCII 以外が含まれる場合を考慮したもの。
ここで、source/part_1_init.txt は以下のように定義し、
const FILENAME_PATHS = __filename.split('\\'); const FILENAME = FILENAME_PATHS[FILENAME_PATHS.length - 1].replace('\.js', ''); const MUSIC_ID = FILENAME; const ACTBL = require('./include/actbl.js'); // 譜面種別 const HTAB = '\t'; const MODE_DP = 'DP'; const SCORE_FLAGS = {'N': [1, 1, 0, 0], 'H': [0, 1, 0, 0], 'A': [0, 0, 1, 0], 'L': [0, 0, 1, 1]}; for (const SCORE_TYPE in SCORE_FLAGS) { // 変数初期化部 ln=[],sp=[],dp=[],tc=[],c1=[],c2=[],cn=[]; genre=title=artist=bpm=opt=lnse=lnhs="",key=ky=back=7,hs=gap=ty=k=1;cncnt=bsscnt=legacy=prt=pty=0; soflan=level=notes=measure=a=l=m=g=db=p1o=hps=flp=off=lnln=lnst=lned=alls=hids=sran=kuro=sftkey=os=hcn=ttl=0; LNDEF=384 // 譜面種別確定部 l = SCORE_FLAGS[SCORE_TYPE][0]; hps = SCORE_FLAGS[SCORE_TYPE][1]; a = SCORE_FLAGS[SCORE_TYPE][2]; kuro = SCORE_FLAGS[SCORE_TYPE][3]; k = 0; if (SCORE_TYPE == 'A' && ACTBL.actbl[MUSIC_ID][19] < 1) { continue; } if (SCORE_TYPE == 'L' && ACTBL.actbl[MUSIC_ID][21] < 1) { continue; } // 譜面データ定義部
同様に、source/part_2_output.txt は以下のように定義する。
// TSV 出力部 let OUTPUT_ARRAYS = {'sp': sp, 'dp': dp, 'c1': c1, 'c2': c2, 'tc': tc, 'ln': ln} for (const ARRAY_NAME in OUTPUT_ARRAYS) { let ARRAY = OUTPUT_ARRAYS[ARRAY_NAME]; if (ARRAY) { for (let ix = 0; ix < ARRAY.length; ix++) { let DATA = ARRAY[ix]; if (DATA === undefined) { DATA = ''; } let DATALINE = ''; if (Array.isArray(DATA)) { DATALINE += '['; for (let jx = 0; jx < DATA.length; jx++) { if (jx > 0) { DATALINE += ', '; } let element = DATA[jx]; if (Array.isArray(element)) { DATALINE += ("[" + element.toString(element) + "]"); } else { DATALINE += element; } } DATALINE += ']'; } else { DATALINE = DATA; } console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, ix, HTAB, DATALINE); } if (ARRAY_NAME == 'tc' && ARRAY.length == 0) { // tc 未定義時の初期BGM補完 let bpm_init = parseInt(bpm, 10); if (bpm_init == bpm) { if (bpm_init < 100) { bpm_init = ' ' + bpm_init; } bpm_init += '0'; console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, ARRAY_NAME, HTAB, 0, HTAB, '["' + bpm_init + '"]'); } } } } console.log(MUSIC_ID, HTAB, MODE_DP, HTAB, SCORE_TYPE, HTAB, 'LNDEF', HTAB, 0, HTAB, LNDEF); }
標準出力に書き出された文字列を js ファイルにリダイレクトするなどして、その js ファイルを Node.js で実行すると TSV ファイルが入手できる。”tc"
の0
としてbpm
値を出力したり、"LNDEF"
の0
としてLNDEF
値を出力する処理を追加した。
これにより、一般的な 4 分の 4 拍子の曲では
511 DP A dp 36 #p7P54DA 511 DP A tc 0 ["1010"] 511 DP A LNDEF 0 384
のように TSV 出力され、それ以外の曲では
penta_cb DP A dp 67 #O7mUpQQAHg6GA penta_cb DP A tc 0 ["1830"] penta_cb DP A LNDEF 0 480
のように TSV 出力される。