Underground 2 Revived
Chrome과 Firefox에서 Need for Speed Underground 2를 WebGPU로 두고 팬 프로젝트입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗公開された Google スプレッドシートを BMS 難易度表として配信します。 データは保存しません。 リクエストのたびにスプレッドシートから取得して変換するだけです。
https://bmstable.vercel.app/t/<スプレッドシートID>/<表の名前>/
この URL(表のページ)を beatoraja の難易度表に登録します。ID を差し替えれば誰の表でも読めます。事前登録は不要です。 1 つのスプレッドシートに複数の難易度表(データシート)を置けます。
| ルート | 内容 |
|---|---|
/t/<ID>/ | スプレッドシート内のシート一覧 |
/t/<ID>/<表の名前>/ | その難易度表のページ。beatoraja に登録する URL |
/t/<ID>/<表の名前>/header.json | ページの <meta name="bmstable"> が指すヘッダ |
/t/<ID>/<表の名前>/data.json | 譜面データ(header.json から相対参照) |
/t/<ID>/<段位シートの名前>/ | その段位認定のページ(課題曲を曲名で並べる) |
/ | スプレッドシートの URL から一覧ページへ飛ぶページ |
data.json は level_order の順 → 曲名順に並べ替えて返します。
スプレッドシートの行順には依存しないので、シートを並べ替えたり行を追記しても順序は安定します。
曲名の比較は Intl.Collator("ja", { numeric: true }) です
(漢字は ICU 日本語照合の順。読み順にするにはシートに読みの列が必要です)。
<表の名前> はデータシートのタブ名です(URL エンコードされます)。
タブ名に / が含まれて指定できない場合は、代わりに gid を使えます。
段位認定も出せます。 シートを 1 枚足してヘッダシートの course 列で名指しすると、
header.json の course に入って beatoraja の選曲画面に段位コースが並びます
(書式は段位認定(course))。
| status | 意味 |
|---|---|
| 200 | 成功 |
| 400 | ID の形式が不正 |
| 404 | ルートが存在しない / そのシートが無い・難易度表でない(本文にシート一覧を出します)/ 段位シートの header.json を叩いた(本文で表の URL を案内します) |
| 502 | スプレッドシートが非公開 / 存在しない / 構成が条件を満たさない / Google がレート制限中 |
結果は 600 秒キャッシュします。
シート 1 枚 = CSV 1 本なので、シートの枚数がそのまま Google へのリクエスト数になります。 40 枚を超えるスプレッドシートを一度に全部取ると Google が 429 を返します (Vercel の出口 IP は共有なので、手元の端末から叩くより早く制限に当たります)。 そのため次のようにしています。
header.json・data.json は 3 リクエスト(段位認定を付けた表は 4)。
読むのは対象のデータシートとヘッダシートの 2 枚だけです。シート一覧(/htmlview)は
CSV を読まないので、枚数に関係なく 1 本で済みます。ヘッダシートは先頭のタブ 1 枚しか
見ないので、シートが何枚あっても本数は変わりません(詳細は下の「ヘッダシートの位置」)。
段位認定シートはヘッダ行の course 列が名指ししたときだけ読むので、
増えるのは段位を持つ表だけ 1 本です(探索はしません)。
段位認定のページは 3 本(曲名が書かれていない課題曲があるときだけ 4 本)です。/t/<ID>/ の一覧ページだけですが、
そこも Promise.all で一斉に投げず、4 本ずつに絞ります。Retry-After があればそれに従います。
再試行の待ち時間はリクエスト全体で 15 秒の予算を共有します
(Edge Middleware の実行時間上限は 25 秒)。ヘッダシートの CSV が読めなかった場合も、表そのものは返します
(name はタブ名、symbol は空文字になり、警告が出ます)。
ヘッダシートは先頭のタブにある場合だけ読みます。 どれがヘッダシートかは列名を
見るまで分からないので、対象のデータシートを除いた先頭の 1 枚だけを読んで判定し、
外れたら探しません。これで構成に関係なく必ず 3 リクエスト
(/htmlview + 対象のデータシート + 1 枚)で終わります。
探し続けないのは、リクエスト数がスプレッドシートの構成次第で膨らむからです。
以前は見つかるまで 1 枚 → 4 枚 → 4 枚…と読んでいたので、ヘッダシートが後ろにあるほど
増え(CSV 1 → 5 → 9 → …)、ヘッダシートを置いていない表では毎回全シートを
読んでいました(シート 40 枚で 41 リクエスト。打ち切る条件が無いため)。
beatoraja は難易度表を読み込むたびに表ページ・header.json・data.json を叩くので、
ここが上限を持たないのは具合が悪いです。
| 構成 | 以前 | 現在 |
|---|---|---|
| ヘッダが先頭 | 3 リクエスト | 3 リクエスト |
| ヘッダが 3 枚目 | 7 リクエスト | 3 リクエスト(ヘッダは読まない) |
| ヘッダ無し・データ 40 枚 | 41 リクエスト | 3 リクエスト |
ヘッダが先頭・course 列あり | — | 4 リクエスト |
段位認定シートを探さないのも同じ理由です。どのタブが段位かは列名を見るまで
分からないので、ヘッダ行の course 列に名指しさせています。course 列が空欄なら
1 本も増えず、名指ししたタブが読めなくても表自体は返します(警告を出します)。
先頭に無い場合、その表は name がタブ名、symbol が空文字になり、description は
消え、level_order はシートに出てきた level を並べたものになります。黙って変わると
気付けないので、表ページと一覧ページに警告を出します。一覧ページは全シートを読むので
ヘッダシートが何枚目にあるかを知っており、そこだけが「ヘッダシートはあるが先頭にない」と
断定できる場所です。
ヘッダシートを置いていない表(データシートだけで成立する構成)でも表ページの警告は 出ます。先頭 1 枚を見ただけでは「無い」のか「後ろにある」のか区別できないためで、 文言は「読んでいません」にしてあります。データシートが 1 枚だけなら読む先が無いので 警告は出ません(2 リクエスト)。
なお一覧ページ(/t/<ID>/)はヘッダシートの位置と無関係です。譜面数を出すために
どうせ全データシートを読むので、ヘッダがどこにあってもシートの枚数 + 1 本になります
(シート 47 枚のスプレッドシートで 48 本・約 9〜12 秒を実測)。一覧を速くするには
譜面数の列を捨てる必要があり、それは別の判断です。
一覧(/htmlview)を省いてシート名で直接 CSV を取れれば更に 1 本減りますが、
両方の手段が黙って壊れるので gid 経由にしています。どちらも実測で確認しました。
/export?format=csv&sheet=<名前> は sheet= を無視して常に先頭シートを返す。
sheet=ほげぇ でもヘッダシートの中身が 200 で返ってきます。エラーにならないので
気付けません。/gviz/tq?tqx=out:csv&sheet=<名前> は名前が効きますが、gviz が列の型を推論して
合わない値を空にします。 ほげぇ の level 列は数値と判定され、交互 が "" に
なりました。level が空の行は読み飛ばすので、譜面が黙って消えます。シート名も gid も決め打ちしていません。列名だけを見て役割を判定します。
| 役割 | 目印の列 |
|---|---|
| データシート | md5 または sha256 |
| ヘッダシート | name / symbol / level_order / description のいずれか |
| 段位認定シート | name と md5 / sha256 の両方 |
段位認定シートはデータシートと同じ列を持てるので、name 列があるかどうかで見分けます
(譜面名はデータシートでは title、段位シートの name は段位名)。ヘッダシートも name を
持ちますが md5 を持たないので、name と md5 の両方があれば段位認定シートです。
そのためデータシートに name 列を作らないでください(段位シートとして扱われます)。
md5 または sha256 の列を持つシート。1 行目が列名、2 行目以降が 1 行 1 譜面。
| level | title | md5 | artist | comment |
|---|---|---|---|---|
| 18 | -+ -EX- | bca300b4… |
level と title、そして md5 / sha256 のどちらかが必須。欠けた行は読み飛ばすartist comment など他の列はそのまま data.json に通るname 列は作らないこと。 name と md5 の両方があるシートは
段位認定シートとして扱われる<表の名前> になる。 対応するヘッダ行が無ければ表の名前にもなる表ページでは次の列がリンクになります。値が空の行は素のテキストで表示します。
| 列 | リンク先 | 由来 |
|---|---|---|
| Level | url_diff | 差分の配布元 |
| Title | Stellaverse IR の譜面ページ | md5 から生成 |
| Artist | url | 本体の配布元 |
url / url_diff はスプレッドシート由来の任意の文字列なので、http:// と https:// で
始まるものだけをリンクにします(javascript: などを踏ませないため)。
この 2 列は beatoraja 側でも使われます。url が BMSTableElement.getURL()、
url_diff が DifficultyTableElement.getAppendURL() として読まれ、
SongData の url / appendurl に入ります。表ページのリンク先を
url = 本体、url_diff = 差分としているのはこれに合わせたものです。
name / symbol / level_order / description のいずれかの列を持つシート。1 行目が列名、2 行目以降が 1 行 1 表。
| name | description | symbol | level_order |
|---|---|---|---|
| さつき難易度表 | ふつうの表 | ☆ | [1,2,3] |
| おにレベル表 | きつい表 | ◆ | [A,B] |
name 列の値がデータシートのタブ名と一致する行が、その表のヘッダとして使われる。
一致しないヘッダ行は使われません。 name は表の表示名とタブの照合キーを兼ねているので、
表示名だけ変えたつもりでも、その表の symbol・description・level_order・course が
まるごと無視されます。黙って落ちると気付けないので、表ページ・一覧ページ・段位ページの
3 箇所で警告を出します(一覧ページはどのタブにも対応しない name を洗い出せます)level_order は [1,2,3] / ["A","B"] / [A,B] / 1,2,3 のいずれでもよい(配列に正規化する)symbol なし)name はタブ名、symbol は空文字になります)。先頭 1 枚しか見ないのは
リクエスト数に上限を持たせるためで、理由と警告の出方は
ヘッダシートの位置に書いてあります段位認定は難易度表の header.json の中に入ります(別ファイルではありません)。
1 行 = 1 課題曲のシートを 1 枚置き、ヘッダシートの course 列にそのタブ名を書きます。
| name | description | symbol | level_order | course |
|---|---|---|---|---|
| さつき難易度表 | ふつうの表 | ☆ | [1,2,3] | さつき段位 |
course 列が空欄の表は段位シートを読みません(リクエストが増えません)。
列ごと無くても構いません。タブ名の代わりに gid でも指せます。
段位シートはデータシートと同じ列で、1 行 = 1 課題曲です。
違うのは name 列があることで、ここに段位名を書きます。
同じ name の行がまとめて 1 つの段位になります。
| name | level | title | artist | md5 | url | url_diff |
|---|---|---|---|---|---|---|
| 発狂初段 | 1 | Act Beloved [HARD] | Nene Anegasaki feat. Salita | 5734e689… | … | … |
| 発狂初段 | 1 | FORBIDDEN APPLE [HARD] | きゃと composed by TiS | 5bb7d406… | … | … |
| 発狂二段 | 3 | -if-扉のその向こう側へ [HARD] | 暁美ほむら | 2c0d139b… | … | … |
level は譜面自体のレベル(その難易度表のレベル)です。段位名は name 列なので
意味が被りません。データシートと同じく symbol は外して書きます(制約 4)name と md5(または sha256)が必須。 欠けた行は段位になりません
(beatoraja の CourseData.validate() が譜面 0 件のコースを捨てるため)title / artist / url / url_diff / level は header.json に埋め込まれます。
course の各段位が md5 の配列ではなく charts 形式で出るので、beatoraja 側では
手元に無い課題曲でも曲名が出て、配布元リンクも辿れます
(course の譜面は DifficultyTableElement として読まれ、toSongData が
title / artist / url / appendurl を SongData に写します)md5 で引いて補います
(header.json を作るときにはもう読んであるのでリクエストは増えません)。
1 行に md5 を複数並べた書き方も通りますが、その行の曲情報はどの譜面のものか
決められないので使われませんconstraint と trophy は段位ごとの値です。1 行 = 1 課題曲 なので同じ段位が
複数行に跨りますが、その段位で最初に書かれた値を使います(先頭の行だけに
書いても、全行に書いても同じです)md5 constraint は level_order と同じ書式です。[a,b,c] / a,b,c / ["a","b"] のいずれでもよく、
セル内改行も区切りとして扱います。md5 列に 64 桁のハッシュを書くと sha256 に振り分けますstyle は扱いません。 bms.table.Course には style フィールドがありますが、
読み戻すのは encodeJSONTableHeader(header.json の書き出し)だけで、beatoraja が
実際に使う CourseData には存在しないので選曲にも演奏にも効きません。NEW GENERATION が
自分の表ページの配色に使っているキーで、Satellite / Stella は出していません
(trophy の中の style も同様で、こちらは誰も読み戻しません)bms.table.Course が持たないキーを出すと表ごと読めなくなる可能性があるためheader.json の course は Course[][](グループの配列)ですが、グループは常に 1 つです。
シート側に分割用の列は置いていません。
beatoraja がこの入れ子を捨てるからです。
TableDataAccessor$DifficultyTableAccessor が
Stream.of(table.getCourse()).flatMap(...) で平らにし、TableData.course は
CourseData[] の一次元配列になります。グループ名を入れるキーも JSON にありません。
つまり分けても選曲画面に並ぶ順序が変わるだけで、段位がフォルダに分かれるわけでは
ないので、シートに列を増やす価値がありません。順序はシートの行順で決めてください。
constraint に書けるのは次の 14 個だけです。CourseDataConstraint.getValue() が
受け付ける文字列を実機の jar で列挙したもので、大文字小文字は区別します
(GRADE_MIRROR や class mirror gauge_7keys は通りません)。
これ以外は落とします。
| type | 書ける値 | 効果 |
|---|---|---|
| 0 | grade / grade_mirror / grade_random | ランダムの許可範囲(正規のみ / ミラーまで / RANDOM可)。このどれかが入っていないと段位扱いにならない(CourseData.isClassCourse() が CLASS / MIRROR / RANDOM のいずれかを探している) |
| 1 | no_speed | 速度変更禁止(ノースピ)。BMSPlayer / LaneRenderer が見ている |
| 2 | no_good / no_great | GOOD / GREAT を出すと即終了。JudgeManager が見ている |
| 3 | gauge_lr2 / gauge_5k / gauge_7k / gauge_9k / gauge_24k | ゲージの種類 |
| 4 | ln / cn / hcn | ロングノートの種類 |
空欄のときは [grade_mirror,gauge_lr2] を入れます。beatoraja 自身は constraint を
省いた course を空配列のまま通すので(=段位扱いにならない)、これは変換側の既定値です。
旧 grade キーしか無い表を beatoraja が course に変換するときに入れる組と同じものを使っています。
実在する表もこの範囲で、NEW GENERATION が [grade_mirror,gauge_lr2] /
[grade_mirror,no_speed] / [grade_mirror,no_good] / [grade_mirror]、
Satellite と Stella が [grade_mirror,gauge_lr2,ln] です。
trophy の書き方は 4 通りです。
| セル | 結果 |
|---|---|
| 空欄 | 銅・銀・金の既定 3 段(7.5/0 5.0/70 2.5/85) |
[] または - | メダルなし |
[silvermedal,goldmedal] | 名前だけ。既知メダルの閾値を補う |
["name": "x", "missrate": 7.5, "scorerate": 0.0] | 明示。name が出てくるたびに次の 1 件に切り替わる |
明示する形はクォートの有無と種類を問いません([{"name":"goldmedal","missrate":2.5}] でも通ります)。
missrate / scorerate を省いた場合は既知メダルの閾値、知らない名前なら 0(条件なし)で埋めます。
course 列が指したシートを読めなかった場合は、表自体は返して表ページに警告を出します
(段位が黙って消えるのを防ぐため)。
/t/<ID>/<段位シートの名前>/ が段位認定のページです。段位ごとに課題曲を並べ、
曲名・レベル・アーティストと、url_diff(差分)・url(本体)へのリンクを出します。
難易度表のページからもリンクしています。
曲名は段位シートに書かれていればそれを使います。書かれていない課題曲があるときだけ、
同じスプレッドシートのデータシートを 1 枚読んで md5 で突き合わせます
(どちらにも無ければ md5 のまま出します)。
外部の IR には問い合わせません。 このサービスが fetch する先は
docs.google.com の /htmlview と /export?format=csv だけです。
Stellaverse IR は譜面ページの href に入れているだけなので、ページを開いても
IR にはリクエストが飛びません(利用者がリンクを押したときに、その人のブラウザが開くだけです)。
難易度表のページ側には段位の中身を出しません。
段位認定: <タブ名> の 1 行をこのページへのリンクとして置くだけです。
登録する URL は難易度表と同じ(表のページ)です。 段位シートに header.json は
ありません(難易度表ではないので data.json も作れません)。段位は表の header.json の
course に入って配信されるので、登録先は表と 1 つで足ります。
このページの <meta name="bmstable"> も表の header.json を指しているため、
段位ページの URL をそのまま beatoraja に登録しても表ごと読み込まれます。
どの表に載っているかは、ヘッダシートの course 列がこのシートを指している行から引きます。
指している行が無い(course 列に書き忘れた、ヘッダシートが先頭のタブに無い)場合は、
段位の一覧だけ出して登録 URL の代わりに警告を出します。
リクエストは 3 本です(/htmlview + 段位シート + ヘッダシート)。段位シートに曲名が
書かれていない課題曲があるときだけ、データシートを読んで 4 本になります。
header.json / data.json を段位シートの URL で叩いた場合は 404 を返し、本文で表のページの
URL を案内します。
共有設定は**「リンクを知っている全員が閲覧可」**にしてください。
0.8.8 の jar を読んで確認し、実機で検証したものです(0.8.9 の行は 0.8.9 の jar で確認)。 変換側で対処済みですが、スプレッドシートを編集するときに関係します。
| 制約 | 根拠 | 対処 |
|---|---|---|
0.8.9 では .json で終わる登録 URL が必ず失敗する | decode() が header 直読みの経路でも getAbsoluteURL(sourceURL, headURL) を呼び、空の sourceURL から new URL("") を作って MalformedURLException: no protocol。ログは「読み込み失敗」だけ | 登録 URL として表のページ(/t/<ID>/<表の名前>/)を案内する。ページの <meta> 経由なら 0.8.8 / 0.8.9 のどちらでも読める。header.json は meta の参照先と、以前に直接登録した URL のために残している |
登録 URL は .json で終わるかどうかで扱いが変わる | if (url.endsWith(".json")) で header 直読みか HTML 解析かを分岐 | ID と表名をクエリではなくパスに入れている |
name と symbol は必須で、かつ String でなければならない | !(result.get("symbol") instanceof String) で IOException("ヘッダ部の情報が不足しています")。譜面データに到達する前に失敗し、ログは「読み込み失敗」だけ。JSON の数値も弾かれる | symbol が空でもキーを省略せず "" を出す。数値になった値は文字列に戻す |
level_order は JSON 配列でなければならない | (List) values.get("level_order") と無条件キャスト。文字列だと ClassCastException で表ごと失敗(ログは「読み込み失敗」だけ) | "1,2,3" でも配列に正規化 |
level_order に無い level の譜面は消える | フォルダは level_order から作られ、level の文字列 equals で振り分け | 未知の level を level_order の末尾に追加 |
level に symbol を含めると二重になる | 表示時に symbol が前置される | 先頭が symbol と一致したら外す |
★21(▼21) のような注記は level_order に一致しない | 同上。一致しないと譜面が消える | 括弧部分を level から外し comment に移す |
相対 data_url の解決でクエリが落ちる | url.substring(0, url.lastIndexOf('/') + 1) + data_url | data_url は "data.json"。header.json と同じ階層に置く |
| Content-Type は見ていない | new URL(s).openStream() を直読み | 気にしなくてよい |
course は 2 段ネストの配列でなければならない | の型が |
段位認定(course)については、生成した header.json を beatoraja 本体の
bms.table.DifficultyTableParser#decodeJSONTableHeader に直接食わせて、
Course[][] のネスト・charts の件数・constraint の解釈まで通ることを確認してあります。
JavaScript も実行しません(HTML を行単位で読んで <meta name="bmstable"> を探すだけ)。
そのため /t/<ID>/<表の名前>/ の meta は絶対 URLにしてあります
(末尾スラッシュの有無で解決先が変わらないようにするため)。
なお level_order に [A,B] のようにクォート無しの要素を書いた場合、JSON としては不正なので
JSON.parse が失敗します。そのまま , で分割すると [A と B] という存在しないレベルができて
空のフォルダが並ぶため、角括弧を剥がしてから分割しています。level が空の行も、
symbol だけの空フォルダになるので読み飛ばします。
middleware.js 全部(ルーティング・シート取得・変換・一覧ページ・表ページ)
index.html 案内ページ(Vercel が静的配信する)
vercel.json master 以外のブランチで自動デプロイを行わない設定
.vercelignore 配信に不要なファイルをデプロイから除外する
dev.mjs ローカル確認用のサーバ(デプロイされない)
デプロイ後に公開されるのは /(index.html)と /t/*(middleware の応答)だけです。
middleware.js と vercel.json は Vercel が設定・実行対象として扱うため静的配信されず、
dev.mjs / README.md / LICENSE は .vercelignore で除外しています。
ルート直下の middleware.js は Vercel が Edge Middleware として扱います。
export const config = { matcher: "/t/:path*" } に一致したリクエストだけこのファイルが処理し、
/ の index.html は静的配信のまま通ります。
middleware はリクエストされたパスをそのまま受け取るので、vercel.json の rewrite は不要です。
依存パッケージも無いので package.json もありません
(export 構文のみで、Vercel の Edge ランタイムはそのまま ESM として扱います)。
関数を api/ に置く構成(api/table.js + vercel.json の rewrite)でも同じことができますが、
ファイルが 1 つ増えるので middleware にしています。
tablePage が返す HTML の <script> は、middleware.js のテンプレートリテラル
(バックティック)の中にあります。ここに正規表現リテラルを書くと黙って壊れます。
// middleware.js にこう書くと
return /^https?:\/\//i.test(s) ? s : null;
// ブラウザに届く JS はこうなる(\/ が / に解釈される)
return /^https?://i.test(s) ? s : null;
// 以降が行コメントになるため、残るのは return /^https?:/ = 正規表現リテラルで、
常に truthy になります。条件判定が無効化されても構文としては正しいので
node --check では捕まりません。実際に safeUrl でこれを踏み、javascript: が
素通りしていました。
\\/ とエスケープすれば書けますが読みにくいので、indexOf などで済ませる方が安全です。
${ も同じ理由でそのままは書けません。既存の埋め込みスクリプトは var と function
で書かれているので、合わせてください。
Node 18 以降が必要です(Request / Response / fetch がグローバルにあればよい)。
node dev.mjs # → http://localhost:3000/
PORT=8080 node dev.mjs
本物の Google スプレッドシートを読むので、index.html に URL を貼るところから
表ページの表示まで通しで確認できます。middleware.js を編集したらブラウザの
再読み込みだけでよく、サーバの再起動は要りません(リクエストごとに読み直します)。
index.html を file:// で直接開いても動きませんブラウザでファイルを開くだけでは、変換を行うサーバが存在しないため動きません。
index.html は URL を組み立てるだけの入力フォームで、実体は middleware.js です。
location.origin も file:// では使えないので、表示される URL も壊れます。
変換処理をクライアント側に移しても解決しません。理由は 2 つあり、どちらも回避不能です。
| 壁 | 内容 |
|---|---|
| CORS | /htmlview に CORS ヘッダが無い。/export?format=csv は最終応答には Access-Control-Allow-Origin: * が付くが、最初の 307 リダイレクトには無い。CORS はリダイレクト連鎖の各応答が検査を通る必要があるため失敗する(file:// の opaque origin ならなおさら) |
| ESM | file:// のスクリプトは opaque origin 扱いになり、ES モジュールとして読み込めない(Chrome / Firefox とも)。middleware.js を import できない |
静的ファイルサーバ(python3 -m http.server など)でも不十分です。index.html は
表示できますが、/t/... は middleware が実行されないので 404 になります。
Vercel の Edge Middleware は Node ではなく V8 isolate で動きます。Node API が無く、
ICU のデータも異なります。middleware.js が Intl.Collator の生成失敗を拾って
コードポイント比較に落としているのはそのためで、dev.mjs で見える日本語の並び順は
本番と一致する保証がありません。曲名順だけは本番でも目視してください。
dev.mjs の実装メモdev.mjs は middleware.js をソースとして読み、data: URL 経由で動的 import します。
export を持つ ES モジュールなのに package.json が無いため、Node は拡張子 .js を
CommonJS として解釈し、import ... from "./middleware.js" と書くと export が構文エラーに
なるからです。package.json を足せば解決しますが、Vercel 側のビルド設定の自動検出に
影響しかねないので避けています。
また、本番と同じく /t/* だけを middleware に渡し、残りは静的配信に落としています
(config.matcher の再現)。ここを揃えないとルーティングの挙動がずれます。
dev.mjs は .vercelignore に入れてあるのでデプロイには含まれません(/dev.mjs は 404)。
ローカル専用のファイルです。
DifficultyTable.coursebms.table.Course[][]group 列で切ってから出す |
| 譜面 0 件のコースは捨てられる | CourseData.validate() が hash の null / 長さ 0 で false | md5 / sha256 が空の行を段位にしない |
知らない constraint は解釈されない | CourseDataConstraint.getValue() が null を返す | 既知の 14 個以外を落とす |