diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c4668f48..9ab871b9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,12 @@ version: 2 updates: - package-ecosystem: "npm" - directories: - - "/" - - "/packages/*" + # The root entry covers every pnpm workspace member: Dependabot bumps + # packages/*/package.json manifests together with the shared root + # pnpm-lock.yaml. Per-package directory entries must not be added back — + # they generate manifest-only PRs that leave pnpm-lock.yaml stale and fail + # CI with ERR_PNPM_OUTDATED_LOCKFILE. + directory: "/" schedule: interval: "weekly" ignore: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58fba925..de3b9840 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -84,7 +84,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup pnpm uses: pnpm/action-setup@v6 @@ -92,7 +92,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -111,8 +111,6 @@ jobs: needs.changes.outputs.code_changed == 'true' runs-on: ubuntu-24.04-arm timeout-minutes: 30 - outputs: - snapshot_b64: ${{ steps.aggregate-head.outputs.snapshot_b64 }} env: BENCH_TIME_MS: 20 BENCH_WARMUP_TIME_MS: 10 @@ -122,7 +120,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} @@ -132,7 +130,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -156,12 +154,18 @@ jobs: run: | set -euo pipefail pnpm run bench:aggregate -- --output tmp/bench/head.json tmp/bench/head-runs/*.json - { - echo "snapshot_b64<<__SNAPSHOT__" - node -e "const fs=require('node:fs'); process.stdout.write(fs.readFileSync('tmp/bench/head.json').toString('base64'));" - echo - echo "__SNAPSHOT__" - } >> "$GITHUB_OUTPUT" + + # The snapshot moves between jobs as an artifact, never as a job output. It outgrew both limits a job + # output would impose: a single argv/envp string caps at `MAX_ARG_STRLEN` (128 KiB) so the compare job + # could neither pass it to `node` nor even start `bash` with it in the environment, and job outputs + # themselves cap at 1 MiB. Artifacts have no such ceiling and keep the payload off the process image. + - name: Upload head benchmark snapshot + uses: actions/upload-artifact@v7 + with: + name: bench-pr-head-snapshot + path: tmp/bench/head.json + retention-days: 1 + if-no-files-found: error benchmark-pr-base: name: Benchmark Base (PR) @@ -173,7 +177,6 @@ jobs: outputs: available: ${{ steps.detect-bench.outputs.available == 'true' && steps.run-base-benchmark.outcome == 'success' }} - snapshot_b64: ${{ steps.aggregate-base.outputs.snapshot_b64 }} env: BENCH_TIME_MS: 20 BENCH_WARMUP_TIME_MS: 10 @@ -183,7 +186,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} @@ -194,7 +197,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -259,12 +262,16 @@ jobs: run: | set -euo pipefail pnpm run bench:aggregate -- --output tmp/bench/base.json tmp/bench/base-runs/*.json - { - echo "snapshot_b64<<__SNAPSHOT__" - node -e "const fs=require('node:fs'); process.stdout.write(fs.readFileSync('tmp/bench/base.json').toString('base64'));" - echo - echo "__SNAPSHOT__" - } >> "$GITHUB_OUTPUT" + + - name: Upload base benchmark snapshot + if: steps.detect-bench.outputs.available == 'true' && + steps.run-base-benchmark.outcome == 'success' + uses: actions/upload-artifact@v7 + with: + name: bench-pr-base-snapshot + path: tmp/bench/base.json + retention-days: 1 + if-no-files-found: error benchmark-pr: name: Benchmark (PR) @@ -284,7 +291,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} @@ -294,7 +301,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -303,20 +310,23 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline + # The snapshot travels as a base64 job output. It MUST reach the decoder through the environment and stdin, + # never as a command-line argument: Linux caps a single argv entry at `MAX_ARG_STRLEN` (128 KiB), and the + # snapshot passed that once the benchmarked export count grew — the step died with + # `node: Argument list too long` (exit 126). `printf` is a bash builtin, so piping the env var into + # `base64 -d` never execs with the payload attached. - name: Restore head benchmark snapshot - shell: bash - run: | - set -euo pipefail - mkdir -p tmp/bench - node -e "const fs=require('node:fs'); fs.writeFileSync('tmp/bench/head.json', Buffer.from(process.argv[1], 'base64'));" '${{ needs.benchmark-pr-head.outputs.snapshot_b64 }}' + uses: actions/download-artifact@v8 + with: + name: bench-pr-head-snapshot + path: tmp/bench - name: Restore base benchmark snapshot if: needs.benchmark-pr-base.outputs.available == 'true' - shell: bash - run: | - set -euo pipefail - mkdir -p tmp/bench - node -e "const fs=require('node:fs'); fs.writeFileSync('tmp/bench/base.json', Buffer.from(process.argv[1], 'base64'));" '${{ needs.benchmark-pr-base.outputs.snapshot_b64 }}' + uses: actions/download-artifact@v8 + with: + name: bench-pr-base-snapshot + path: tmp/bench - name: Generate benchmark report shell: bash @@ -331,6 +341,7 @@ jobs: - name: Post benchmark comment uses: actions/github-script@v9 with: + retries: 3 script: | const fs = require('node:fs'); const marker = ''; @@ -375,8 +386,6 @@ jobs: if: github.event_name == 'push' && needs.changes.outputs.code_changed == 'true' runs-on: ubuntu-24.04-arm timeout-minutes: 30 - outputs: - snapshot_b64: ${{ steps.aggregate-head.outputs.snapshot_b64 }} env: BENCH_TIME_MS: 20 BENCH_WARMUP_TIME_MS: 10 @@ -386,7 +395,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.sha }} @@ -396,7 +405,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -420,12 +429,14 @@ jobs: run: | set -euo pipefail pnpm run bench:aggregate -- --output tmp/bench/head.json tmp/bench/head-runs/*.json - { - echo "snapshot_b64<<__SNAPSHOT__" - node -e "const fs=require('node:fs'); process.stdout.write(fs.readFileSync('tmp/bench/head.json').toString('base64'));" - echo - echo "__SNAPSHOT__" - } >> "$GITHUB_OUTPUT" + + - name: Upload head benchmark snapshot + uses: actions/upload-artifact@v7 + with: + name: bench-push-head-snapshot + path: tmp/bench/head.json + retention-days: 1 + if-no-files-found: error benchmark-push-base: name: Benchmark Base (Push) @@ -436,7 +447,6 @@ jobs: outputs: available: ${{ steps.detect-bench.outputs.available == 'true' && steps.run-base-benchmark.outcome == 'success' }} - snapshot_b64: ${{ steps.aggregate-base.outputs.snapshot_b64 }} env: BENCH_TIME_MS: 20 BENCH_WARMUP_TIME_MS: 10 @@ -446,7 +456,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{ github.sha }} @@ -457,7 +467,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -527,12 +537,16 @@ jobs: run: | set -euo pipefail pnpm run bench:aggregate -- --output tmp/bench/base.json tmp/bench/base-runs/*.json - { - echo "snapshot_b64<<__SNAPSHOT__" - node -e "const fs=require('node:fs'); process.stdout.write(fs.readFileSync('tmp/bench/base.json').toString('base64'));" - echo - echo "__SNAPSHOT__" - } >> "$GITHUB_OUTPUT" + + - name: Upload base benchmark snapshot + if: steps.detect-bench.outputs.available == 'true' && + steps.run-base-benchmark.outcome == 'success' + uses: actions/upload-artifact@v7 + with: + name: bench-push-base-snapshot + path: tmp/bench/base.json + retention-days: 1 + if-no-files-found: error benchmark-push: name: Benchmark (Push) @@ -550,7 +564,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.sha }} @@ -560,7 +574,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -569,20 +583,19 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline + # Env + stdin, never argv — see the matching note on the PR-side restore steps (`MAX_ARG_STRLEN`). - name: Restore head benchmark snapshot - shell: bash - run: | - set -euo pipefail - mkdir -p tmp/bench - node -e "const fs=require('node:fs'); fs.writeFileSync('tmp/bench/head.json', Buffer.from(process.argv[1], 'base64'));" '${{ needs.benchmark-push-head.outputs.snapshot_b64 }}' + uses: actions/download-artifact@v8 + with: + name: bench-push-head-snapshot + path: tmp/bench - name: Restore base benchmark snapshot if: needs.benchmark-push-base.outputs.available == 'true' - shell: bash - run: | - set -euo pipefail - mkdir -p tmp/bench - node -e "const fs=require('node:fs'); fs.writeFileSync('tmp/bench/base.json', Buffer.from(process.argv[1], 'base64'));" '${{ needs.benchmark-push-base.outputs.snapshot_b64 }}' + uses: actions/download-artifact@v8 + with: + name: bench-push-base-snapshot + path: tmp/bench - name: Generate benchmark report shell: bash @@ -597,6 +610,7 @@ jobs: - name: Post benchmark comment uses: actions/github-script@v9 with: + retries: 3 script: | const fs = require('node:fs'); const marker = ''; @@ -652,7 +666,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup pnpm uses: pnpm/action-setup@v6 @@ -660,7 +674,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d8cea77..e9d3143e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout workflow sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.sha }} fetch-depth: 0 @@ -56,7 +56,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -110,7 +110,7 @@ jobs: steps: - name: Checkout release target - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ needs.prepare.outputs.release_target }} @@ -120,7 +120,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm @@ -295,7 +295,7 @@ jobs: steps: - name: Checkout workflow sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.sha }} @@ -305,7 +305,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version-file: .node-version cache: pnpm diff --git a/.node-version b/.node-version index 9af71224..91d2624e 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -25.8.2 +26.7.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ee2b8f1..3bc5ccf7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,7 +57,6 @@ "stringifier", "subartists", "SWBGA", - "tsgo", "Unjudged", "VIDEOFILE", "VOLWAV", diff --git a/AGENTS.md b/AGENTS.md index 8e7b790e..8dffaa51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,20 @@ - Do not prefix pull request titles with `[codex]`. - Use plain, descriptive pull request titles. +# Changeset Rules + +- Base the unreleased set on the diff from the last release: list candidates with `git log --oneline origin/main..origin/devel`. Skip commits already covered by an existing `.changeset/*.md` and only add entries for the uncovered ones. +- One changeset per package. Putting several packages in a single markdown makes Changesets duplicate the same note into every package's CHANGELOG. For a commit that spans multiple packages, split it into one changeset per package and write each note from that package's own perspective. +- Only write entries that are worth landing in a CHANGELOG: + - Include: `fix` / `feat` / `perf`, and bumps of public (runtime `dependencies`) packages (e.g. `fflate`, `node-web-audio-api`). + - Exclude: `devDependencies` dependabot bumps (e.g. `typescript`, `oxlint`, `wrangler`, `tsdown`), tooling-only config changes that do not affect consumers (tsconfig paths, build scripts, bench cases), and WIP / in-progress features. +- Bump level: new export / new subpath / new public API → `minor`; bug fix / internal refactor / dependency bump → `patch`. +- Writing style: + - Keep each body paragraph on a single unwrapped line. + - Put each list item on its own line. + - State what changed and why, concisely; lead with the symptom when the change fixes an observable one. +- Releasing: run `pnpm run release:version` on `devel`, commit the generated `packages/*/package.json` and `CHANGELOG.md` together as `chore: version packages`, then merge the `devel -> main` PR to publish. See README "Package Releases". + # Markdown Localization Rules - Keep English as the canonical `.md` document and Japanese as the `.ja.md` counterpart, except for `AGENTS.md` and `.changeset/README.md`, which remain English only. diff --git a/README.ja.md b/README.ja.md index 74072d44..ebbefe09 100644 --- a/README.ja.md +++ b/README.ja.md @@ -22,7 +22,7 @@ TypeScript + pnpm workspaces で構成した BMS/BMSON ツールチェーンで ## 必要環境 -- Node.js `>= 25` +- Node.js `>= 26` - pnpm workspaces ## セットアップ @@ -70,6 +70,7 @@ tag は `@be-music/package-name@x.y.z` 形式で作成されます。 - [Player 実装仕様](docs/player-spec.ja.md) - [Terminal player 実装メモ](docs/player-tui.ja.md) - [Browser player 実装メモ](docs/player-web.ja.md) +- [プレイログ(プレイ履歴)仕様](docs/playlog.ja.md) - [LR2 skin 実装メモ](docs/lr2-skin.ja.md) - [beatoraja skin 実装メモ](docs/beatoraja-skin.ja.md) - [BMS/BMSON 中間表現 (`@be-music/json`) 実装仕様](docs/json-spec.ja.md) @@ -400,7 +401,7 @@ pnpm run audio-renderer:sea --node-binary /path/to/node 補足: -- Node.js 25.5+ が必要です。 +- Node.js 26+ が必要です。 - SEA 生成は built-in の `--build-sea` を使用します。 ## Exports ベンチマーク diff --git a/README.md b/README.md index 0e63a03d..f86a55fa 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ BMS/BMSON toolchain composed of TypeScript + pnpm workspaces. ## Required environment -- Node.js `>= 25` +- Node.js `>= 26` - pnpm workspaces ## Setup @@ -70,6 +70,7 @@ The tag is created in the format `@be-music/package-name@x.y.z`. - [Player implementation specification](./docs/player-spec.md) - [Terminal player implementation notes](./docs/player-tui.md) - [Browser player implementation notes](./docs/player-web.md) +- [Play log (play history) specification](./docs/playlog.md) - [LR2 skin implementation notes](./docs/lr2-skin.md) - [beatoraja skin implementation notes](./docs/beatoraja-skin.md) - [BMS/BMSON intermediate representation (`@be-music/json`) implementation specification](./docs/json-spec.md) @@ -400,7 +401,7 @@ pnpm run audio-renderer:sea --node-binary /path/to/node supplement: -- Requires Node.js 25.5+. +- Requires Node.js 26+. - SEA generation uses built-in `--build-sea`. ## Exports Benchmark diff --git a/docs/README.ja.md b/docs/README.ja.md index 84f2348a..d96f4772 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -10,6 +10,7 @@ - [Player 実装仕様](player-spec.ja.md) - [Terminal player 実装メモ](player-tui.ja.md) - [Browser player 実装メモ](player-web.ja.md) +- [プレイログ(プレイ履歴)仕様](playlog.ja.md) - [LR2 skin 実装メモ](lr2-skin.ja.md) - [beatoraja skin 実装メモ](beatoraja-skin.ja.md) - [BMS/BMSON 中間表現 (`@be-music/json`) 実装仕様](json-spec.ja.md) diff --git a/docs/README.md b/docs/README.md index e03313e1..7b9217e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ This directory is a Markdown collection of specifications used in the `be-music` - [Player implementation specification](./player-spec.md) - [Terminal player implementation notes](./player-tui.md) - [Browser player implementation notes](./player-web.md) +- [Play log (play history) specification](./playlog.md) - [LR2 skin implementation notes](./lr2-skin.md) - [beatoraja skin implementation notes](./beatoraja-skin.md) - [BMS/BMSON intermediate representation (`@be-music/json`) implementation specification](./json-spec.md) diff --git a/docs/player-web.ja.md b/docs/player-web.ja.md index 05be7fef..c0cf6919 100644 --- a/docs/player-web.ja.md +++ b/docs/player-web.ja.md @@ -162,6 +162,7 @@ pnpm bench -- --packages player-web - browser demo は、未対応 video asset を playback 前に ffmpeg.wasm path で transcode できます。 - chart の自然終了時は、短い post-chart delay を置いてから result scene を開きます。視覚的に result へ遷移しても、残っている gameplay audio tail は中断しません。 - gameplay recorder は WebM output を書き出し、active recording が gameplay bus の破棄前に flush されるよう、scene disposal より前に stop / finalization を終えます。 +- 毎プレイでプレイログ(`*.bmplay.json` 入力リプレイ — 解決済み譜面、生の押下/解放列、プレイ設定。[playlog.ja.md](./playlog.ja.md) 参照)を記録します。demo は result scene のマウント時に自動ダウンロードし、Debug Menu の「Auto-save play history」チェックボックス(デフォルト ON。曲開始時にラッチされ、プレイ中は disabled)で制御します。`bms-playlog` CLI がこのファイルから LR2 / beatoraja / IIDX のリザルトを再現します。プレイログをページへドロップすると、対応する楽曲がロード済みの場合に記録されたランをリプレイ再生します。 ## Compatibility boundary diff --git a/docs/player-web.md b/docs/player-web.md index 34ba00cf..6c10ffce 100644 --- a/docs/player-web.md +++ b/docs/player-web.md @@ -172,6 +172,7 @@ pnpm bench -- --packages player-web - Natural chart completion waits for a short post-chart delay before opening the result scene. The visual transition does not cut off the remaining gameplay audio tail. - The gameplay recorder writes WebM output and coordinates stop/finalization before scene disposal so active recordings are flushed before the gameplay bus is torn down. +- Every play records a play log (`*.bmplay.json` input replay — resolved chart, raw press/release stream, play settings; see [playlog.md](./playlog.md)). The demo auto-downloads it when the result scene mounts, controlled by the Debug Menu's "Auto-save play history" checkbox (ON by default; latched at song start and disabled during a play). The `bms-playlog` CLI re-derives LR2 / beatoraja / IIDX results from the file, and dropping a play-log onto the page replays the recorded run when the matching song is loaded. ## Compatibility boundary diff --git a/docs/playlog.ja.md b/docs/playlog.ja.md new file mode 100644 index 00000000..f48a692b --- /dev/null +++ b/docs/playlog.ja.md @@ -0,0 +1,132 @@ +[English version](./playlog.md) + +# プレイログ(プレイ履歴)仕様 + +このドキュメントは be-music のプレイログ(ゲームプレイ中に記録されるプレイ履歴ファイル)と、そこから +LR2 / beatoraja / IIDX のリザルトを再現するツールについて説明します。 + +## 設計原則: 「結果ログ」ではなく「入力リプレイ」 + +プレイログは意図的に**判定結果の列ではありません**。正本となるデータは次の 3 つです。 + +1. **`chart`** — 実際に画面を流れた解決済み譜面。`#RANDOM` 制御構文の解決後、レーンシャッフル + (RANDOM / MIRROR / S-RANDOM)と DP FLIP の適用後の最終配置です。ロングノートは `timeUs` / `endTimeUs` + を持つ 1 オブジェクトとして保存し、地雷は解決済みのゲージダメージを持ち、`#TOTAL` / `#RANK` / + `#DEFEXRANK` は生の値のまま保持します(各ルールセットが自分のデフォルト式を適用できるように)。 + `chart.sha256` には譜面**ファイル**の SHA-256 を刻印し、ログを内容ベースで元譜面と照合できます。 +2. **`inputs`** — プレイアブルレーンに届いた全ての生のキー押下・解放 + (`{ seq, timeUs, action: 'down' | 'up', channels }`)。入力は判定へ変換せず、ノート ID への割り当ても + 行わず、空 POOR の原因になる「ノートに対応しない入力」もすべて残します。ソートキーは常に + `(timeUs, seq)` で、`seq` が同時刻イベントの順序を確定します。 +3. **`play`** — スコアに影響する設定。モード(manual / auto)、オートスクラッチ、選択ゲージ、最終配置を + 生んだレーンオプションのラベル、ライブ判定幅ルールセット(`judgeRuleset`、省略時 `'lr2'`)、デバッグ用 + 判定幅オーバーライド、ESC 中断フラグです。 + +判定数・EX スコア・コンボ・ゲージ値は **`results`**(ルールセット ID をキーとする再生成可能なキャッシュ。 +`native` は記録時のエンジン自身のサマリ)にのみ存在します。正本が入力列なので、後からルールセットを修正 +しても、過去の全プレイを取り直しなしで再計算できます。 + +時刻は譜面ゼロ基準の整数マイクロ秒です(エンジンのノート時刻と同じ t = 0)。 + +TypeScript 型・シリアライザ・防御的パーサは +[`packages/player/src/playlog/format.ts`](../packages/player/src/playlog/format.ts)(`@be-music/player/playlog`) +にあります。推奨拡張子は `.bmplay.json` です。 + +## 記録 + +記録はエンジン側が担い、全ホスト(ブラウザの LR2 / beatoraja シーン、将来の TUI 採用)が 1 つの実装を共有 +します。`PlayerOptions.onPlaylogRecorded` を設定すると記録が有効になり、エンジンは prepared chart を +スナップショットし、判定対象の押下(`lane-input`)と解放(`kitty-state` release)を `pressedAt` 補正済みの +譜面相対時刻で記録し、空 POOR を native キャッシュ用にカウントし、`autoPlay` / `manualPlay` の解決直前 +(ESC 中断時を含む)に組み上がった `BeMusicPlaylog` をコールバックへ渡します。エンジンが知り得ない +ホスト側設定(選択ゲージ、レーンオプションのラベル、DP FLIP、自由形式の `native` 追加情報)は +`PlayerOptions.recordPlaylog` で渡します。 + +ブラウザプレイヤーでは: + +- LR2 / default ゲームプレイシーンは `PixiGameplayResultData.playlog` として公開します。 +- beatoraja ゲームプレイシーンは `PixiBeatorajaGameplayView.getPlaylog()` として公開します。 +- デモはリザルトシーンのマウント時に `<タイトル>-<タイムスタンプ>.bmplay.json` を自動ダウンロードします。 + Debug Menu の **Auto-save play history** チェックボックス(デフォルト ON)で制御します。プレイログ関連 + オプションは曲の開始時にラッチされ、曲の途中では変更できません — プレイ中はコントロールが disabled + になり、曲開始時点の値がそのプレイに適用されます。 +- Debug Menu の **Play options** フォルダが記録対象のプレイ設定(オートプレイ、判定幅 + LR2 / beatoraja / IIDX、ゲージ、Random 1P/2P、DP FLIP、オートスクラッチ)を持ちます。LR2 セレクトシーンの + PLAY OPTION パネルに対応項目がある設定は双方向に同期します。 + +## ライブ判定幅ルールセット + +共有エンジンはライブプレイを `'lr2'`(デフォルト)/ `'beatoraja'` / `'iidx'` の判定幅で判定できます +(`PlayerOptions.judgeRuleset`)。切り替わるのは**判定幅のみ**です: LR2 はランク補間済みの LR2 幅、 +beatoraja は SEVENKEYS 幅を beatoraja の judgerank で線形スケール(非対称な BAD 窓は ±250 ms × judgerank に +対称化)、IIDX は固定 ±16.67/±33.33/±116.67/±250 ms。ノート選択・空 POOR・ロングノート仕様・ゲージは +エンジンの LR2 準拠のままです — ルールセットの完全再現はプレイログシミュレータが担います。動的 `#EXRANKxx` +は `'lr2'` のときだけ適用されます。選択値は `play.judgeRuleset` として記録され、リプレイは自動的に同じ +判定幅を再適用します。 + +## リプレイ再生 + +`*.bmplay.json` ファイルをブラウザプレイヤーへドロップすると、対応する楽曲がロード済みの場合にリプレイ再生が +始まります(曲フォルダとログを同時にドロップしても動作します — 曲のロード後にリプレイが始まります)。 +楽曲のマッチングは譜面ファイルのハッシュ(`chart.sha256` — セッションやファイル移動をまたいで安定)を最優先し、 +次に記録された `play.native.chartPath`、どちらも無い古いログはタイトル+アーティストで照合します。 + +リプレイは共有エンジン内で記録済み入力列を決定論的に再駆動します(`PlayerOptions.replayInputs`)。各イベントは +記録された譜面相対マイクロ秒の時刻ちょうどで発火し、ライブのレーン入力は無視され(ESC / ポーズ / ハイスピは +有効)、リプレイ実行では新しいプレイログを記録しません。ログは解決済みの最終配置を保存しているため、譜面準備 +では RANDOM / MIRROR を引き直す代わりに記録済みチャンネルを再適用します(`applyPlaylogArrangement`)— つまり +シャッフルされたプレイも正確にリプレイできます。`#RANDOM` 制御構文の分岐が記録時と異なる譜面は再整列できない +ため、ステータス表示でエラーになります。リプレイは記録時のスキンファミリーに関わらず常に LR2 / default の +ゲームプレイパスで実行されます。 + +## LR2 / beatoraja / IIDX リザルトの再現 + +`@be-music/player/playlog` の `simulatePlaylog(playlog, { ruleset })` が 1 ルールセットで入力列を再生し、 +`simulatePlaylogRulesets(playlog)` が 3 つ全てを実行します。判定幅・ノート選択・ロングノート仕様・ +空 POOR・ゲージ表がルールセットごとに異なります。定数の出典は +[`packages/player/src/playlog/rulesets.ts`](../packages/player/src/playlog/rulesets.ts) に記載しています。 + +| 項目 | LR2 (`lr2/1`) | beatoraja (`beatoraja/1`) | IIDX (`iidx/1`) | +| --- | --- | --- | --- | +| 出典 | lr2oraja / OpenLR2 | beatoraja master | コミュニティ実測 (iidx.org) | +| 判定幅(RANK NORMAL) | ±18/±40/±100/±200 ms | ±15/±45/±112.5/late 210 · early 165 ms(7K, judgerank 75 %) | ±16.67/±33.33/±116.67/±250 ms | +| ランクスケーリング | LR2 アンカー補間、BAD 固定 | judgerank 線形、空 POOR 窓固定 | なし | +| ノート選択 | Lowest + multi-BAD 連鎖 | Combo(デフォルト。duration / lowest / score 選択可) | Lowest | +| ロングノート | 全て LN(終端確定の 1 判定) | ノートごとの LN / CN / HCN | 全て CN(mode 3 は HCN ゲージ) | +| 空 POOR 窓 | 早側のみ 1000 ms | late 150 / early 500 ms(7K) | 未測定 — beatoraja の窓を代用 | +| マネースコア | `(4·PG + 2·GR + GD) × 50000 / notes` | — | —(BISTROVER で廃止) | +| ゲージ | lr2oraja LR2 表(2 % 未満即死、32 % 未満ダメージ ×0.6) | beatoraja ネイティブ表 | iidx.org 表(a 値回復、HARD は 30 % 以下でダメージ半減) | + +EX スコアは全ルールセットで PGREAT × 2 + GREAT × 1、DJ LEVEL は IIDX の 9 分率です。チャージノート系の +ルールセットはロングノートの始点・終端を 2 判定ノートとして数えます(各ルールセットの分母は +`result.noteCount` が報告します)。 + +### 再現度に関する注意 + +- LR2 ルールセットは lr2oraja に従い、OpenLR2 の書き起こしと相互検証しています。両者が食い違う箇所 + (LN 頭の見逃し POOR 閾値、HAZARD 表)は lr2oraja の挙動を採用しています。 +- IIDX の内部仕様は非公開です。判定幅・ゲージ表・DJ LEVEL 境界は現在のコミュニティ実測の合意値であり、 + 空 POOR 窓と CN 終端窓は未測定のため beatoraja の値を代用、HCN のゲージ tick は実測されている + 16 分音符間隔の代わりに固定 200 ms を使用しています。「ほぼ一致」は期待できますが、ビット単位の + 完全一致は保証されません。 +- beatoraja の未モデル化要素: PMS の「1 ノートにつき空 POOR 1 回」規則、PMS の 200 ms チャージ解放 + マージン、SEVENKEYS 以外のモード別ゲージ表(全モードで 7K のゲージ定数を使用)。 +- プレイログは解決済み譜面を保存するため、プレイヤー間の `#RANDOM` やレーンシャッフルの実装差は + 再シミュレーションに影響しません。 + +## CLI + +`@be-music/player-tui` は 2 つ目のバイナリ `bms-playlog` を提供します。 + +```bash +pnpm playlog -- results/Song-2026-08-17T10-00-00-000Z.bmplay.json +``` + +オプション: `--ruleset=lr2,beatoraja,iidx|all`、`--gauge=`(ルールセット固有のゲージ上書き)、 +`--algorithm=combo|duration|lowest|score`(beatoraja のノート選択)、`--json`。 + +## バージョニング + +`format: "be-music-playlog"`、`version: 1`。未知の追加フィールドはパース時に無視されるため、後方互換の +追加は version を変えずに行えます。非互換変更は `version` を上げます。ルールセット結果 ID は独自の +リビジョン(`lr2/1` など)を持ち、ルールセットの修正はリビジョンを上げて既存ファイルを再計算するだけです。 diff --git a/docs/playlog.md b/docs/playlog.md new file mode 100644 index 00000000..1cb88a3b --- /dev/null +++ b/docs/playlog.md @@ -0,0 +1,140 @@ +[Japanese version](./playlog.ja.md) + +# Play log (play history) specification + +This document describes the be-music play log ("playlog") — the play-history file recorded during gameplay — and +the tools that re-derive LR2 / beatoraja / IIDX results from it. + +## Design principle: an input replay, not a result log + +A playlog is deliberately **not** a list of judgments. Its canonical payload is: + +1. **`chart`** — the resolved chart that actually scrolled past the player: post-`#RANDOM` control flow, post + lane-shuffle (RANDOM / MIRROR / S-RANDOM), post DP-flip. Long notes are stored as single objects with a + `timeUs` / `endTimeUs` pair, mines carry their resolved gauge damage, and the raw `#TOTAL` / `#RANK` / + `#DEFEXRANK` metadata is preserved so each ruleset can apply its own defaults. `chart.sha256` stamps the + SHA-256 of the source chart FILE bytes so a log can be matched back to its chart by content. +2. **`inputs`** — every raw key press / release that reached a playable lane, as + `{ seq, timeUs, action: 'down' | 'up', channels }`. Inputs are never converted into judgments, never assigned + to a note id, and phantom presses (the ones that fire LR2 empty POORs) are kept. The sort key is always + `(timeUs, seq)` — `seq` disambiguates same-microsecond events. +3. **`play`** — the settings that affect scoring: mode (manual / auto), auto scratch, the selected gauge, the + lane-arrangement options that produced the resolved chart, the live judge-window ruleset (`judgeRuleset`, + absent = `'lr2'`), the debug judge-window override, and an ESC flag. + +Judgments, EX-SCORE, combo, and gauge values live only in **`results`** — a regenerable cache keyed by ruleset id +(`native` holds the engine's own summary at record time). Because the canonical data is the input stream, a later +fix to any ruleset re-scores every previously recorded play without re-recording anything. + +Timestamps are integer microseconds relative to chart zero (the same t = 0 the engine's note timing uses). + +The TypeScript types, serializer, and defensive parser live in +[`packages/player/src/playlog/format.ts`](../packages/player/src/playlog/format.ts) (`@be-music/player/playlog`). +The recommended file suffix is `.bmplay.json`. + +## Recording + +Recording is engine-owned so every host (browser LR2 / beatoraja scenes, and any future TUI adoption) shares one +implementation. `PlayerOptions.onPlaylogRecorded` enables it: the engine snapshots its prepared chart, records +every judged press (`lane-input`) and release (`kitty-state` release) with `pressedAt`-corrected chart-relative +timestamps, counts empty POORs for the native cache, and hands the assembled `BeMusicPlaylog` to the callback right +before `autoPlay` / `manualPlay` resolves — including the ESC (aborted) exit. `PlayerOptions.recordPlaylog` carries +the host-declared settings the engine cannot know (selected gauge, lane-shuffle labels, DP flip, freeform +`native` extras). + +In the browser player: + +- The LR2 / default gameplay scene exposes the recorded log through `PixiGameplayResultData.playlog`. +- The beatoraja gameplay scene exposes it through `PixiBeatorajaGameplayView.getPlaylog()`. +- The demo auto-downloads the log as `-<timestamp>.bmplay.json` when the result scene mounts, controlled by + the Debug Menu's **Auto-save play history** checkbox (ON by default). Play-log options are latched at song start + and cannot change mid-play — the controls are disabled while a song is playing, and the values in effect when + the song started govern that play. +- The Debug Menu's **Play options** folder covers the recorded play settings: auto play, judge windows + (LR2 / beatoraja / IIDX), gauge, Random 1P/2P, DP flip, and auto scratch. Where the LR2 select scene's + PLAY OPTION panel has a counterpart, the two surfaces two-way sync. + +## Live judge-window rulesets + +The shared engine can judge a live play under `'lr2'` (default), `'beatoraja'`, or `'iidx'` windows +(`PlayerOptions.judgeRuleset`). Only the WINDOW WIDTHS switch: LR2 uses the rank-interpolated LR2 windows, +beatoraja scales its SEVENKEYS windows linearly by beatoraja's judgerank (the asymmetric BAD gate is symmetrized +to ±250 ms × judgerank), and IIDX uses the fixed ±16.67/±33.33/±116.67/±250 ms widths. Note selection, empty-POOR +behavior, long-note mechanics, and the groove gauge stay on the engine's LR2-aligned semantics — the playlog +simulators remain the full per-ruleset reproduction. Dynamic `#EXRANKxx` only applies under `'lr2'`. The selected +ruleset is recorded as `play.judgeRuleset` and replays re-apply it automatically. + +## Replay playback + +Dropping a `*.bmplay.json` file onto the browser player starts replay playback when the matching song is loaded +(dropping the song folder together with the log works too — the songs load first). Matching prefers the chart-file +hash (`chart.sha256` — stable across sessions and file moves), then the recorded `play.native.chartPath`, then a +title + artist fallback for older logs. + +Replay re-drives the recorded input stream deterministically inside the shared engine +(`PlayerOptions.replayInputs`): every event fires at its exact chart-relative microsecond timestamp, live lane +input is ignored (ESC / pause / hi-speed keep working), and no new play-log is recorded for the run. Because the +log stores the resolved note arrangement, the chart prepare re-applies the recorded channels onto the freshly +loaded chart (`applyPlaylogArrangement`) instead of re-rolling RANDOM / MIRROR — so shuffled plays replay exactly. +A chart whose `#RANDOM` control flow rolls differently from the recorded run cannot be re-aligned; the replay +fails with a status message instead of playing a mismatched chart. Replay always runs on the LR2 / default +gameplay path regardless of which skin family recorded the log. + +## Re-deriving LR2 / beatoraja / IIDX results + +`simulatePlaylog(playlog, { ruleset })` (from `@be-music/player/playlog`) replays the input stream through one +ruleset; `simulatePlaylogRulesets(playlog)` runs all three. Each ruleset differs in judge windows, note selection, +long-note semantics, empty-POOR behavior, and gauge tables — the constants are documented per source in +[`packages/player/src/playlog/rulesets.ts`](../packages/player/src/playlog/rulesets.ts): + +| aspect | LR2 (`lr2/1`) | beatoraja (`beatoraja/1`) | IIDX (`iidx/1`) | +| --- | --- | --- | --- | +| source | lr2oraja / OpenLR2 | beatoraja master | community measurements (iidx.org) | +| windows (NORMAL-rank) | ±18/±40/±100/±200 ms | ±15/±45/±112.5/late 210 · early 165 ms (7K, judgerank 75 %) | ±16.67/±33.33/±116.67/±250 ms | +| rank scaling | LR2 anchor interpolation, BAD fixed | linear × judgerank, MS fixed | none | +| note selection | Lowest + multi-BAD chain | Combo (default; duration / lowest / score selectable) | Lowest | +| long notes | all LN (deferred single judgment) | per-note LN / CN / HCN | all CN (HCN gauge when mode 3) | +| empty POOR window | early-only 1000 ms | late 150 / early 500 ms (7K) | unmeasured — beatoraja window used | +| money score | `(4·PG + 2·GR + GD) × 50000 / notes` | — | — (abolished in BISTROVER) | +| gauges | lr2oraja LR2 tables (death 2 %, guts < 32 % ×0.6) | beatoraja native tables | iidx.org tables (a-value recovery, ≤ 30 % half damage on HARD) | + +EX-SCORE is PGREAT × 2 + GREAT × 1 everywhere; DJ LEVEL uses the IIDX ninths table. Charge-note rulesets count a +long note's head and tail as two judgment notes (`result.noteCount` reports each ruleset's denominator). + +### Fidelity notes + +- The LR2 ruleset follows lr2oraja, cross-checked against the OpenLR2 transcription; where the two disagree + (missed-POOR threshold for LN heads, HAZARD table) the lr2oraja behavior is used. +- IIDX internals are not public. The judge windows, gauge tables, and DJ LEVEL boundaries are current community + consensus; the empty-POOR window and CN release windows are unmeasured and use beatoraja's values as stand-ins, + and the HCN gauge tick uses a fixed 200 ms interval instead of IIDX's measured 16th-note interval. Expect + close — not bit-exact — reproduction. +- beatoraja niceties not modeled: PMS's one-empty-POOR-per-note rule, the PMS 200 ms charge-release margin, and + per-mode gauge tables other than SEVENKEYS (the 7K gauge constants are used for every mode). +- Because the playlog stores the resolved chart, `#RANDOM` and lane-shuffle differences between players never + affect re-simulation. + +## CLI + +`@be-music/player-tui` ships a second binary, `bms-playlog`: + +```bash +pnpm playlog -- results/Song-2026-08-17T10-00-00-000Z.bmplay.json +``` + +``` +RULESET EX RATE DJ PG GR GD BD PR EPR FAST SLOW COMBO SCORE GAUGE +be-music/native 1180 83.10% AA ... +lr2/1 1180 83.10% AA ... ... 140000 96.0% GROOVE CLEAR +beatoraja/1 1174 82.68% AA ... +iidx/1 1102 77.61% AA ... +``` + +Options: `--ruleset=lr2,beatoraja,iidx|all`, `--gauge=<id>` (ruleset-scoped gauge override), +`--algorithm=combo|duration|lowest|score` (beatoraja note selection), `--json`. + +## Versioning + +`format: "be-music-playlog"`, `version: 1`. Unknown extra fields are ignored on parse so minor additions stay +readable; incompatible changes bump `version`. Ruleset result ids carry their own revision (`lr2/1` etc.) — a +corrected ruleset bumps its revision and simply re-simulates existing files. diff --git a/package.json b/package.json index 990c06ff..43aaa0d4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "bench:aggregate": "tsx --tsconfig tsconfig.typecheck.json scripts/bench/aggregate-results.ts", "bench:compare": "tsx --tsconfig tsconfig.typecheck.json scripts/bench/compare-results.ts", "player": "pnpm --filter @be-music/player-tui run dev", + "playlog": "pnpm --filter @be-music/player-tui run playlog", "player:web": "pnpm --filter @be-music/player-web-demo run dev", "player:diag:keyboard": "pnpm --filter @be-music/player-tui run diag:keyboard", "player:diag:gameplay-input": "pnpm --filter @be-music/player-tui run diag:gameplay-input", @@ -32,21 +33,21 @@ "stringify": "pnpm --filter @be-music/stringifier run dev" }, "devDependencies": { - "@changesets/cli": "^2.31.0", - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260611.2", - "@vitest/coverage-v8": "^4.0.18", - "oxfmt": "^0.54.0", - "oxlint": "^1.69.0", + "@changesets/cli": "^2.31.1", + "@types/node": "^26.2.0", + "@vitest/coverage-v8": "^4.1.10", + "oxfmt": "^0.57.0", + "oxlint": "^1.77.0", "rimraf": "^6.0.1", - "tinybench": "^6.0.2", - "tsdown": "^0.22.2", - "tsx": "^4.22.4", - "vite": "^8.0.16", - "vitest": "^4.0.18" + "tinybench": "^6.1.3", + "tsdown": "^0.22.14", + "tsx": "^4.23.12", + "typescript": "^7.0.2", + "vite": "^8.1.3", + "vitest": "^4.1.10" }, "engines": { - "node": ">=25" + "node": ">=26" }, "packageManager": "pnpm@10.30.3", "pnpm": { diff --git a/packages/audio-renderer/package.json b/packages/audio-renderer/package.json index 12f69479..251b1711 100644 --- a/packages/audio-renderer/package.json +++ b/packages/audio-renderer/package.json @@ -30,7 +30,7 @@ "build:sea": "tsx ../../scripts/build-sea.ts --package audio-renderer", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsx --tsconfig ../../tsconfig.typecheck.json src/cli.ts", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/beatoraja-skin/package.json b/packages/beatoraja-skin/package.json index f138feb8..38abe6e9 100644 --- a/packages/beatoraja-skin/package.json +++ b/packages/beatoraja-skin/package.json @@ -20,7 +20,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/chart/package.json b/packages/chart/package.json index 678245ed..46f33d8c 100644 --- a/packages/chart/package.json +++ b/packages/chart/package.json @@ -20,7 +20,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/editor/package.json b/packages/editor/package.json index 2afbccab..c1f97578 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -24,7 +24,7 @@ "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsx --tsconfig ../../tsconfig.typecheck.json src/cli.ts", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/json/package.json b/packages/json/package.json index 70aa5d4e..0c56281d 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -20,7 +20,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/lr2-skin/CHANGELOG.md b/packages/lr2-skin/CHANGELOG.md index 7969d781..7cfa66a4 100644 --- a/packages/lr2-skin/CHANGELOG.md +++ b/packages/lr2-skin/CHANGELOG.md @@ -1,5 +1,11 @@ # @be-music/lr2-skin +## 0.1.5 + +### Patch Changes + +- Emit declaration-compatible types for the public `LR2_PLAY_VARIANTS` and path-table constants so consumers building under `isolatedDeclarations` no longer fail on the inferred `as const satisfies ...` types. + ## 0.1.4 ### Patch Changes diff --git a/packages/lr2-skin/package.json b/packages/lr2-skin/package.json index 1722a4f9..5c34159a 100644 --- a/packages/lr2-skin/package.json +++ b/packages/lr2-skin/package.json @@ -1,6 +1,6 @@ { "name": "@be-music/lr2-skin", - "version": "0.1.4", + "version": "0.1.5", "description": "Renderer-independent Lunatic Rave 2 skin parser and theme loader for be-music", "license": "MIT", "files": [ @@ -20,7 +20,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/lr2-skin/src/paths.ts b/packages/lr2-skin/src/paths.ts index 987b6bec..dec2ea6d 100644 --- a/packages/lr2-skin/src/paths.ts +++ b/packages/lr2-skin/src/paths.ts @@ -36,7 +36,7 @@ export const LR2_SKIN_INFORMATION_TYPE = { RESULT: 7, KEY_CONFIG: 8, SKIN_SELECT: 9, -} as const satisfies Record<string, number>; +} as const; export type Lr2SkinInformationType = (typeof LR2_SKIN_INFORMATION_TYPE)[keyof typeof LR2_SKIN_INFORMATION_TYPE]; diff --git a/packages/lr2-skin/src/play-skin.ts b/packages/lr2-skin/src/play-skin.ts index bf24fb3b..8d9e2828 100644 --- a/packages/lr2-skin/src/play-skin.ts +++ b/packages/lr2-skin/src/play-skin.ts @@ -5,7 +5,7 @@ import { asLoadedBytes, readFilesIntoBytesMap, type Lr2SkinFileEntry } from './f import type { Lr2SkinInputFile } from './file-lookup.ts'; import { loadLr2SkinFromSourceFiles, type Lr2PlayVariant, type Lr2Skin } from './skin.ts'; -export const LR2_PLAY_VARIANTS = ['7', '14', '10', '5', '9'] as const satisfies readonly Lr2PlayVariant[]; +export const LR2_PLAY_VARIANTS: readonly Lr2PlayVariant[] = ['7', '14', '10', '5', '9']; export type Lr2PlaySkinMap = Partial<Record<Lr2PlayVariant, Lr2Skin>>; diff --git a/packages/parser/package.json b/packages/parser/package.json index 0e67fffc..553de79a 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -24,7 +24,7 @@ "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsx --tsconfig ../../tsconfig.typecheck.json src/cli.ts", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/player-tui/CHANGELOG.md b/packages/player-tui/CHANGELOG.md index 2aa9755d..a6cdf4e1 100644 --- a/packages/player-tui/CHANGELOG.md +++ b/packages/player-tui/CHANGELOG.md @@ -1,5 +1,17 @@ # @be-music/player +## 0.4.0 + +### Minor Changes + +- ab210cb: Add the `bms-playlog` CLI: reads recorded play-log files (`*.bmplay.json`) and re-derives LR2 / beatoraja / IIDX judgments, EX-SCORE, DJ LEVEL, max combo, money score, and groove gauge from the raw input replay, printing a per-ruleset comparison table (or `--json`). + +### Patch Changes + +- Updated dependencies +- Updated dependencies [ab210cb] + - @be-music/player@0.6.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/player-tui/package.json b/packages/player-tui/package.json index 4a1d8930..0a4c679f 100644 --- a/packages/player-tui/package.json +++ b/packages/player-tui/package.json @@ -1,10 +1,11 @@ { "name": "@be-music/player-tui", - "version": "0.3.0", + "version": "0.4.0", "description": "Terminal UI and CLI frontend for the be-music player", "license": "MIT", "bin": { - "bms-player": "dist/cli.js" + "bms-player": "dist/cli.js", + "bms-playlog": "dist/playlog-cli.js" }, "files": [ "dist" @@ -25,9 +26,10 @@ "build:sea": "tsx ../../scripts/build-sea.ts --package player", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsx --tsconfig ../../tsconfig.typecheck.json src/cli.ts", + "playlog": "tsx --tsconfig ../../tsconfig.typecheck.json src/playlog-cli.ts", "diag:keyboard": "tsx --tsconfig ../../tsconfig.typecheck.json src/keyboard-diagnostic.ts", "diag:gameplay-input": "tsx --tsconfig ../../tsconfig.typecheck.json src/gameplay-input-diagnostic.ts", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/player-tui/src/playlog-cli.test.ts b/packages/player-tui/src/playlog-cli.test.ts new file mode 100644 index 00000000..755534f5 --- /dev/null +++ b/packages/player-tui/src/playlog-cli.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, test } from 'vitest'; +import type { BeMusicPlaylog, PlaylogRulesetResult } from '@be-music/player/playlog'; +import { buildPlaylogReport, parsePlaylogCliArgs } from './playlog-cli.ts'; + +function makePlaylog(): BeMusicPlaylog { + return { + format: 'be-music-playlog', + version: 1, + createdAt: '2026-08-17T10:00:00.000Z', + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + title: 'Sample Song', + artist: 'someone', + sourceFormat: 'bms', + laneMode: '7keys', + total: 300, + lnMode: 1, + judgeRank: { percent: 75, sourceRank: 2 }, + noteCount: 10, + notes: [], + }, + inputs: [], + play: { mode: 'manual', autoScratch: false, gauge: 'GROOVE' }, + }; +} + +function makeResult(overrides: Partial<PlaylogRulesetResult> = {}): PlaylogRulesetResult { + return { + ruleset: 'lr2/1', + judge: { pgreat: 8, great: 1, good: 0, bad: 1, poor: 0, emptyPoor: 2 }, + fast: 1, + slow: 0, + exScore: 17, + noteCount: 10, + maxCombo: 9, + score: 170000, + djLevel: 'AA', + gauge: { type: 'GROOVE', final: 84, cleared: true }, + ...overrides, + }; +} + +describe('playlog-cli', () => { + test('parsePlaylogCliArgs: defaults to all rulesets and collects files', () => { + const args = parsePlaylogCliArgs(['a.bmplay.json', 'b.bmplay.json']); + expect(args.files).toEqual(['a.bmplay.json', 'b.bmplay.json']); + expect(args.rulesets).toEqual(['lr2', 'beatoraja', 'iidx']); + expect(args.json).toBe(false); + expect(args.help).toBe(false); + expect(args.gauge).toBeUndefined(); + expect(args.judgeAlgorithm).toBeUndefined(); + }); + + test('parsePlaylogCliArgs: parses ruleset list, gauge, algorithm, and flags', () => { + const args = parsePlaylogCliArgs([ + '--ruleset=lr2,iidx', + '--gauge=hard', + '--algorithm=lowest', + '--json', + 'file.bmplay.json', + ]); + expect(args.rulesets).toEqual(['lr2', 'iidx']); + expect(args.gauge).toBe('HARD'); + expect(args.judgeAlgorithm).toBe('lowest'); + expect(args.json).toBe(true); + expect(args.files).toEqual(['file.bmplay.json']); + }); + + test('parsePlaylogCliArgs: --ruleset=all restores every simulator', () => { + expect(parsePlaylogCliArgs(['--ruleset=all']).rulesets).toEqual(['lr2', 'beatoraja', 'iidx']); + }); + + test('parsePlaylogCliArgs: rejects unknown rulesets, algorithms, and options', () => { + expect(() => parsePlaylogCliArgs(['--ruleset=osu'])).toThrow(/unknown ruleset 'osu'/); + expect(() => parsePlaylogCliArgs(['--algorithm=psychic'])).toThrow(/unknown judge algorithm/); + expect(() => parsePlaylogCliArgs(['--frobnicate'])).toThrow(/unknown option '--frobnicate'/); + }); + + test('parsePlaylogCliArgs: --help and -h set the help flag', () => { + expect(parsePlaylogCliArgs(['--help']).help).toBe(true); + expect(parsePlaylogCliArgs(['-h']).help).toBe(true); + }); + + test('buildPlaylogReport: renders the chart header and one row per result', () => { + const report = buildPlaylogReport('file.bmplay.json', makePlaylog(), [ + { label: 'lr2/1', result: makeResult() }, + { + label: 'iidx/1', + result: makeResult({ + ruleset: 'iidx/1', + score: undefined, + djLevel: 'A', + gauge: { type: 'HARD', final: 0, cleared: false, failedMidPlay: true }, + }), + }, + ]); + expect(report).toContain('=== file.bmplay.json'); + expect(report).toContain('Sample Song / someone [7keys] notes=10 mode=manual gauge=GROOVE'); + expect(report).toContain('@ 2026-08-17T10:00:00.000Z'); + const lines = report.split('\n'); + const lr2Line = lines.find((line) => line.startsWith('lr2/1')); + expect(lr2Line).toBeDefined(); + // EX 17 / 10 notes → 85.00%, money score printed, gauge cleared. + expect(lr2Line).toContain('17'); + expect(lr2Line).toContain('85.00%'); + expect(lr2Line).toContain('170000'); + expect(lr2Line).toContain('84.0% GROOVE CLEAR'); + const iidxLine = lines.find((line) => line.startsWith('iidx/1')); + expect(iidxLine).toContain('0.0% HARD FAILED(0%)'); + // No money score for IIDX. + expect(iidxLine).toContain(' - '); + }); + + test('buildPlaylogReport: marks aborted plays and auto scratch in the header', () => { + const playlog = makePlaylog(); + playlog.play.autoScratch = true; + playlog.play.aborted = true; + const report = buildPlaylogReport('x', playlog, []); + expect(report).toContain('mode=manual+autoscratch'); + expect(report).toContain('(aborted)'); + }); +}); diff --git a/packages/player-tui/src/playlog-cli.ts b/packages/player-tui/src/playlog-cli.ts new file mode 100644 index 00000000..ad7b3385 --- /dev/null +++ b/packages/player-tui/src/playlog-cli.ts @@ -0,0 +1,238 @@ +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isMainThread } from 'node:worker_threads'; +import { + parsePlaylog, + simulatePlaylog, + PLAYLOG_SIMULATOR_RULESETS, + type BeMusicPlaylog, + type BeatorajaJudgeAlgorithm, + type PlaylogRulesetId, + type PlaylogRulesetResult, +} from '@be-music/player/playlog'; + +/** + * `bms-playlog` — re-derives LR2 / beatoraja / IIDX scores from a recorded play-log (`*.bmplay.json`). + * + * The playlog is an input replay: the resolved chart, every raw key press / release, and the play settings. Each + * ruleset simulator replays the same input stream through its own judge windows, note-selection algorithm, + * long-note semantics, and gauge tables — so one play yields the score it would have earned in each player. + */ + +export interface PlaylogCliArgs { + files: string[]; + rulesets: PlaylogRulesetId[]; + gauge?: string; + judgeAlgorithm?: BeatorajaJudgeAlgorithm; + json: boolean; + help: boolean; +} + +const USAGE = `Usage: bms-playlog [options] <file.bmplay.json> [...more files] + +Re-derives LR2 / beatoraja / IIDX results from a recorded be-music play-log. + +Options: + --ruleset=<list> Comma-separated rulesets to simulate: lr2, beatoraja, iidx, all (default: all) + --gauge=<id> Override the simulated gauge (ruleset-scoped id, e.g. GROOVE / NORMAL / HARD / EX-HARD) + --algorithm=<name> beatoraja note-selection algorithm: combo (default) / duration / lowest / score + --json Emit the raw result objects as JSON instead of the text table + --help Show this help +`; + +export function parsePlaylogCliArgs(argv: readonly string[]): PlaylogCliArgs { + const args: PlaylogCliArgs = { files: [], rulesets: [...PLAYLOG_SIMULATOR_RULESETS], json: false, help: false }; + for (const arg of argv) { + if (arg === '--help' || arg === '-h') { + args.help = true; + } else if (arg === '--json') { + args.json = true; + } else if (arg.startsWith('--ruleset=')) { + const value = arg.slice('--ruleset='.length).trim().toLowerCase(); + if (value === 'all' || value === '') { + args.rulesets = [...PLAYLOG_SIMULATOR_RULESETS]; + } else { + const parsed: PlaylogRulesetId[] = []; + for (const entry of value.split(',')) { + const id = entry.trim(); + if (id === 'lr2' || id === 'beatoraja' || id === 'iidx') { + parsed.push(id); + } else if (id.length > 0) { + throw new Error(`unknown ruleset '${id}' (expected lr2 / beatoraja / iidx / all)`); + } + } + args.rulesets = parsed; + } + } else if (arg.startsWith('--gauge=')) { + const value = arg.slice('--gauge='.length).trim().toUpperCase(); + if (value.length > 0) { + args.gauge = value; + } + } else if (arg.startsWith('--algorithm=')) { + const value = arg.slice('--algorithm='.length).trim().toLowerCase(); + if (value === 'combo' || value === 'duration' || value === 'lowest' || value === 'score') { + args.judgeAlgorithm = value; + } else { + throw new Error(`unknown judge algorithm '${value}' (expected combo / duration / lowest / score)`); + } + } else if (arg.startsWith('--')) { + throw new Error(`unknown option '${arg}'`); + } else { + args.files.push(arg); + } + } + return args; +} + +function formatRate(exScore: number, noteCount: number): string { + if (noteCount <= 0) return '-'; + return `${((exScore / (noteCount * 2)) * 100).toFixed(2)}%`; +} + +interface ReportRow { + label: string; + result: PlaylogRulesetResult; +} + +/** Builds the human-readable comparison table for one playlog. */ +export function buildPlaylogReport(fileLabel: string, playlog: BeMusicPlaylog, rows: readonly ReportRow[]): string { + const chart = playlog.chart; + const lines: string[] = []; + const title = [chart.title, chart.subtitle].filter((part) => part && part.length > 0).join(' '); + lines.push(`=== ${fileLabel}`); + lines.push( + `${title || '(untitled)'} / ${chart.artist ?? '-'} [${chart.laneMode}] notes=${chart.noteCount}` + + ` mode=${playlog.play.mode}${playlog.play.autoScratch ? '+autoscratch' : ''} gauge=${playlog.play.gauge}` + + `${playlog.play.aborted === true ? ' (aborted)' : ''}${playlog.createdAt ? ` @ ${playlog.createdAt}` : ''}`, + ); + const header = [ + pad('RULESET', 14), + pad('EX', 6), + pad('RATE', 8), + pad('DJ', 4), + pad('PG', 6), + pad('GR', 6), + pad('GD', 5), + pad('BD', 5), + pad('PR', 5), + pad('EPR', 5), + pad('FAST', 6), + pad('SLOW', 6), + pad('COMBO', 6), + pad('SCORE', 7), + 'GAUGE', + ].join(' '); + lines.push(header); + for (const { label, result } of rows) { + const gauge = `${result.gauge.final.toFixed(1)}% ${result.gauge.type} ${ + result.gauge.cleared ? 'CLEAR' : result.gauge.failedMidPlay === true ? 'FAILED(0%)' : 'FAILED' + }`; + lines.push( + [ + pad(label, 14), + pad(String(result.exScore), 6), + pad(formatRate(result.exScore, result.noteCount), 8), + pad(result.djLevel ?? '-', 4), + pad(String(result.judge.pgreat), 6), + pad(String(result.judge.great), 6), + pad(String(result.judge.good), 5), + pad(String(result.judge.bad), 5), + pad(String(result.judge.poor), 5), + pad(String(result.judge.emptyPoor), 5), + pad(String(result.fast), 6), + pad(String(result.slow), 6), + pad(String(result.maxCombo), 6), + pad(result.score !== undefined ? String(result.score) : '-', 7), + gauge, + ].join(' '), + ); + } + return lines.join('\n'); +} + +function pad(value: string, width: number): string { + return value.length >= width ? value : value + ' '.repeat(width - value.length); +} + +export async function runPlaylogCli(argv: readonly string[] = process.argv.slice(2)): Promise<number> { + let args: PlaylogCliArgs; + try { + args = parsePlaylogCliArgs(argv); + } catch (error) { + process.stderr.write(`${(error as Error).message}\n\n${USAGE}`); + return 2; + } + if (args.help || args.files.length === 0) { + process.stdout.write(USAGE); + return args.help ? 0 : 2; + } + + let failures = 0; + const jsonOutput: Record<string, unknown>[] = []; + for (const file of args.files) { + let playlog: BeMusicPlaylog; + try { + playlog = parsePlaylog(await readFile(file, 'utf8')); + } catch (error) { + process.stderr.write(`${file}: ${(error as Error).message}\n`); + failures += 1; + continue; + } + const rows: ReportRow[] = []; + const native = playlog.results?.native; + if (native) { + rows.push({ label: native.ruleset, result: native }); + } + const results: Record<string, PlaylogRulesetResult> = {}; + for (const ruleset of args.rulesets) { + const result = simulatePlaylog(playlog, { + ruleset, + ...(args.gauge !== undefined ? { gauge: args.gauge } : {}), + ...(args.judgeAlgorithm !== undefined ? { judgeAlgorithm: args.judgeAlgorithm } : {}), + }); + results[ruleset] = result; + rows.push({ label: result.ruleset, result }); + } + if (args.json) { + jsonOutput.push({ file, chart: playlog.chart.title, play: playlog.play, native, results }); + } else { + process.stdout.write(`${buildPlaylogReport(file, playlog, rows)}\n\n`); + } + } + if (args.json) { + process.stdout.write(`${JSON.stringify(jsonOutput, null, 2)}\n`); + } + return failures > 0 ? 1 : 0; +} + +function isCliEntryPoint(): boolean { + if (!isMainThread) { + return false; + } + const entry = process.argv[1]; + if (!entry) { + return false; + } + try { + const moduleUrl = (import.meta as { url?: unknown }).url; + if (typeof moduleUrl === 'string' && moduleUrl.length > 0) { + return resolve(entry) === fileURLToPath(moduleUrl); + } + } catch { + // SEA/CJS bundles may not provide import.meta.url. + } + return resolve(entry) === resolve(process.execPath); +} + +if (isCliEntryPoint()) { + void runPlaylogCli() + .then((code) => { + process.exit(code); + }) + .catch((error) => { + const message = error instanceof Error && error.message ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exit(1); + }); +} diff --git a/packages/player-tui/tsdown.config.ts b/packages/player-tui/tsdown.config.ts index 00cb8536..ab92bf5d 100644 --- a/packages/player-tui/tsdown.config.ts +++ b/packages/player-tui/tsdown.config.ts @@ -9,6 +9,7 @@ export default createPackageTsdownConfig({ entries: { index: 'src/index.ts', cli: 'src/cli.ts', + 'playlog-cli': 'src/playlog-cli.ts', 'bga-video-worker': 'src/bga-video-worker.ts', 'node-gameplay-worker': 'src/node/node-gameplay-worker.ts', 'node-ui-worker': 'src/node/node-ui-worker.ts', diff --git a/packages/player-web-demo/CHANGELOG.md b/packages/player-web-demo/CHANGELOG.md index aa56b6c3..325c5fa9 100644 --- a/packages/player-web-demo/CHANGELOG.md +++ b/packages/player-web-demo/CHANGELOG.md @@ -1,5 +1,15 @@ # @be-music/player-web-demo +## 0.3.2 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies [ab210cb] + - @be-music/lr2-skin@0.1.5 + - @be-music/player-web@0.7.0 + ## 0.3.1 ### Patch Changes diff --git a/packages/player-web-demo/package.json b/packages/player-web-demo/package.json index acb25fa1..da121777 100644 --- a/packages/player-web-demo/package.json +++ b/packages/player-web-demo/package.json @@ -1,6 +1,6 @@ { "name": "@be-music/player-web-demo", - "version": "0.3.1", + "version": "0.3.2", "private": true, "type": "module", "scripts": { @@ -11,7 +11,7 @@ "cf:r2:create": "wrangler r2 bucket create be-music-ffmpeg-core", "cf:r2:push": "pnpm run build:cf && node scripts/sync-ffmpeg-core.mjs --force", "clean": "rimraf dist tsconfig.tsbuildinfo node_modules/.vite", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", @@ -25,10 +25,10 @@ "lil-gui": "^0.21.0" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20260611.1", + "@cloudflare/workers-types": "^5.20260813.1", "ts-ebml": "^3.0.2", - "vite": "^8.0.16", + "vite": "^8.1.3", "vite-plugin-node-polyfills": "^0.28.0", - "wrangler": "^4.100.0" + "wrangler": "^4.107.0" } } diff --git a/packages/player-web-demo/src/main.ts b/packages/player-web-demo/src/main.ts index bda77a2e..1f9d8faf 100644 --- a/packages/player-web-demo/src/main.ts +++ b/packages/player-web-demo/src/main.ts @@ -16,6 +16,7 @@ import { PixiSongSelectView, type BeatorajaSelectSystemSoundPaths, type PixiGameplayResultData, + type PixiPlayOptions, type PixiSongSelectNavigation, } from '@be-music/player-web/scenes'; import { @@ -40,6 +41,7 @@ import { prepareBeatorajaGameplayChart, type PreparedBeatorajaGameplayChart } fr import { BrowserSongCollectionStore, checkBrowserCompat, + computeChartFileSha256, describeSongCollection, loadAssetBytes, readDroppedFiles, @@ -48,7 +50,16 @@ import { type BrowserSongCollection, type BrowserSongEntry, } from '@be-music/player-web/collection'; -import { parseCompressorMode, type CompressorMode } from '@be-music/player-web/runtime'; +import { + downloadBlob, + parseCompressorMode, + parsePlaylog, + resolvePlaylogFilename, + serializePlaylog, + PLAYLOG_FILE_SUFFIX, + type BeMusicPlaylog, + type CompressorMode, +} from '@be-music/player-web/runtime'; import { logger } from '@be-music/player-web'; import { discoverLr2Themes, @@ -357,6 +368,26 @@ class PlayerWebDemoApp { private gui: GUI | undefined; private compressorStageFolder: GUI | undefined; private recordController: Controller | undefined; + /** + * "Auto-save play history" checkbox controller. Held so the play-start path can `disable()` it for the duration + * of a play — the playlog options are latched when a song starts and cannot change mid-song — and the result / + * select paths can `enable()` it again. + */ + private playlogSaveController: Controller | undefined; + /** + * Every "Play options" folder controller (auto play / judge / gauge / random / DP flip / auto scratch). Disabled + * together while a song is playing (`lockPlaylogOptionsForPlay`) and refreshed when the LR2 select panel pushes + * a change (`refreshPlayOptionControllers`). + */ + private readonly playOptionControllers: Controller[] = []; + /** Per-entry chart-file SHA-256 cache (`''` = lookup failed) — see {@link resolveChartSha256}. */ + private readonly chartSha256Cache = new Map<string, string>(); + /** + * `guiState.autoSavePlaylog` as it was when the current play STARTED. The result-screen auto-save decision reads + * this latch, not the live checkbox, so the value in effect at song start governs the whole play (the checkbox is + * disabled during gameplay anyway — this latch is the enforcement for any path that slips through). + */ + private activePlayAutoSavePlaylog = true; /** * Top-level "Skin family" dropdown. Rebuilt via {@link rebuildSkinFamilyPicker} on every theme load / wipe so its * option list reflects which families are actually selectable (LR2 only shows up once an `.lr2skin` has been @@ -415,6 +446,20 @@ class PlayerWebDemoApp { // users coming from those players expect. The dropdown lets users opt into the `'KEEP_SCROLLING'` mode (≈ // beatoraja LANEEFFECT ON) for timing-learning play. judgedNoteDisplay: 'HIDE', + // Play-history auto-save defaults ON: every finished play downloads its play-log (`*.bmplay.json`) when the + // result scene mounts, so the input replay is preserved without the user having to remember anything. The + // Debug Menu checkbox turns the download off for users who don't want per-play files piling up. + autoSavePlaylog: true, + // Play options (playMode / chartOptions / assists / judge / gauge equivalents). Two-way synced with the LR2 + // select scene's PLAY OPTION panel where a counterpart exists; latched at song start and disabled during a + // play. The judge ruleset has no in-skin counterpart — the Debug Menu is its only surface. + judgeRuleset: 'lr2', + gauge: 'GROOVE', + random1P: 'OFF', + random2P: 'OFF', + dpFlip: false, + autoScratch1P: false, + autoScratch2P: false, // Skin-family routing defaults to `'auto'`: beatoraja > LR2 > default, picked per-scene from what's loaded. // The Debug Menu's "Skin family" dropdown lets users force a specific family; LR2 / beatoraja entries appear // in the dropdown only when their theme is loaded (see {@link rebuildSkinFamilyPicker}). @@ -737,6 +782,89 @@ class PlayerWebDemoApp { this.guiState.judgedNoteDisplay = value; this.gameplayView?.setJudgedNoteDisplay(value); }); + // Play options — the playMode / chartOptions / assists / judge / gauge settings that get recorded into the + // play-log. Two-way synced with the LR2 select scene's PLAY OPTION panel where a counterpart exists (the panel + // pushes user edits back through `onPlayOptionsChange`; these controllers push through `setPlayOptions`). All + // of them are latched at song start and DISABLED during a play — mid-song changes never apply. + const playOptionsFolder = gui.addFolder('Play options'); + const trackPlayOption = (controller: Controller): Controller => { + this.playOptionControllers.push(controller); + return controller; + }; + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'autoPlay') + .name('Auto play') + .onChange((value: boolean) => { + this.selectView?.setPlayOptions({ autoPlay: value }); + }), + ); + // Judge-window ruleset — no in-skin counterpart; the Debug Menu is its only surface. Window widths only (note + // selection / empty POOR / LN mechanics / gauge stay LR2-aligned — see docs/playlog.md). + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'judgeRuleset', { LR2: 'lr2', beatoraja: 'beatoraja', IIDX: 'iidx' }) + .name('Judge windows'), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'gauge', ['GROOVE', 'EASY', 'HARD', 'DEATH']) + .name('Gauge') + .onChange((value: DemoGuiState['gauge']) => { + this.selectView?.setPlayOptions({ gauge1P: value }); + }), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'random1P', ['OFF', 'MIRROR', 'RANDOM', 'S-RANDOM', 'SCATTER']) + .name('Random 1P') + .onChange((value: DemoGuiState['random1P']) => { + this.selectView?.setPlayOptions({ random1P: value }); + }), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'random2P', ['OFF', 'MIRROR', 'RANDOM', 'S-RANDOM', 'SCATTER']) + .name('Random 2P') + .onChange((value: DemoGuiState['random2P']) => { + this.selectView?.setPlayOptions({ random2P: value }); + }), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'dpFlip') + .name('DP flip') + .onChange((value: boolean) => { + this.selectView?.setPlayOptions({ dpFlip: value }); + }), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'autoScratch1P') + .name('Auto scratch 1P') + .onChange((value: boolean) => { + this.selectView?.setPlayOptions({ autoScratch1P: value }); + }), + ); + trackPlayOption( + playOptionsFolder + .add(this.guiState, 'autoScratch2P') + .name('Auto scratch 2P') + .onChange((value: boolean) => { + this.selectView?.setPlayOptions({ autoScratch2P: value }); + }), + ); + // Play-history auto-save — ON by default. When enabled, every finished play downloads its play-log + // (`*.bmplay.json`, the raw input replay defined in `@be-music/player/playlog`) as soon as the result scene + // mounts. The file feeds the `bms-playlog` CLI, which re-derives LR2 / beatoraja / IIDX scores from the replay. + // The value is LATCHED at song start (`lockPlaylogOptionsForPlay`) and the controller is disabled while a song + // is playing — playlog options cannot change mid-play. + this.playlogSaveController = gui + .add(this.guiState, 'autoSavePlaylog') + .name('Auto-save play history') + .onChange((value: boolean) => { + this.guiState.autoSavePlaylog = value; + }); this.recordController = gui.add(this.guiState, 'record').name('● Record'); // Screenshot button — captures the Pixi stage at its native size (= the renderer's // current screen dimensions; matches what the user sees in the canvas, downloaded as @@ -830,6 +958,16 @@ class PlayerWebDemoApp { if (files.length === 0) { return; } + // Play-log files ride their own path: they are replay inputs, not chart / theme assets. They are peeled off + // BEFORE the song/theme split so a stray `.bmplay.json` never classifies as a theme candidate — and handled + // AFTER the split's loaders ran, so dropping a song folder together with its play-log loads the song first and + // then starts the replay in one gesture. + const playlogFiles = files.filter((file) => file.name.toLowerCase().endsWith(PLAYLOG_FILE_SUFFIX)); + files = files.filter((file) => !file.name.toLowerCase().endsWith(PLAYLOG_FILE_SUFFIX)); + if (files.length === 0 && playlogFiles.length > 0) { + await this.startPlaylogReplayFromFile(playlogFiles[0]!); + return; + } const { themeFiles, songFiles } = splitDroppedSongAndThemeFiles(files); // `splitDroppedSongAndThemeFiles` routes any non-chart files outside a chart directory into `themeFiles`. That // includes stray `readme.txt` / `info.json` / album-art images sitting at the root of a BMS pack that isn't a real @@ -886,6 +1024,86 @@ class PlayerWebDemoApp { this.setStatus('Theme loaded'); } await this.showSelect(); + if (playlogFiles.length > 0) { + await this.startPlaylogReplayFromFile(playlogFiles[0]!); + } + } + + /** + * Parses a dropped play-log file and starts replay playback when the matching song is loaded. Matching prefers + * the exact `chartPath` the log recorded (`play.native.chartPath`); older logs without it fall back to a + * title + artist match. When no match (or the chart's `#RANDOM` roll differs so the resolved notes can't be + * re-aligned), the failure lands in the status row instead of throwing — a bad drop must never wedge the UI. + */ + private async startPlaylogReplayFromFile(file: File): Promise<void> { + let playlog: BeMusicPlaylog; + try { + playlog = parsePlaylog(await file.text()); + } catch (error) { + dropLog.warn('play-log parse failed', error); + this.setStatus(`Replay: invalid play-log (${(error as Error).message})`); + return; + } + const song = await this.findSongForPlaylog(playlog); + if (!song) { + this.setStatus(`Replay: song not loaded (${playlog.chart.title ?? 'untitled'})`); + return; + } + this.setStatus(`Replaying: ${song.title}`); + try { + // Replay always drives the LR2/default gameplay path — the shared engine owns the playback either way, so + // the recorded run reproduces regardless of which skin family recorded it. + await this.playSong(song, { autoPlay: playlog.play.mode === 'auto', replay: playlog }); + } catch (error) { + gameplayLog.warn('play-log replay failed', error); + this.setStatus(`Replay failed: ${(error as Error).message}`); + await this.showSelect(); + } + } + + /** + * Finds the loaded song a play-log belongs to. Chart-file SHA-256 first (`chart.sha256`), then the recorded + * `chartPath`, then a title + artist heuristic for logs recorded before either rode along. + */ + private async findSongForPlaylog(playlog: BeMusicPlaylog): Promise<BrowserSongEntry | undefined> { + // Content hash first — stable across sessions, machines, and file moves. + const sha256 = playlog.chart.sha256?.toLowerCase(); + if (sha256 !== undefined && sha256.length > 0) { + for (const entry of this.collection.songs) { + if ((await this.resolveChartSha256(entry)) === sha256) { + return entry; + } + } + } + const nativeChartPath = playlog.play.native?.chartPath; + if (typeof nativeChartPath === 'string' && nativeChartPath.length > 0) { + const exact = this.collection.songs.find((entry) => entry.chartPath === nativeChartPath); + if (exact) { + return exact; + } + } + const title = playlog.chart.title; + if (title === undefined || title.length === 0) { + return undefined; + } + return this.collection.songs.find( + (entry) => entry.title === title && (playlog.chart.artist === undefined || entry.artist === playlog.chart.artist), + ); + } + + /** + * SHA-256 (lowercase hex) of a song's source chart file, cached per entry id — the hash feeds both the play-log + * recording (`chart.sha256`) and dropped-log matching, and chart files are small enough that hashing the whole + * library on a dropped log stays cheap. + */ + private async resolveChartSha256(song: BrowserSongEntry): Promise<string | undefined> { + const cached = this.chartSha256Cache.get(song.id); + if (cached !== undefined) { + return cached === '' ? undefined : cached; + } + const hash = await computeChartFileSha256(resolveSongSource(this.collection, song), song.chartPath); + this.chartSha256Cache.set(song.id, hash ?? ''); + return hash; } /** @@ -1298,6 +1516,8 @@ class PlayerWebDemoApp { private async playSongBeatoraja(song: BrowserSongEntry, overrides: { autoPlay?: boolean }): Promise<void> { const bundle = this.beatorajaTheme; if (!bundle) return; + // Chart-file hash for the recorded play-log (matched back on a future play-log drop). + const chartSha256 = await this.resolveChartSha256(song); // **TWO distinct variants** flow into the gameplay scene: // - `skinVariant`: which `play_*` skin file to load. The theme may not ship every // variant (a popular 7-only theme lacks a `play_9` skin, etc.), so this falls back @@ -1487,12 +1707,20 @@ class PlayerWebDemoApp { audio: prep.audio, skinAudio: this.beatorajaSkinAudio, mode: (overrides.autoPlay ?? this.guiState.autoPlay) ? 'auto' : 'manual', - // DP flip — when the user enabled it via the play-options panel AND the chart is a - // DP variant (10 / 14 keys), the chart's lane channels mirror at construction. SP - // charts pass through unchanged because they have no 2P channels to swap with. - // Reads from the LR2 select view's persisted play options (the same surface that - // already drives `dpFlip` on the LR2 path). - dpFlip: this.selectView?.getPlayOptions().dpFlip, + // DP flip — when the user enabled it via the Play options (Debug Menu / LR2 select panel, two-way synced + // into `guiState`) AND the chart is a DP variant (10 / 14 keys), the chart's lane channels mirror at + // construction. SP charts pass through unchanged because they have no 2P channels to swap with. + dpFlip: this.guiState.dpFlip, + // Judge-window ruleset + auto scratch + play-log extras ride the engine-options passthrough — the beatoraja + // scene forwards them onto the shared engine and merges `recordPlaylog` with its own fields. + engineOptions: { + judgeRuleset: this.guiState.judgeRuleset, + autoScratch: this.guiState.autoScratch1P || this.guiState.autoScratch2P, + recordPlaylog: { + gauge: this.guiState.gauge, + ...(chartSha256 !== undefined ? { chartSha256 } : {}), + }, + }, bgaTextures: prep.bga.textures, bgaVideoElements: prep.bga.videoElements, bgaCues: prep.bga.cues, @@ -1513,9 +1741,16 @@ class PlayerWebDemoApp { // `finishBeatorajaGameplayThen` drops the gameplay scene first so the result scene gets a // clean stage; the result scene mounts in the `then` branch. `history` carries the // per-judge score / gauge polyline samples for the result skin's graph elements. + // The play-log is captured HERE (while the gameplay view is guaranteed alive) — the finish + // transition below disposes the scene before the result mounts. + const playlog = this.beatorajaGameplayView?.getPlaylog(); void this.finishBeatorajaGameplayThen(async () => { const mounted = await this.showBeatorajaResult(song, summary, maxCombo, history); if (!mounted) await this.showSelect(); + // Auto-save fires even when the theme ships no result skin — falling back to select + // shouldn't cost the user their replay file. + this.maybeAutoSavePlaylog(playlog); + this.unlockPlaylogOptions(); }); }, onError: (error) => { @@ -1531,6 +1766,7 @@ class PlayerWebDemoApp { this.currentBeatorajaPlayVariant = variant; await this.sceneHost.setScene(this.beatorajaGameplayView); this.setStatus(`Playing (beatoraja): ${song.title}`); + this.lockPlaylogOptionsForPlay(); // Consume the "user pressed Record on the select screen" flag. Same flow as the LR2 // gameplay path — `autoRecordArmed` is set when the user clicks the Record button @@ -2572,6 +2808,7 @@ class PlayerWebDemoApp { private async showSelect(): Promise<void> { this.elements.shell.classList.remove('playing'); + this.unlockPlaylogOptions(); // The `.empty` class drives the centered "Drop BMS folder…" hint. Toggle it off the moment we have charts to show, // and back on after a wipe / failed drop so the hint comes back instead of leaving the user staring at a blank // canvas. @@ -2642,12 +2879,28 @@ class PlayerWebDemoApp { decideBgm: this.decideBgmBytes, systemSounds: this.systemSoundBundle, initialNavigation: this.lastSelectNavigation, - // Seed the in-scene panel's autoPlay value from the cached demo state (carries the last value the user picked - // across re-mounts of the select view). - initialPlayOptions: { autoPlay: this.guiState.autoPlay }, - onPlayOptionsChange: (options: { autoPlay: boolean }) => { - // Cache the last value so it survives a select-view re-mount even though the lil-gui toggle is gone. + // Seed the in-scene panel from the Debug Menu's "Play options" state (two-way sync: the panel's own edits + // come back through `onPlayOptionsChange` below; lil-gui edits push through `setPlayOptions`). + initialPlayOptions: { + autoPlay: this.guiState.autoPlay, + gauge1P: this.guiState.gauge, + random1P: this.guiState.random1P, + random2P: this.guiState.random2P, + dpFlip: this.guiState.dpFlip, + autoScratch1P: this.guiState.autoScratch1P, + autoScratch2P: this.guiState.autoScratch2P, + }, + onPlayOptionsChange: (options: PixiPlayOptions) => { + // Mirror the in-skin PLAY OPTION panel's edits back into the Debug Menu state so both surfaces always + // agree, then repaint the lil-gui controllers. this.guiState.autoPlay = options.autoPlay; + this.guiState.gauge = options.gauge1P; + this.guiState.random1P = options.random1P; + this.guiState.random2P = options.random2P; + this.guiState.dpFlip = options.dpFlip; + this.guiState.autoScratch1P = options.autoScratch1P; + this.guiState.autoScratch2P = options.autoScratch2P; + this.refreshPlayOptionControllers(); }, onSongSelected: (song: BrowserSongEntry) => { // Fire the decide cue first — it plays through the select view's AudioContext which keeps running even after @@ -2740,14 +2993,18 @@ class PlayerWebDemoApp { * Wired up here (rather than inline in `showDecide`) because the same option-marshalling + callback wiring is needed * whether we're going through Decide or the no-decide fast-path. `playSong` shares this construction shape. */ - private preloadGameplay(song: BrowserSongEntry, overrides: { autoPlay?: boolean }): Promise<void> { + private async preloadGameplay(song: BrowserSongEntry, overrides: { autoPlay?: boolean }): Promise<void> { this.recordingFilenameBase = sanitizeFilenameStem(song.title) || `gameplay-${Date.now()}`; // Same family check as `playSong` — picking `'default'` from the Debug Menu strips the LR2 skin even when one // is loaded for this chart. Computing `gameplayFamily` here keeps the decide → gameplay preload path consistent // with the no-decide fast-path. const gameplayFamily = pickActiveFamilyForScene(this.familyDispatchState(), 'gameplay', song); const playSkin = gameplayFamily === 'lr2' ? pickLr2PlaySkin(this.playSkins, song) : undefined; - this.gameplayView = this.buildLr2GameplayView(song, playSkin, overrides); + const chartSha256 = await this.resolveChartSha256(song); + this.gameplayView = this.buildLr2GameplayView(song, playSkin, { + ...overrides, + ...(chartSha256 !== undefined ? { chartSha256 } : {}), + }); return this.gameplayView.prepare(this.sceneHost, song, resolveSongSource(this.collection, song)); } @@ -2765,11 +3022,15 @@ class PlayerWebDemoApp { private buildLr2GameplayView( song: BrowserSongEntry, playSkin: Lr2Skin | undefined, - overrides: { autoPlay?: boolean }, + overrides: { autoPlay?: boolean; replay?: BeMusicPlaylog; chartSha256?: string }, ): PixiGameplayView { const playOptions = this.selectView?.getPlayOptions(); + const replay = overrides.replay; const sharedOptions = { - autoPlay: overrides.autoPlay ?? playOptions?.autoPlay ?? this.guiState.autoPlay, + // The playlog-relevant options (auto play / random / DP flip / auto scratch / gauge / judge) read from + // `guiState` — the Debug Menu's "Play options" folder and the LR2 select panel two-way sync into it, and it + // is the surface that exists on every family (default / beatoraja selects have no LR2 PLAY OPTION panel). + autoPlay: overrides.autoPlay ?? this.guiState.autoPlay, autoPauseOnBlur: this.guiState.autoPauseOnBlur, initialHiSpeed: playOptions?.hiSpeed, bga: playOptions?.bga, @@ -2780,12 +3041,18 @@ class PlayerWebDemoApp { hiddenSudden2P: playOptions?.hiddenSudden2P, shutter: playOptions?.shutter, laneCover: playOptions?.laneCover, - autoScratch1P: playOptions?.autoScratch1P, - autoScratch2P: playOptions?.autoScratch2P, - dpFlip: playOptions?.dpFlip, - random1P: playOptions?.random1P, - random2P: playOptions?.random2P, - gauge: playOptions?.gauge1P, + // Replay playback restores the RECORDED play setup: the log's auto-scratch / gauge / judge ruleset govern + // judging and the gauge readout, and the lane transforms stay off because the view re-applies the recorded + // arrangement directly (see `PixiGameplayViewOptions.replay`). + autoScratch1P: replay !== undefined ? replay.play.autoScratch : this.guiState.autoScratch1P, + autoScratch2P: replay !== undefined ? false : this.guiState.autoScratch2P, + dpFlip: replay !== undefined ? false : this.guiState.dpFlip, + random1P: replay !== undefined ? ('OFF' as const) : this.guiState.random1P, + random2P: replay !== undefined ? ('OFF' as const) : this.guiState.random2P, + gauge: replay !== undefined ? replay.play.gauge : this.guiState.gauge, + ...(replay === undefined ? { judgeRuleset: this.guiState.judgeRuleset } : {}), + ...(overrides.chartSha256 !== undefined ? { chartSha256: overrides.chartSha256 } : {}), + replay, audioCompressor: this.guiState.compressor, audioCompressorMode: this.compressorMode, audioCompressorStages: { @@ -2848,6 +3115,7 @@ class PlayerWebDemoApp { this.decideView?.dispose(); this.decideView = undefined; this.setStatus(`Playing: ${song.title}`); + this.lockPlaylogOptionsForPlay(); this.gameplayView.start(); if (this.autoRecordArmed) { this.autoRecordArmed = false; @@ -2865,12 +3133,16 @@ class PlayerWebDemoApp { } } - private async playSong(song: BrowserSongEntry, overrides: { autoPlay?: boolean } = {}): Promise<void> { + private async playSong( + song: BrowserSongEntry, + overrides: { autoPlay?: boolean; replay?: BeMusicPlaylog } = {}, + ): Promise<void> { // Family dispatch for the gameplay scene. The Debug Menu's "Skin family" pick funnels through here so a user // override of `'lr2'` / `'default'` forces that family even when beatoraja is technically available, while - // `'auto'` keeps the legacy preference (beatoraja → LR2 → default). + // `'auto'` keeps the legacy preference (beatoraja → LR2 → default). Replay playback always stays on the + // LR2/default path — that's where the play-log arrangement remap and `replayInputs` are wired. const gameplayFamily = pickActiveFamilyForScene(this.familyDispatchState(), 'gameplay', song); - if (gameplayFamily === 'beatoraja') { + if (gameplayFamily === 'beatoraja' && overrides.replay === undefined) { await this.playSongBeatoraja(song, overrides); return; } @@ -2892,8 +3164,14 @@ class PlayerWebDemoApp { // is undefined and `PixiGameplayView` otherwise; see `preloadGameplay` for the matching call shape (both feed // the same helper so option marshalling stays in one place). const playSkin = gameplayFamily === 'lr2' ? pickLr2PlaySkin(this.playSkins, song) : undefined; - this.gameplayView = this.buildLr2GameplayView(song, playSkin, overrides); + // Chart-file hash for the recorded play-log (skipped for replays — they record nothing). + const chartSha256 = overrides.replay === undefined ? await this.resolveChartSha256(song) : undefined; + this.gameplayView = this.buildLr2GameplayView(song, playSkin, { + ...overrides, + ...(chartSha256 !== undefined ? { chartSha256 } : {}), + }); this.setStatus(`Playing: ${song.title}`); + this.lockPlaylogOptionsForPlay(); await this.gameplayView.mount(this.sceneHost, song, resolveSongSource(this.collection, song)); // Consume the "user pressed Record on the select screen" flag now that gameplay is mounted — `startRecording` // requires the gameplay AudioContext to exist, which only happens after `mount`. Failing here is non-fatal: the @@ -2954,6 +3232,55 @@ class PlayerWebDemoApp { this.gameplayView?.dispose({ preserveAudioTail: true }); this.gameplayView = undefined; this.setStatus(`Result: ${data.song.title}`); + this.maybeAutoSavePlaylog(data.playlog); + this.unlockPlaylogOptions(); + } + + /** + * Latches the playlog-related GUI options for the play that is about to start and disables their controllers. + * Playlog options are per-play: the value in effect at song start governs the whole play, and mid-song changes + * are rejected by keeping the controls disabled until {@link unlockPlaylogOptions} runs (result mounted or back + * at select). + */ + private lockPlaylogOptionsForPlay(): void { + this.activePlayAutoSavePlaylog = this.guiState.autoSavePlaylog; + this.playlogSaveController?.disable(); + for (const controller of this.playOptionControllers) { + controller.disable(); + } + } + + /** Re-enables the playlog option controllers once no play is in flight. */ + private unlockPlaylogOptions(): void { + this.playlogSaveController?.enable(); + for (const controller of this.playOptionControllers) { + controller.enable(); + } + } + + /** Repaints the "Play options" controllers after an external write (the LR2 select panel's edits). */ + private refreshPlayOptionControllers(): void { + for (const controller of this.playOptionControllers) { + controller.updateDisplay(); + } + } + + /** + * Downloads the play-log (`*.bmplay.json` input replay) when the "Auto-save play history" toggle was on at song + * start (the latched value — the checkbox itself is disabled during a play). Called from both result paths + * (LR2/default `showResult`, beatoraja `onComplete`) the moment the result presentation is up. Failures are + * non-fatal — the result screen must never be blocked by a download hiccup. + */ + private maybeAutoSavePlaylog(playlog: BeMusicPlaylog | undefined): void { + if (!this.activePlayAutoSavePlaylog || playlog === undefined) { + return; + } + try { + const blob = new Blob([serializePlaylog(playlog)], { type: 'application/json' }); + downloadBlob(blob, resolvePlaylogFilename(playlog)); + } catch (error) { + gameplayLog.warn('play-log auto-save failed', error); + } } } diff --git a/packages/player-web-demo/src/types.ts b/packages/player-web-demo/src/types.ts index f1f13c80..05fbe369 100644 --- a/packages/player-web-demo/src/types.ts +++ b/packages/player-web-demo/src/types.ts @@ -101,6 +101,30 @@ export interface DemoGuiState { * Long-note bodies are unaffected — they always persist until the tail crosses the line. */ judgedNoteDisplay: 'KEEP_SCROLLING' | 'HIDE'; + /** + * When true (the default), the play-log (`*.bmplay.json` input replay — see `@be-music/player/playlog`) recorded + * during gameplay is automatically downloaded the moment the result scene mounts, for both the LR2/default and + * beatoraja result paths. Toggled from the Debug Menu's "Auto-save play history" checkbox. + */ + autoSavePlaylog: boolean; + /** + * Judge-window ruleset for the shared engine (`PlayerOptions.judgeRuleset`): LR2 (default) / beatoraja / IIDX. + * Only the window widths switch — note selection, empty POOR, LN mechanics, and the gauge stay LR2-aligned. + * Latched at song start (the Play options folder is disabled during a play) and recorded into the play-log. + */ + judgeRuleset: 'lr2' | 'beatoraja' | 'iidx'; + /** Gauge variant for the next play. Two-way synced with the LR2 select scene's PLAY OPTION panel. */ + gauge: 'GROOVE' | 'HARD' | 'DEATH' | 'EASY'; + /** 1P lane arrangement for the next play. Two-way synced with the LR2 select panel. */ + random1P: 'OFF' | 'MIRROR' | 'RANDOM' | 'S-RANDOM' | 'SCATTER'; + /** 2P lane arrangement for the next play. Two-way synced with the LR2 select panel. */ + random2P: 'OFF' | 'MIRROR' | 'RANDOM' | 'S-RANDOM' | 'SCATTER'; + /** DP FLIP for the next play. Two-way synced with the LR2 select panel. */ + dpFlip: boolean; + /** 1P auto scratch for the next play. Two-way synced with the LR2 select panel. */ + autoScratch1P: boolean; + /** 2P auto scratch for the next play. Two-way synced with the LR2 select panel. */ + autoScratch2P: boolean; /** * Explicit skin-family override picked from the Debug Menu dropdown. `'auto'` (default) keeps the legacy * priority — beatoraja if a beatoraja theme is loaded and covers the chart, otherwise LR2 (which itself falls diff --git a/packages/player-web-demo/vite.config.ts b/packages/player-web-demo/vite.config.ts index 067e905a..b3c11690 100644 --- a/packages/player-web-demo/vite.config.ts +++ b/packages/player-web-demo/vite.config.ts @@ -441,6 +441,10 @@ const workspaceSubpathAliases: WorkspaceAlias[] = [ find: '@be-music/player/judging', replacement: resolve(repositoryDir, 'packages/player/src/judging.ts'), }, + { + find: '@be-music/player/playlog', + replacement: resolve(repositoryDir, 'packages/player/src/playlog/index.ts'), + }, { find: '@be-music/player/state-signals', replacement: resolve(repositoryDir, 'packages/player/src/state-signals.ts'), diff --git a/packages/player-web/CHANGELOG.md b/packages/player-web/CHANGELOG.md index faffaf68..cfc7fb89 100644 --- a/packages/player-web/CHANGELOG.md +++ b/packages/player-web/CHANGELOG.md @@ -1,5 +1,22 @@ # @be-music/player-web +## 0.7.0 + +### Minor Changes + +- ab210cb: Record a play log (`*.bmplay.json` input replay) for every gameplay run: the LR2/default gameplay scene exposes it through `PixiGameplayResultData.playlog`, the beatoraja scene through `PixiBeatorajaGameplayView.getPlaylog()`, and the `@be-music/player-web/runtime` subpath re-exports the playlog serializer helpers (`serializePlaylog`, `parsePlaylog`, `resolvePlaylogFilename`) for hosts. + + The LR2/default gameplay scene also plays a recorded log back: `PixiGameplayViewOptions.replay` re-applies the log's resolved note arrangement onto the freshly prepared chart (`applyPlaylogArrangement` — RANDOM / MIRROR arrangements replay without re-rolling) and feeds the recorded inputs through the engine's deterministic replay path, restoring the log's judge-window ruleset. The `@be-music/player-web/collection` subpath adds `computeChartFileSha256` / `computeSha256Hex` for stamping and matching the playlog's chart-file hash, and both gameplay scenes accept the host-computed hash and judge ruleset for recording. + +### Patch Changes + +- Emit declaration-compatible types for the beatoraja theme's playable-variant constant so consumers building under `isolatedDeclarations` no longer fail on the inferred `as const satisfies ...` type. +- Updated dependencies +- Updated dependencies +- Updated dependencies [ab210cb] + - @be-music/lr2-skin@0.1.5 + - @be-music/player@0.6.0 + ## 0.6.2 ### Patch Changes diff --git a/packages/player-web/package.json b/packages/player-web/package.json index 53407270..0c0c6aca 100644 --- a/packages/player-web/package.json +++ b/packages/player-web/package.json @@ -1,6 +1,6 @@ { "name": "@be-music/player-web", - "version": "0.6.2", + "version": "0.7.0", "description": "Vanilla browser PixiJS core for be-music song selection and gameplay", "license": "MIT", "files": [ @@ -44,7 +44,7 @@ "scripts": { "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/player-web/scripts/exports-cases.ts b/packages/player-web/scripts/exports-cases.ts index 00e5e05a..7ddb41e4 100644 --- a/packages/player-web/scripts/exports-cases.ts +++ b/packages/player-web/scripts/exports-cases.ts @@ -18,6 +18,8 @@ const BENCH_APPEND_BMS_FILE = makeBenchFile('Songs/BenchExtra/main.bms', '#TITLE const BENCH_WAV_FILE = makeBenchFile('Songs/Bench/kick.wav', 'RIFF'); const BENCH_SOURCE = makeBenchSource(); const BENCH_SONG = makeBenchSong(); +const BENCH_PLAYLOG = makeBenchPlaylog(); +const BENCH_PLAYLOG_JSON = JSON.stringify(BENCH_PLAYLOG); const BENCH_PMS_SONG = makeBenchPmsSong(); const BENCH_COLLECTION = { sources: [BENCH_SOURCE], @@ -225,6 +227,16 @@ export function registerPlayerWebCoreExportsCases(define: DefineBenchmarkCase): playerWebCoreApi.computeResultOps(makeResultData(), makeLr2Skin()); }, }); + define('player-web.computeChartFileSha256', { + run: async () => { + await playerWebCoreApi.computeChartFileSha256(BENCH_SOURCE, 'Songs/Bench/main.bms'); + }, + }); + define('player-web.computeSha256Hex', { + run: async () => { + await playerWebCoreApi.computeSha256Hex(BENCH_BYTES); + }, + }); define('player-web.createSkinFamilyRegistry', { run: () => { const registry = playerWebCoreApi.createSkinFamilyRegistry([ @@ -423,6 +435,21 @@ export function registerPlayerWebCoreExportsCases(define: DefineBenchmarkCase): playerWebCoreApi.parseCompressorMode('split'); }, }); + define('player-web.parsePlaylog', { + run: () => { + playerWebCoreApi.parsePlaylog(BENCH_PLAYLOG_JSON); + }, + }); + define('player-web.resolvePlaylogFilename', { + run: () => { + playerWebCoreApi.resolvePlaylogFilename(BENCH_PLAYLOG); + }, + }); + define('player-web.serializePlaylog', { + run: () => { + playerWebCoreApi.serializePlaylog(BENCH_PLAYLOG); + }, + }); define('player-web.pickRecorderMimeType', { run: () => { playerWebCoreApi.pickRecorderMimeType((type) => type === 'video/webm'); @@ -690,6 +717,37 @@ function makeBenchSong(): playerWebCoreApi.BrowserSongEntry { }; } +function makeBenchPlaylog(): playerWebCoreApi.BeMusicPlaylog { + return { + format: 'be-music-playlog', + version: 1, + createdAt: '2026-01-01T00:00:00.000Z', + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + title: 'Bench Song', + sourceFormat: 'bms', + laneMode: '7keys', + total: 300, + lnMode: 1, + judgeRank: { percent: 75, sourceRank: 2 }, + noteCount: 32, + notes: Array.from({ length: 32 }, (_, index) => ({ + id: index, + channel: `1${(index % 7) + 1}`, + type: 'normal' as const, + timeUs: 250_000 * (index + 1), + })), + }, + inputs: Array.from({ length: 64 }, (_, index) => ({ + seq: index, + timeUs: 125_000 * (index + 1), + action: index % 2 === 0 ? ('down' as const) : ('up' as const), + channels: [`1${((index >> 1) % 7) + 1}`], + })), + play: { mode: 'manual', autoScratch: false, gauge: 'GROOVE' }, + }; +} + function makeBenchPmsSong(): playerWebCoreApi.BrowserSongEntry { const chart: BeMusicJson = createEmptyJson('bms'); chart.metadata.title = 'Bench PMS Song'; diff --git a/packages/player-web/src/chart/playlog-arrangement.test.ts b/packages/player-web/src/chart/playlog-arrangement.test.ts new file mode 100644 index 00000000..ac0656c9 --- /dev/null +++ b/packages/player-web/src/chart/playlog-arrangement.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'vitest'; +import type { PlaylogNote } from '@be-music/player/playlog'; +import { applyPlaylogArrangement } from './playlog-arrangement.ts'; + +function note(channel: string, seconds: number, endSeconds?: number) { + return endSeconds === undefined ? { channel, seconds } : { channel, seconds, endSeconds }; +} + +function playlogNote(overrides: Partial<PlaylogNote> & Pick<PlaylogNote, 'channel' | 'timeUs'>): PlaylogNote { + return { id: 0, type: 'normal', ...overrides }; +} + +describe('playlog-arrangement', () => { + test('re-applies a mirrored arrangement onto the original channels', () => { + // Original chart: lane 11 at 1.0s, lane 13 at 2.0s. Recorded (mirrored): 15 at 1.0s, 13 at 2.0s. + const target = { + notes: [note('11', 1), note('13', 2)], + landmineNotes: [], + invisibleNotes: [], + activeFreeZoneChannels: new Set<string>(), + }; + const result = applyPlaylogArrangement( + [playlogNote({ channel: '15', timeUs: 1_000_000 }), playlogNote({ channel: '13', timeUs: 2_000_000 })], + target, + ); + expect(result).toEqual({ ok: true }); + expect(target.notes.map((entry) => entry.channel)).toEqual(['15', '13']); + }); + + test('matches long notes by tail time and keeps mines / invisibles / freezones separate', () => { + const target = { + notes: [note('11', 1, 2), note('12', 1), note('17', 3, 4)], + landmineNotes: [note('13', 1)], + invisibleNotes: [note('14', 1)], + activeFreeZoneChannels: new Set(['17']), + }; + const result = applyPlaylogArrangement( + [ + playlogNote({ channel: '15', type: 'long', timeUs: 1_000_000, endTimeUs: 2_000_000, lnMode: 1 }), + playlogNote({ channel: '11', timeUs: 1_000_000 }), + playlogNote({ channel: '17', type: 'freezone', timeUs: 3_000_000, endTimeUs: 4_000_000 }), + playlogNote({ channel: '12', type: 'mine', timeUs: 1_000_000, damage: 4 }), + playlogNote({ channel: '13', type: 'invisible', timeUs: 1_000_000 }), + ], + target, + ); + expect(result).toEqual({ ok: true }); + // The long note takes the recorded long channel — never the same-time normal note's channel. + expect(target.notes.map((entry) => entry.channel)).toEqual(['15', '11', '17']); + expect(target.landmineNotes[0]!.channel).toBe('12'); + expect(target.invisibleNotes[0]!.channel).toBe('13'); + }); + + test('fails without mutating when the prepared chart has a note the log does not', () => { + const target = { + notes: [note('11', 1), note('12', 1.5)], + landmineNotes: [], + invisibleNotes: [], + activeFreeZoneChannels: new Set<string>(), + }; + const result = applyPlaylogArrangement([playlogNote({ channel: '11', timeUs: 1_000_000 })], target); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.reason).toContain('1500000'); + } + expect(target.notes.map((entry) => entry.channel)).toEqual(['11', '12']); + }); + + test('fails when the log has extra notes the prepared chart does not', () => { + const target = { + notes: [note('11', 1)], + landmineNotes: [], + invisibleNotes: [], + activeFreeZoneChannels: new Set<string>(), + }; + const result = applyPlaylogArrangement( + [playlogNote({ channel: '11', timeUs: 1_000_000 }), playlogNote({ channel: '12', timeUs: 2_000_000 })], + target, + ); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.reason).toContain('unmatched'); + } + }); + + test('chords at the same time consume recorded channels without duplication', () => { + const target = { + notes: [note('11', 1), note('12', 1), note('13', 1)], + landmineNotes: [], + invisibleNotes: [], + activeFreeZoneChannels: new Set<string>(), + }; + const result = applyPlaylogArrangement( + [ + playlogNote({ channel: '13', timeUs: 1_000_000 }), + playlogNote({ channel: '14', timeUs: 1_000_000 }), + playlogNote({ channel: '15', timeUs: 1_000_000 }), + ], + target, + ); + expect(result).toEqual({ ok: true }); + expect(target.notes.map((entry) => entry.channel).sort()).toEqual(['13', '14', '15']); + }); +}); diff --git a/packages/player-web/src/chart/playlog-arrangement.ts b/packages/player-web/src/chart/playlog-arrangement.ts new file mode 100644 index 00000000..8ddf051e --- /dev/null +++ b/packages/player-web/src/chart/playlog-arrangement.ts @@ -0,0 +1,91 @@ +import type { PlaylogNote } from '@be-music/player/playlog'; + +/** + * Re-applies a recorded play-log's note arrangement onto a freshly prepared chart for replay playback. + * + * A playlog stores the RESOLVED chart the player saw — post lane-shuffle (RANDOM / MIRROR / S-RANDOM) and post + * DP-flip. A fresh chart prepare (with those options off) yields the same notes at the same times but with the + * ORIGINAL channels, so replaying against it would judge the inputs on the wrong lanes. Lane transforms never move + * a note in time, only across channels, so the recorded arrangement can be re-applied by matching notes on + * `(kind, timeUs, endTimeUs)` and assigning the recorded channels back — no shuffle seed needed. + * + * Mutates the `channel` field of the given note objects in place (they are the same instances the prepared-chart + * bundle and the engine share). Returns an error string when the chart does not match the playlog — a different + * `#RANDOM` control-flow roll, a different chart file, or an edited chart — in which case the notes are left + * untouched (channels are only written after every bucket matched). + */ +export function applyPlaylogArrangement( + playlogNotes: readonly PlaylogNote[], + target: { + /** Playable notes (freezone included) — `PreparedPlaybackChartData.notes`. */ + notes: Array<{ channel: string; seconds: number; endSeconds?: number }>; + landmineNotes: Array<{ channel: string; seconds: number }>; + invisibleNotes: Array<{ channel: string; seconds: number }>; + activeFreeZoneChannels: ReadonlySet<string>; + }, +): { ok: true } | { ok: false; reason: string } { + const buckets = new Map<string, string[]>(); + for (const note of playlogNotes) { + const key = `${note.type}:${note.timeUs}:${note.endTimeUs ?? ''}`; + const bucket = buckets.get(key); + if (bucket) { + bucket.push(note.channel); + } else { + buckets.set(key, [note.channel]); + } + } + + interface Assignment { + note: { channel: string }; + channel: string; + } + const assignments: Assignment[] = []; + const takeChannel = (key: string): string | undefined => { + const bucket = buckets.get(key); + if (!bucket || bucket.length === 0) { + return undefined; + } + return bucket.shift(); + }; + + for (const note of target.notes) { + const timeUs = secondsToMicroseconds(note.seconds); + const hasTail = + typeof note.endSeconds === 'number' && Number.isFinite(note.endSeconds) && note.endSeconds > note.seconds; + const endTimeUs = hasTail ? secondsToMicroseconds(note.endSeconds!) : undefined; + const kind = target.activeFreeZoneChannels.has(note.channel) ? 'freezone' : hasTail ? 'long' : 'normal'; + const channel = takeChannel(`${kind}:${timeUs}:${endTimeUs ?? ''}`); + if (channel === undefined) { + return { ok: false, reason: `no recorded ${kind} note at ${timeUs}µs` }; + } + assignments.push({ note, channel }); + } + for (const mine of target.landmineNotes) { + const channel = takeChannel(`mine:${secondsToMicroseconds(mine.seconds)}:`); + if (channel === undefined) { + return { ok: false, reason: `no recorded mine at ${secondsToMicroseconds(mine.seconds)}µs` }; + } + assignments.push({ note: mine, channel }); + } + for (const invisible of target.invisibleNotes) { + const channel = takeChannel(`invisible:${secondsToMicroseconds(invisible.seconds)}:`); + if (channel === undefined) { + return { ok: false, reason: `no recorded invisible note at ${secondsToMicroseconds(invisible.seconds)}µs` }; + } + assignments.push({ note: invisible, channel }); + } + for (const [key, bucket] of buckets) { + if (bucket.length > 0) { + return { ok: false, reason: `recorded chart has ${bucket.length} unmatched note(s) at ${key}` }; + } + } + + for (const { note, channel } of assignments) { + note.channel = channel; + } + return { ok: true }; +} + +function secondsToMicroseconds(seconds: number): number { + return Math.round(seconds * 1_000_000); +} diff --git a/packages/player-web/src/collection/chart-hash.test.ts b/packages/player-web/src/collection/chart-hash.test.ts new file mode 100644 index 00000000..7f222a26 --- /dev/null +++ b/packages/player-web/src/collection/chart-hash.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'vitest'; +import type { BrowserSongAssetSource } from './types.ts'; +import { computeChartFileSha256, computeSha256Hex } from './chart-hash.ts'; + +const encoder = new TextEncoder(); + +function makeSource(files: Record<string, Uint8Array>): BrowserSongAssetSource { + return { + id: 'src', + kind: 'directory', + label: 'src', + files: new Map(Object.entries(files)), + }; +} + +describe('chart-hash', () => { + test('computeSha256Hex matches the SHA-256 test vector for "abc"', async () => { + expect(await computeSha256Hex(encoder.encode('abc'))).toBe( + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + ); + }); + + test('computeChartFileSha256 hashes the chart file bytes (case-insensitive path lookup)', async () => { + const bytes = encoder.encode('#TITLE test\n#BPM 120\n'); + const source = makeSource({ 'songs/test.bms': bytes }); + expect(await computeChartFileSha256(source, 'songs/TEST.BMS')).toBe(await computeSha256Hex(bytes)); + }); + + test('computeChartFileSha256 returns undefined for a missing chart or source', async () => { + expect(await computeChartFileSha256(makeSource({}), 'songs/missing.bms')).toBeUndefined(); + expect(await computeChartFileSha256(undefined, 'songs/missing.bms')).toBeUndefined(); + }); +}); diff --git a/packages/player-web/src/collection/chart-hash.ts b/packages/player-web/src/collection/chart-hash.ts new file mode 100644 index 00000000..3da053b2 --- /dev/null +++ b/packages/player-web/src/collection/chart-hash.ts @@ -0,0 +1,40 @@ +import { normalizePath } from '@be-music/utils/core'; +import { loadAssetBytes, lookupBytesCaseInsensitive } from './file-lookup.ts'; +import type { BrowserSongAssetSource } from './types.ts'; + +/** + * SHA-256 (lowercase hex) of a byte buffer via Web Crypto. Shared by the play-log recorder plumbing (stamping + * `chart.sha256` at record time) and the play-log drop matching (hashing loaded charts for comparison). + */ +export async function computeSha256Hex(bytes: Uint8Array): Promise<string> { + const digest = await crypto.subtle.digest('SHA-256', bytes as BufferSource); + let hex = ''; + for (const byte of new Uint8Array(digest)) { + hex += byte.toString(16).padStart(2, '0'); + } + return hex; +} + +/** + * SHA-256 (lowercase hex) of a song entry's source chart FILE bytes, or `undefined` when the chart file cannot be + * located in the asset source (or the runtime lacks Web Crypto). The hash is over the file exactly as dropped — + * the same bytes any other tool would hash — so it is stable across sessions, machines, and `#RANDOM` rolls. + */ +export async function computeChartFileSha256( + source: BrowserSongAssetSource | undefined, + chartPath: string, +): Promise<string | undefined> { + if (!source || typeof crypto === 'undefined' || !crypto.subtle) { + return undefined; + } + const entry = lookupBytesCaseInsensitive(source.files, normalizePath(chartPath)); + const bytes = await loadAssetBytes(entry); + if (!bytes) { + return undefined; + } + try { + return await computeSha256Hex(bytes); + } catch { + return undefined; + } +} diff --git a/packages/player-web/src/collection/index.ts b/packages/player-web/src/collection/index.ts index 02ba8e19..03c528db 100644 --- a/packages/player-web/src/collection/index.ts +++ b/packages/player-web/src/collection/index.ts @@ -8,6 +8,7 @@ * Anything else under `collection/` (the case-insensitive file-lookup helpers) stays internal to the package. */ export * from './collection.ts'; +export * from './chart-hash.ts'; export * from './types.ts'; // Browser-side input helpers — same logical surface (= "getting files INTO the player from the browser"), kept diff --git a/packages/player-web/src/runtime/index.ts b/packages/player-web/src/runtime/index.ts index 9a517266..d558b445 100644 --- a/packages/player-web/src/runtime/index.ts +++ b/packages/player-web/src/runtime/index.ts @@ -10,3 +10,13 @@ export * from './audio-bus.ts'; // Gameplay recorder lives in its own directory but is conceptually a runtime output channel — same scope as the // audio bus that feeds it. export * from '../recording/gameplay-recorder.ts'; + +// Play-log format helpers re-exported for hosts (the demo has no direct `@be-music/player` dependency). The +// gameplay scenes populate a `BeMusicPlaylog` per run; these helpers serialize it for the auto-save download. +export { + serializePlaylog, + parsePlaylog, + resolvePlaylogFilename, + PLAYLOG_FILE_SUFFIX, + type BeMusicPlaylog, +} from '@be-music/player/playlog'; diff --git a/packages/player-web/src/scene/beatoraja/gameplay.ts b/packages/player-web/src/scene/beatoraja/gameplay.ts index 94c726b1..ca1b8ae8 100644 --- a/packages/player-web/src/scene/beatoraja/gameplay.ts +++ b/packages/player-web/src/scene/beatoraja/gameplay.ts @@ -54,6 +54,7 @@ import type { BgaCue } from '../lr2/gameplay-bga.ts'; import type { Texture } from 'pixi.js'; import type { BeatorajaFontCache } from '../../skin/beatoraja/fonts.ts'; import type { PlayerOptions, PlayerSummary } from '@be-music/player/core/engine'; +import type { BeMusicPlaylog } from '@be-music/player/playlog'; import type { PlayerInputSignalBus } from '@be-music/player/core/input-signal-bus'; import type { PlayerStateSignals } from '@be-music/player/state-signals'; import type { PlayerUiFramePayload, PlayerUiSignalBus } from '@be-music/player/core/ui-signal-bus'; @@ -247,6 +248,11 @@ export class PixiBeatorajaGameplayView implements PixiScene { private enginePromise?: Promise<EngineDriverResult>; private engineSettled = false; private exitRequested = false; + /** + * Play-log assembled by the shared engine right before its play promise settles (`onPlaylogRecorded`). + * Hosts read it back through {@link getPlaylog} after `onComplete` fires. + */ + private playlog: BeMusicPlaylog | undefined; private disposed = false; /** * Active canvas + audio recorder when the host has called {@link startRecording}. Tied to @@ -483,7 +489,27 @@ export class PixiBeatorajaGameplayView implements PixiScene { // "9 KEY lane 6+ doesn't accept input, AUTO doesn't paint laser/keybeam/judge popup". // Host-provided `engineOptions.laneModeExtension` (if any) wins so callers explicitly // overriding the inference (rare) still take precedence. - engineOptions: composeBeatorajaEngineOptions(this.options), + engineOptions: { + ...composeBeatorajaEngineOptions(this.options), + // Play-log recording — host-supplied `engineOptions.recordPlaylog` fields (gauge pick, chart hash, ...) + // are preserved; the view adds what it owns: the DP-flip intent it applied to the chart, and the chart + // path in `native` so a dropped play-log can be matched back to its song. + recordPlaylog: { + ...this.options.engineOptions?.recordPlaylog, + ...(this.options.dpFlip !== undefined ? { dpFlip: this.options.dpFlip } : {}), + ...(this.options.chartPath !== undefined + ? { + native: { + ...this.options.engineOptions?.recordPlaylog?.native, + chartPath: this.options.chartPath, + }, + } + : {}), + }, + onPlaylogRecorded: (playlog) => { + this.playlog = playlog; + }, + }, onInputSignalsReady: ({ inputSignals }) => { this.inputSignals = inputSignals; // The engine input bus is up — stamp the `startinput` timer so chrome gated on it (input-active @@ -613,6 +639,15 @@ export class PixiBeatorajaGameplayView implements PixiScene { return this.enginePromise; } + /** + * Play-log the shared engine recorded for this run (resolved chart + raw input replay + play settings), or + * `undefined` while the run is still in flight. Populated right before `onComplete` fires; survives `dispose()` + * so the host can read it after tearing the scene down. See `@be-music/player/playlog`. + */ + getPlaylog(): BeMusicPlaylog | undefined { + return this.playlog; + } + /** * Begin recording the play scene (canvas video + audio bus mix) into a WebM blob. Same * `GameplayRecorder` infrastructure the LR2 gameplay path uses, just bound to the @@ -997,7 +1032,6 @@ export class PixiBeatorajaGameplayView implements PixiScene { }); this.bgaLayer?.update(this.currentFrame.currentSeconds, ctx, this.adapter.isPoorBgaActive()); } - } private renderContextWithSkinAudio(ctx: BeatorajaRenderContext): BeatorajaRenderContext { diff --git a/packages/player-web/src/scene/lr2/gameplay.ts b/packages/player-web/src/scene/lr2/gameplay.ts index d06af6e7..a23fa700 100644 --- a/packages/player-web/src/scene/lr2/gameplay.ts +++ b/packages/player-web/src/scene/lr2/gameplay.ts @@ -27,7 +27,11 @@ import type { ChartPlayVariant } from '@be-music/player/core/lane-layout'; import type { PlayerInputSignalBus } from '@be-music/player/core/input-signal-bus'; import type { PlayerJudgeComboSignalState, PlayerStateSignals } from '@be-music/player/state-signals'; import type { PlayerUiCommand, PlayerUiFramePayload, PlayerUiSignalBus } from '@be-music/player/core/ui-signal-bus'; -import { createGrooveGaugeState, isGrooveGaugeCleared, type GrooveGaugeState } from '@be-music/player/core/groove-gauge'; +import { + createGrooveGaugeState, + isGrooveGaugeCleared, + type GrooveGaugeState, +} from '@be-music/player/core/groove-gauge'; import { DEFAULT_POOR_BGA_DISPLAY_SECONDS } from '@be-music/player/core/bga-timeline'; import { createBeatAtSecondsResolverFromTimingResolver, @@ -36,6 +40,8 @@ import { } from '@be-music/player/core/timeline'; import { createScrollDistanceMapper, type ScrollDistanceMapperLike } from '@be-music/player/core/scroll-distance'; import { type TimedLandmineNote, type TimedPlayableNote } from '@be-music/player/playable-notes'; +import type { BeMusicPlaylog } from '@be-music/player/playlog'; +import { applyPlaylogArrangement } from '../../chart/playlog-arrangement.ts'; import { findFirstIndexAtOrAfter, findFirstIndexNumberAtOrAfter, runWithConcurrency } from '@be-music/utils/core'; import type { BrowserSongAssetSource, BrowserSongEntry } from '../../collection/types.ts'; import { @@ -299,6 +305,11 @@ export interface PixiGameplayResultData { gaugeHistory: GaugeHistorySample[]; /** Per-judge samples of `(progress, exScore)`. Drives `#SRC_SCORECHART`. */ scoreHistory: ScoreHistorySample[]; + /** + * Play-log recorded by the shared engine (resolved chart + raw input replay + play settings). `undefined` for + * legacy paths that finished without the shared engine having produced one. See `@be-music/player/playlog`. + */ + playlog?: BeMusicPlaylog; } export interface PixiGameplayViewOptions { @@ -405,6 +416,24 @@ export interface PixiGameplayViewOptions { random1P?: 'OFF' | 'MIRROR' | 'RANDOM' | 'S-RANDOM' | 'SCATTER'; /** 2P side note arrangement (`#SRC_BUTTON,type=43`). */ random2P?: 'OFF' | 'MIRROR' | 'RANDOM' | 'S-RANDOM' | 'SCATTER'; + /** + * Replay playback: a recorded play-log to re-drive instead of live keyboard input. The chart prepare skips the + * usual DP-flip / lane-shuffle passes and re-applies the RECORDED arrangement (`applyPlaylogArrangement`), the + * engine consumes `playlog.inputs` deterministically (`PlayerOptions.replayInputs`), live lane input is ignored, + * and no new play-log is recorded for the run. `prepare` rejects when the loaded chart does not match the log + * (different `#RANDOM` roll / different chart file). + */ + replay?: BeMusicPlaylog; + /** + * Judge-window ruleset for the shared engine (`PlayerOptions.judgeRuleset`): `'lr2'` (default) / `'beatoraja'` + * / `'iidx'`. Recorded into the play-log; replays re-apply the log's own value instead. + */ + judgeRuleset?: 'lr2' | 'beatoraja' | 'iidx'; + /** + * SHA-256 (lowercase hex) of the source chart file bytes, when the host computed one. Stamped into the recorded + * play-log (`chart.sha256`) so a dropped log can be matched back to its chart by content. + */ + chartSha256?: string; /** * 1P gauge variant (`#SRC_BUTTON,type=40`). Drives both the gauge formula (`createGrooveGaugeState`) and the * gauge-on- red-branch op flags (43 / 45). 2P-side gauge isn't yet separately wired — `createGrooveGaugeState` @@ -841,6 +870,11 @@ export class PixiGameplayView { * hit count rather than the longest unbroken streak. */ private maxCombo = 0; + /** + * Play-log assembled by the shared engine right before its play promise settles (`onPlaylogRecorded`). Snapshotted + * into {@link getResultData} so the result host can offer it as a download. + */ + private playlog: BeMusicPlaylog | undefined; /** * Per-play sampled history of `(progress, gauge%)` pairs. Recorded inside `publishJudge` (the single chokepoint for * every judge event) and seeded with a `(0, initialGauge)` entry on `prepareSong` so the polyline starts from the LR2 @@ -1842,41 +1876,52 @@ export class PixiGameplayView { this.notes = prepared.notes; this.mineNotes = prepared.landmineNotes; this.invisibleNotes = prepared.invisibleNotes; - // DP FLIP — swap 1P / 2P channels in place. Cheap O(n) walk because we already iterate `notes` for sorting; SP - // charts skip every entry (no `2x` channels exist). Mine notes are flipped together so they stay anchored to the - // same visual lane after the flip. - if (this.options.dpFlip) { - for (const note of this.notes) { - note.channel = flipDpChannel(note.channel); - } - for (const mine of this.mineNotes) { - mine.channel = flipDpChannel(mine.channel); + if (this.options.replay !== undefined) { + // Replay playback — the recorded play-log carries the FINAL note arrangement (post-flip, post-shuffle), so + // instead of re-rolling the lane transforms we re-apply the recorded channels onto the freshly prepared + // notes. A mismatch means the loaded chart is not the one the log was recorded against (different `#RANDOM` + // roll or a different file) — surface it as a prepare failure rather than replaying garbage. + const arranged = applyPlaylogArrangement(this.options.replay.chart.notes, prepared); + if (!arranged.ok) { + throw new Error(`play-log replay chart mismatch: ${arranged.reason}`); } - for (const invisible of this.invisibleNotes) { - invisible.channel = flipDpChannel(invisible.channel); + } else { + // DP FLIP — swap 1P / 2P channels in place. Cheap O(n) walk because we already iterate `notes` for sorting; SP + // charts skip every entry (no `2x` channels exist). Mine notes are flipped together so they stay anchored to the + // same visual lane after the flip. + if (this.options.dpFlip) { + for (const note of this.notes) { + note.channel = flipDpChannel(note.channel); + } + for (const mine of this.mineNotes) { + mine.channel = flipDpChannel(mine.channel); + } + for (const invisible of this.invisibleNotes) { + invisible.channel = flipDpChannel(invisible.channel); + } } + // RANDOM / MIRROR / S-RANDOM / SCATTER — shuffle the 1P / 2P keyboard lanes independently. Scratch (channels 16 / + // 26) never moves. Per LR2 convention, the shuffle is drawn at chart-prepare time so a single play session has a + // stable arrangement (F5-restart re-rolls it). Mine channels are included in the same shuffle pass so a mine on + // lane 4 lands wherever the shuffle moved lane 4 — keeping the mine's visual relationship to the surrounding chord + // intact. + applyRandomMode( + this.notes as Array<{ channel: string }>, + '1', + this.options.random1P ?? 'OFF', + Math.random, + this.mineNotes as Array<{ channel: string }>, + this.invisibleNotes as Array<{ channel: string }>, + ); + applyRandomMode( + this.notes as Array<{ channel: string }>, + '2', + this.options.random2P ?? 'OFF', + Math.random, + this.mineNotes as Array<{ channel: string }>, + this.invisibleNotes as Array<{ channel: string }>, + ); } - // RANDOM / MIRROR / S-RANDOM / SCATTER — shuffle the 1P / 2P keyboard lanes independently. Scratch (channels 16 / - // 26) never moves. Per LR2 convention, the shuffle is drawn at chart-prepare time so a single play session has a - // stable arrangement (F5-restart re-rolls it). Mine channels are included in the same shuffle pass so a mine on - // lane 4 lands wherever the shuffle moved lane 4 — keeping the mine's visual relationship to the surrounding chord - // intact. - applyRandomMode( - this.notes as Array<{ channel: string }>, - '1', - this.options.random1P ?? 'OFF', - Math.random, - this.mineNotes as Array<{ channel: string }>, - this.invisibleNotes as Array<{ channel: string }>, - ); - applyRandomMode( - this.notes as Array<{ channel: string }>, - '2', - this.options.random2P ?? 'OFF', - Math.random, - this.mineNotes as Array<{ channel: string }>, - this.invisibleNotes as Array<{ channel: string }>, - ); this.maxLongNoteBeatSpan = this.notes.reduce((max, note) => { if (note.endBeat === undefined) { return max; @@ -3473,6 +3518,7 @@ export class PixiGameplayView { song: this.song, gaugeHistory, scoreHistory, + ...(this.playlog !== undefined ? { playlog: this.playlog } : {}), }; } @@ -5572,6 +5618,42 @@ export class PixiGameplayView { // structurally impossible because there is only one extract. preparedChart: this.preparedChart, signal: (this.sharedEngineAbortController = new AbortController()).signal, + ...(this.options.replay !== undefined + ? { + // Replay playback — feed the recorded input stream; the engine ignores live lane input, and the run + // records no new play-log (replaying a replay would only duplicate the file). Auto scratch, the + // judge ruleset, and the debug judge-window override are restored from the log so the judging setup + // matches the recording. + replayInputs: this.options.replay.inputs, + autoScratch: this.options.replay.play.autoScratch, + judgeRuleset: this.options.replay.play.judgeRuleset ?? 'lr2', + ...(this.options.replay.play.judgeWindowOverrideMs !== undefined + ? { judgeWindowMs: this.options.replay.play.judgeWindowOverrideMs } + : {}), + } + : { + ...(this.options.judgeRuleset !== undefined ? { judgeRuleset: this.options.judgeRuleset } : {}), + // Play-log recording: the engine snapshots the resolved (post-shuffle) chart and the raw input + // replay, then hands the assembled log here right before the play promise settles. The host reads it + // back through `getResultData().playlog` for the result screen's auto-save. + recordPlaylog: { + gauge: this.options.gauge, + ...(this.options.chartSha256 !== undefined ? { chartSha256: this.options.chartSha256 } : {}), + ...(this.options.random1P !== undefined || this.options.random2P !== undefined + ? { + randomLane: { + ...(this.options.random1P !== undefined ? { p1: this.options.random1P } : {}), + ...(this.options.random2P !== undefined ? { p2: this.options.random2P } : {}), + }, + } + : {}), + ...(this.options.dpFlip !== undefined ? { dpFlip: this.options.dpFlip } : {}), + ...(this.song?.chartPath !== undefined ? { native: { chartPath: this.song.chartPath } } : {}), + }, + onPlaylogRecorded: (playlog) => { + this.playlog = playlog; + }, + }), }, }) .then((summary) => { @@ -5836,13 +5918,16 @@ export class PixiGameplayView { this.applyFinalComboSummary(finalSummary); } const result = this.getResultData(); - this.beginExitSequence(() => { - if (this.options.onChartFinished && result) { - this.options.onChartFinished(result); - return; - } - this.options.onExit?.(); - }, { fadeAudio: false }); + this.beginExitSequence( + () => { + if (this.options.onChartFinished && result) { + this.options.onChartFinished(result); + return; + } + this.options.onExit?.(); + }, + { fadeAudio: false }, + ); }, delayMs); } } diff --git a/packages/player-web/src/skin/beatoraja/theme.ts b/packages/player-web/src/skin/beatoraja/theme.ts index e47f4987..b78cd6ff 100644 --- a/packages/player-web/src/skin/beatoraja/theme.ts +++ b/packages/player-web/src/skin/beatoraja/theme.ts @@ -95,7 +95,7 @@ export function summarizeBeatorajaPlaySkins(playSkins: BeatorajaPlaySkinMap, sep * If the dropped theme only ships a 24-key skin, the host should fall back to the LR2 theme for gameplay. This * matches how `pickLr2PlaySkin` chains through fallbacks for missing variants. */ -const BEATORAJA_PLAYABLE_VARIANTS = ['7', '5', '9', '10', '14'] as const satisfies ReadonlyArray<BeatorajaPlayVariant>; +const BEATORAJA_PLAYABLE_VARIANTS = ['7', '5', '9', '10', '14'] as const; export type BeatorajaPlayableVariant = (typeof BEATORAJA_PLAYABLE_VARIANTS)[number]; /** diff --git a/packages/player/CHANGELOG.md b/packages/player/CHANGELOG.md index fd300190..c5b8a354 100644 --- a/packages/player/CHANGELOG.md +++ b/packages/player/CHANGELOG.md @@ -1,5 +1,17 @@ # @be-music/player +## 0.6.0 + +### Minor Changes + +- ab210cb: Add the `@be-music/player/playlog` subpath: a play-history ("playlog") format that records the resolved chart, the raw key press/release stream, and the play settings as an input replay, plus LR2 / beatoraja / IIDX ruleset simulators (`simulatePlaylog`) that re-derive judgments, EX-SCORE, max combo, money score, and groove gauge from the same recorded inputs. + + New `PlayerOptions.onPlaylogRecorded` / `PlayerOptions.recordPlaylog` enable engine-side recording for both `manualPlay` and `autoPlay` (including ESC-aborted runs), and `PlayerOptions.replayInputs` re-drives a recorded input stream deterministically for replay playback (live lane input is ignored while a replay is active). `PlayerOptions.judgeRuleset` switches the live judge windows between LR2 (default), beatoraja, and IIDX — recorded as `play.judgeRuleset` so replays re-apply the same windows — and the playlog stamps the source chart file's SHA-256 (`chart.sha256`) when the host supplies one. `ScoreTracker` now latches `maxCombo`, `resolveJudgeRankPercent` exposes the chart's initial judgerank percent, and the landmine gauge-damage rule moved to the shared `core/landmine.ts` helper. + +### Patch Changes + +- Bump the `node-web-audio-api` runtime dependency from `2.0.0` to `2.2.0`. + ## 0.5.0 ### Minor Changes diff --git a/packages/player/package.json b/packages/player/package.json index 63d25c7e..aad9cdd6 100644 --- a/packages/player/package.json +++ b/packages/player/package.json @@ -1,6 +1,6 @@ { "name": "@be-music/player", - "version": "0.5.0", + "version": "0.6.0", "description": "Core playback engine and shared gameplay helpers for be-music", "license": "MIT", "files": [ @@ -20,6 +20,11 @@ "source": "./src/playable-notes.ts", "default": "./dist/playable-notes.js" }, + "./playlog": { + "types": "./dist/playlog.d.ts", + "source": "./src/playlog/index.ts", + "default": "./dist/playlog.js" + }, "./audio-sink": { "types": "./dist/audio-sink.d.ts", "source": "./src/audio-sink.ts", @@ -110,7 +115,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", @@ -124,6 +129,6 @@ "@be-music/parser": "workspace:*", "@be-music/utils": "workspace:*", "alien-signals": "^3.2.1", - "node-web-audio-api": "^2.0.0" + "node-web-audio-api": "^2.2.0" } } diff --git a/packages/player/scripts/exports-cases.ts b/packages/player/scripts/exports-cases.ts index 8f9c7d30..7e7429bb 100644 --- a/packages/player/scripts/exports-cases.ts +++ b/packages/player/scripts/exports-cases.ts @@ -1,6 +1,8 @@ import * as playerApi from '@be-music/player'; import * as judgingApi from '@be-music/player/judging'; +import * as playlogApi from '@be-music/player/playlog'; import type { PlayerOptions } from '@be-music/player'; +import type { BeMusicPlaylog } from '@be-music/player/playlog'; import type { DefineBenchmarkCase } from '../../../scripts/bench/exports.types.ts'; const BENCH_PLAYER_OPTIONS: PlayerOptions = { @@ -127,8 +129,57 @@ export function registerPlayerExportsCases(define: DefineBenchmarkCase): void { new playerApi.PlayerInterruptedError('escape'); }, }); + define('player.playlog.serializePlaylog', { + run: () => { + playlogApi.serializePlaylog(BENCH_PLAYLOG); + }, + }); + define('player.playlog.parsePlaylog', { + run: () => { + playlogApi.parsePlaylog(BENCH_PLAYLOG_JSON); + }, + }); + define('player.playlog.simulatePlaylog', { + run: () => { + playlogApi.simulatePlaylog(BENCH_PLAYLOG, { ruleset: 'lr2' }); + }, + }); + define('player.playlog.simulatePlaylogRulesets', { + run: () => { + playlogApi.simulatePlaylogRulesets(BENCH_PLAYLOG); + }, + }); } +const BENCH_PLAYLOG: BeMusicPlaylog = { + format: 'be-music-playlog', + version: 1, + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + title: 'bench', + sourceFormat: 'bms', + laneMode: '7keys', + total: 300, + lnMode: 1, + judgeRank: { percent: 75, sourceRank: 2 }, + noteCount: 64, + notes: Array.from({ length: 64 }, (_, index) => ({ + id: index, + channel: `1${(index % 7) + 1}`, + type: 'normal' as const, + timeUs: 250_000 * (index + 1), + })), + }, + inputs: Array.from({ length: 128 }, (_, index) => ({ + seq: index, + timeUs: 125_000 * (index + 1) + (index % 2 === 0 ? 5_000 : 0), + action: index % 2 === 0 ? ('down' as const) : ('up' as const), + channels: [`1${((index >> 1) % 7) + 1}`], + })), + play: { mode: 'manual', autoScratch: false, gauge: 'GROOVE' }, +}; +const BENCH_PLAYLOG_JSON = JSON.stringify(BENCH_PLAYLOG); + async function runSilently<T>(task: () => Promise<T>): Promise<T> { const stdout = process.stdout as NodeJS.WriteStream & { write: typeof process.stdout.write }; const stderr = process.stderr as NodeJS.WriteStream & { write: typeof process.stderr.write }; diff --git a/packages/player/src/core/engine.ts b/packages/player/src/core/engine.ts index f0e95796..1b629ab3 100644 --- a/packages/player/src/core/engine.ts +++ b/packages/player/src/core/engine.ts @@ -76,7 +76,15 @@ import { type JudgeKind, } from './scoring.ts'; import { type GrooveGaugeJudgeKind, type GrooveGaugeType } from './groove-gauge.ts'; -import { resolveBmsJudgeWindowsMsForExRankValue, resolveJudgeWindowsMs } from './judge-window.ts'; +import { resolveLandmineGaugeEffect } from './landmine.ts'; +import { + resolveBmsJudgeWindowsMsForExRankValue, + resolveJudgeWindowsMs, + resolveJudgeWindowsMsForRuleset, + type JudgeWindowRuleset, +} from './judge-window.ts'; +import { createPlaylogRecorder, type PlaylogRecordingOptions } from '../playlog/recorder.ts'; +import type { BeMusicPlaylog, PlaylogInputEvent } from '../playlog/format.ts'; import { createBeatAtSecondsResolverFromTimingResolver, createBpmTimeline, @@ -244,6 +252,40 @@ export interface PlayerOptions { onResolvedChart?: (json: BeMusicJson) => void; onLog?: (entry: LogEntry) => void; writeOutput?: (text: string) => void; + /** + * Host-declared play settings merged into the recorded play-log (`gauge`, `randomLane`, `dpFlip`, `native`). + * The engine itself knows mode / auto-scratch / judge-window override; everything host-side (which gauge the + * player picked, which lane shuffle produced `preparedChart`, ...) arrives through this bag. Only meaningful + * together with {@link onPlaylogRecorded}. + */ + recordPlaylog?: PlaylogRecordingOptions; + /** + * Enables play-log recording: when set, the engine snapshots the resolved chart it actually played + * (post-`#RANDOM`, post lane-shuffle via `preparedChart`), records every judged key press / release with + * chart-relative timestamps, and hands the assembled {@link BeMusicPlaylog} here right before `autoPlay` / + * `manualPlay` resolves — including the ESC (aborted) exit. The playlog's `results.native` caches this run's + * engine summary; see `@be-music/player/playlog` for the format and the LR2 / beatoraja / IIDX re-simulation + * tools. + */ + onPlaylogRecorded?: (playlog: BeMusicPlaylog) => void; + /** + * Judge-window ruleset for manual play: `'lr2'` (default — the engine's LR2-aligned windows), `'beatoraja'` + * (SEVENKEYS windows scaled by beatoraja's judgerank), or `'iidx'` (fixed ±16.67/±33.33/±116.67/±250 ms). + * Only the WINDOW WIDTHS switch — note selection, empty-POOR, long-note mechanics, and the gauge stay on the + * engine's LR2-aligned semantics (the playlog simulators are the full per-ruleset reproduction). Dynamic + * `#EXRANKxx` changes are an LR2 concept and only apply under `'lr2'`. Recorded into the playlog + * (`play.judgeRuleset`) so replays re-apply the same windows. + */ + judgeRuleset?: JudgeWindowRuleset; + /** + * Replay playback: a recorded play-log input stream (`playlog.inputs`) `manualPlay` re-drives DETERMINISTICALLY. + * Each event fires at its exact chart-relative microsecond timestamp (no wall-clock jitter — the judge timestamp + * is the recorded one), so replaying a log against the same resolved chart reproduces the original judgments. + * While a replay is active, live lane / kitty input commands are ignored; pause, high-speed, and interrupt + * commands keep working. The caller is responsible for mounting the SAME resolved chart the log was recorded + * against (`preparedChart`, or a chart remapped to the log's note arrangement). + */ + replayInputs?: readonly PlaylogInputEvent[]; } export interface PlayerSummary { @@ -378,8 +420,6 @@ interface OutputDynamicsConfig { } const LANDMINE_EXPLOSION_SAMPLE_KEY = '00'; -const DEFAULT_LANDMINE_GAUGE_DAMAGE = 4; -const BASE36_OBJECT_KEY_PATTERN = /^[0-9A-Z]{2}$/; interface PlaybackClock { nowMs: () => number; @@ -868,54 +908,8 @@ function resolveLandmineExplosionEvent( }; } -function resolveLandmineGaugeEffect( - landmineEvent: Pick<BeMusicEvent, 'value' | 'bmson'>, - base: 36 | 62 = 36, -): { - objectValue: string; - damage: number; - gaugeDelta: number; -} { - // Mine damage encodes the value in base-36 regardless of the chart's `#BASE` setting (the damage encoding is a - // chart-format constant, not an indexed-resource lookup), so the ID is normalized under the chart's base only to - // keep the returned `objectValue` in sync with the rest of the resource-key reporting. LR2 and beatoraja both - // interpret the value DIRECTLY as the gauge-damage percentage (losak's LR2 mine writeup; jbms-parser passes the raw - // base-36 value into `MineNote`) — the nanasi-era `value / 2` rule in hitkey's memo is a different lineage and is - // NOT what LR2 does. `ZZ` (= 1295) therefore wipes any gauge: survival gauges die instantly, GROOVE / EASY hit - // their 2 % floor. - const objectValue = normalizeObjectKey(landmineEvent.value, base); - // bmson `key_channels[].notes[].damage` is an explicit per-mine gauge percentage; when present it wins over the BMS - // `value / 2` rule because the event value there is the WAV slot, not a damage encoding. `damage: 0` is a valid - // authored value (a no-damage decoration mine), so the guard checks finiteness rather than truthiness. - const bmsonDamage = landmineEvent.bmson?.damage; - if (typeof bmsonDamage === 'number' && Number.isFinite(bmsonDamage) && bmsonDamage >= 0) { - return { - objectValue, - damage: bmsonDamage, - gaugeDelta: -bmsonDamage, - }; - } - if (!BASE36_OBJECT_KEY_PATTERN.test(objectValue)) { - return { - objectValue, - damage: DEFAULT_LANDMINE_GAUGE_DAMAGE, - gaugeDelta: -DEFAULT_LANDMINE_GAUGE_DAMAGE, - }; - } - const parsedDamage = Number.parseInt(objectValue, 36); - if (!Number.isFinite(parsedDamage) || parsedDamage <= 0) { - return { - objectValue, - damage: DEFAULT_LANDMINE_GAUGE_DAMAGE, - gaugeDelta: -DEFAULT_LANDMINE_GAUGE_DAMAGE, - }; - } - return { - objectValue, - damage: parsedDamage, - gaugeDelta: -parsedDamage, - }; -} +// Mine gauge-damage resolution lives in `core/landmine.ts` so the play-log recorder / simulators share the +// exact same value interpretation. Re-imported here for the manual landmine hit path. function writeSampleStopEventLog( writeOutput: (text: string) => void, @@ -1815,6 +1809,27 @@ export async function autoPlay(json: BeMusicJson, options: PlayerOptions = {}): const keyMap = new Map(laneBindings.map((binding) => [binding.channel, binding.keyLabel])); const { summary, applyGaugeJudge } = createInitialPlayerSummary(scorableNotes.length, resolvedJson.metadata.total); const scoreTracker = createScoreTracker(); + // AUTO plays never have manual inputs, but recording still snapshots the resolved chart + play settings so an + // auto run produces a structurally complete playlog (simulators treat an empty input stream as all-miss; the + // cached native result carries the actual AUTO outcome). + const playlogRecorder = options.onPlaylogRecorded + ? (() => { + const { chartSha256, ...hostPlaySettings } = options.recordPlaylog ?? {}; + return createPlaylogRecorder({ + json: resolvedJson, + chart: playbackChart, + chartSha256, + dynamicJudgeRankChanges: collectDynamicBmsJudgeRankChanges(resolvedJson, timingResolver), + play: { + mode: 'auto', + autoScratch: false, + judgeWindowOverrideMs: options.judgeWindowMs, + judgeRuleset: options.judgeRuleset, + ...hostPlaySettings, + }, + }); + })() + : undefined; let combo = 0; let interruptedReason: PlayerInterruptReason | undefined; let highSpeed = resolveHighSpeedMultiplier(options.highSpeed); @@ -2339,6 +2354,15 @@ export async function autoPlay(json: BeMusicJson, options: PlayerOptions = {}): summary, }); } + if (playlogRecorder) { + options.onPlaylogRecorded?.( + playlogRecorder.finalize({ + summary, + maxCombo: scoreTracker.maxCombo, + aborted: interruptedReason === 'escape', + }), + ); + } writeOutput(renderSummary(summary)); return summary; } @@ -2354,11 +2378,15 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) const inferBmsLnTypeWhenMissing = Boolean(options.inferBmsLnTypeWhenMissing); const autoScratchEnabled = options.autoScratch === true; const speed = options.speed ?? 1; - let judgeWindows = resolveJudgeWindowsMs(resolvedJson, options.judgeWindowMs); + const judgeRuleset: JudgeWindowRuleset = options.judgeRuleset ?? 'lr2'; + let judgeWindows = resolveJudgeWindowsMsForRuleset(resolvedJson, judgeRuleset, options.judgeWindowMs); let badWindowMs = judgeWindows.bad; let badWindowSeconds = badWindowMs / 1000; const timingResolver = createTimingResolver(resolvedJson); - const dynamicJudgeRankChanges = collectDynamicBmsJudgeRankChanges(resolvedJson, timingResolver); + // Dynamic `#EXRANKxx` is an LR2 concept — beatoraja ignores it and IIDX has no BMS rank axis at all, so the + // non-LR2 rulesets keep their initial windows for the whole chart. + const dynamicJudgeRankChanges = + judgeRuleset === 'lr2' ? collectDynamicBmsJudgeRankChanges(resolvedJson, timingResolver) : []; const realtimeAudioVolumeEvents = collectRealtimeAudioVolumeEvents(resolvedJson, timingResolver); let dynamicJudgeRankCursor = 0; let maxBadWindowMs = badWindowMs; @@ -2419,6 +2447,24 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) resolvedJson.metadata.total, ); const scoreTracker = createScoreTracker(); + const playlogRecorder = options.onPlaylogRecorded + ? (() => { + const { chartSha256, ...hostPlaySettings } = options.recordPlaylog ?? {}; + return createPlaylogRecorder({ + json: resolvedJson, + chart: playbackChart, + chartSha256, + dynamicJudgeRankChanges, + play: { + mode: 'manual', + autoScratch: autoScratchEnabled, + judgeWindowOverrideMs: options.judgeWindowMs, + judgeRuleset: options.judgeRuleset, + ...hostPlaySettings, + }, + }); + })() + : undefined; let combo = 0; let highSpeed = resolveHighSpeedMultiplier(options.highSpeed); const stateSignals = createPlayerStateSignals(highSpeed); @@ -3073,6 +3119,21 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) if (candidateChannels.size === 0) { return; } + handleLaneInputChannels(candidateChannels, tokens, nowMs, nowSec); + }; + + // Channel-direct core of the lane press handling. Live input goes through `handleMappedInputTokens` (token → + // channel resolution); replay playback calls this directly with the recorded channel set and the recorded + // chart-relative timestamp. + const handleLaneInputChannels = ( + candidateChannels: ReadonlySet<string>, + tokens: readonly string[], + nowMs: number, + nowSec: number, + ): void => { + // Play-log press event — recorded BEFORE any judging so the log stays a raw input replay (recordInput copies + // the shared channel-buffer synchronously). + playlogRecorder?.recordInput('down', nowSec, tokens, candidateChannels); if (uiEnabled) { for (const mappedChannel of candidateChannels) { @@ -3141,6 +3202,7 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) // DEATH -10 — see `applyGrooveGaugeJudge('EMPTY_POOR')`) and fire the POOR BGA, but DO NOT break combo or // increment `summary.poor`. Repeatable per note (LR2's MissCondition.ALWAYS). applyLoggedGaugeJudge(nowSec, 'EMPTY_POOR', 'empty-poor'); + playlogRecorder?.recordEmptyPoor(); uiSignals.pushCommand({ kind: 'trigger-poor-bga', seconds: nowSec }); if (!uiEnabled) { writeRuntimeEventLog(writeOutput, 'judge', [ @@ -3278,6 +3340,11 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) }); }, onUnhandledCommand: (command) => { + // Replay playback drives the lanes from the recorded input stream — live lane / key-state input must not + // interleave with it (pause / high-speed / interrupt still arrive through the standard command path). + if (options.replayInputs !== undefined && (command.kind === 'kitty-state' || command.kind === 'lane-input')) { + return; + } if (command.kind === 'kitty-state') { if (!uiEnabled) { if (command.pressTokens.length > 0) { @@ -3310,6 +3377,15 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) } } const releasedChannels = resolveMappedInputChannels(command.releaseTokens); + if (playlogRecorder && releasedChannels.size > 0) { + const releaseNowMs = resolveJudgeNowMsFromPressedAt(playbackClock.nowMs(), command.pressedAt); + playlogRecorder.recordInput( + 'up', + elapsedMsToGameSeconds(releaseNowMs, speed), + command.releaseTokens, + releasedChannels, + ); + } for (const channel of releasedChannels) { activeKittyPressedChannels.delete(channel); if (uiEnabled) { @@ -3364,6 +3440,57 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) }); }; + // Replay playback — recorded play-log inputs re-driven at their exact chart-relative timestamps. Events are + // processed at each tick boundary but judged with THEIR OWN chart seconds, so the replayed judgments are + // deterministic and independent of tick timing. Presses maintain `activeKittyPressedChannels` so long-note holds + // work exactly like the recorded run's key-state stream did. + const replayEvents = + options.replayInputs !== undefined && options.replayInputs.length > 0 + ? [...options.replayInputs].sort((left, right) => left.timeUs - right.timeUs || left.seq - right.seq) + : undefined; + let replayCursor = 0; + const processReplayEventsUntil = (untilSec: number): void => { + if (!replayEvents) return; + while (replayCursor < replayEvents.length) { + const event = replayEvents[replayCursor]!; + const eventSec = event.timeUs / 1_000_000; + if (eventSec > untilSec) break; + replayCursor += 1; + const channels = new Set(event.channels); + if (autoScratchEnabled) { + for (const channel of channels) { + if (scratchPlayableChannels.has(channel)) { + channels.delete(channel); + } + } + } + if (channels.size === 0) continue; + const eventMs = (eventSec * 1000) / speed; + if (event.action === 'down') { + for (const channel of channels) { + activeKittyPressedChannels.add(channel); + if (uiEnabled) { + uiSignals.pushCommand({ kind: 'press-lane', channel }); + } + } + playbackEventTracer.flushUntil(eventSec); + handleLaneInputChannels(channels, event.tokens ?? [], eventMs, eventSec); + } else { + // Mirror the live kitty-release recording so a replayed run re-records an equivalent playlog. + playlogRecorder?.recordInput('up', eventSec, event.tokens ?? [], channels); + for (const channel of channels) { + activeKittyPressedChannels.delete(channel); + if (uiEnabled) { + uiSignals.pushCommand({ kind: 'release-lane', channel }); + } + if (activeLongNotesByChannel.has(channel)) { + longHoldUntilMsByChannel.set(channel, eventMs); + } + } + } + } + }; + try { while (playbackClock.nowMs() < horizon) { consumeInputCommands(); @@ -3383,6 +3510,7 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) const nowSec = elapsedMsToGameSeconds(nowMs, speed); const scheduledSec = elapsedMsToGameSeconds(scheduledMs, speed); const nowBeat = beatAtSeconds(nowSec); + processReplayEventsUntil(nowSec); advanceDynamicJudgeRankChanges(nowSec); playbackEventTracer.flushUntil(nowSec); @@ -3442,19 +3570,11 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) // over the same elapsed duration. Without this branch HCNs were one-shot // gauge sinks — once a player broke a hold, the only recovery path was // through subsequent normal-note PERFECTs. - applyLoggedGaugeDelta( - nowSec, - elapsedSeconds * HELL_CHARGE_GAUGE_GAIN_PER_SECOND, - 'hold-gain', - ); + applyLoggedGaugeDelta(nowSec, elapsedSeconds * HELL_CHARGE_GAUGE_GAIN_PER_SECOND, 'hold-gain'); } else { // HCN DRAIN — hold broken during this frame. Mirrors upstream // `JudgeManager.java:341-344`'s `gauge.update(3, 0.5f)` per 200 ms tick. - applyLoggedGaugeDelta( - nowSec, - -elapsedSeconds * HELL_CHARGE_GAUGE_DRAIN_PER_SECOND, - 'hold-drain', - ); + applyLoggedGaugeDelta(nowSec, -elapsedSeconds * HELL_CHARGE_GAUGE_DRAIN_PER_SECOND, 'hold-drain'); } } hold.gaugeDrainCursorSeconds = accumulateUntilSeconds; @@ -3658,6 +3778,11 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) summary, }); } + if (playlogRecorder) { + options.onPlaylogRecorded?.( + playlogRecorder.finalize({ summary, maxCombo: scoreTracker.maxCombo, aborted: true }), + ); + } writeOutput(renderSummary(summary)); return summary; } @@ -3674,6 +3799,9 @@ export async function manualPlay(json: BeMusicJson, options: PlayerOptions = {}) summary, }); } + if (playlogRecorder) { + options.onPlaylogRecorded?.(playlogRecorder.finalize({ summary, maxCombo: scoreTracker.maxCombo })); + } writeOutput(renderSummary(summary)); return summary; } diff --git a/packages/player/src/core/judge-window.test.ts b/packages/player/src/core/judge-window.test.ts index e8b615cc..81274241 100644 --- a/packages/player/src/core/judge-window.test.ts +++ b/packages/player/src/core/judge-window.test.ts @@ -2,9 +2,12 @@ import { createEmptyJson } from '@be-music/json'; import { describe, expect, test } from 'vitest'; import { bmsExRankValueToJudgeRankPercent, + resolveBeatorajaJudgeRankPercent, resolveBmsJudgeWindowsMsForExRankValue, resolveBmsJudgeWindowsMsForPercent, + resolveJudgeRankPercent, resolveJudgeWindowsMs, + resolveJudgeWindowsMsForRuleset, } from './judge-window.ts'; describe('judge-window', () => { @@ -86,6 +89,42 @@ describe('judge-window', () => { expect(bmsExRankValueToJudgeRankPercent(0)).toBe(0); }); + test('resolveJudgeRankPercent maps #RANK onto the internal percent axis', () => { + const expectations: Array<[number, number]> = [ + [0, 25], // VERY HARD + [1, 50], // HARD + [2, 75], // NORMAL + [3, 100], // EASY + [4, 75], // VERY EASY — LR2 treats #RANK 4 as NORMAL + ]; + for (const [rank, percent] of expectations) { + const json = createEmptyJson('bms'); + json.metadata.rank = rank; + expect(resolveJudgeRankPercent(json), `rank ${rank}`).toBe(percent); + } + }); + + test('resolveJudgeRankPercent: #DEFEXRANK wins over #RANK, and NORMAL is the default', () => { + const json = createEmptyJson('bms'); + json.metadata.rank = 0; + json.bms.defExRank = 120; // 120 × 75 / 100 + expect(resolveJudgeRankPercent(json)).toBeCloseTo(90, 9); + + expect(resolveJudgeRankPercent(createEmptyJson('bms'))).toBe(75); + }); + + test('resolveJudgeRankPercent: bmson prefers info.judge_rank, then metadata, then the spec default', () => { + const json = createEmptyJson('bmson'); + json.bmson.info.judgeRank = 140; + json.metadata.rank = 60; + expect(resolveJudgeRankPercent(json)).toBeCloseTo(105, 9); // 140 × 75 / 100 + + json.bmson.info.judgeRank = 0; // invalid → metadata rank 60 → 45 + expect(resolveJudgeRankPercent(json)).toBeCloseTo(45, 9); + + expect(resolveJudgeRankPercent(createEmptyJson('bmson'))).toBe(75); // judge_rank 100 default + }); + test('resolveBmsJudgeWindowsMsForExRankValue shares the RANK 2 = 100 unit with #DEFEXRANK', () => { const dynamic = resolveBmsJudgeWindowsMsForExRankValue(100); expect(dynamic).toEqual(resolveBmsJudgeWindowsMsForPercent(bmsExRankValueToJudgeRankPercent(100))); @@ -95,4 +134,50 @@ describe('judge-window', () => { // `#EXRANK 120` matches the documented `#DEFEXRANK 120` interpolation. expect(resolveBmsJudgeWindowsMsForExRankValue(120).great).toBeCloseTo(52, 6); }); + + test("resolveJudgeWindowsMsForRuleset: 'iidx' uses the fixed IIDX widths regardless of #RANK", () => { + const json = createEmptyJson('bms'); + json.metadata.rank = 0; // VERY HARD — must not narrow the IIDX windows + const windows = resolveJudgeWindowsMsForRuleset(json, 'iidx'); + expect(windows.pgreat).toBeCloseTo(16.67, 6); + expect(windows.great).toBeCloseTo(33.33, 6); + expect(windows.good).toBeCloseTo(116.67, 6); + expect(windows.bad).toBe(250); + // The debug override still replaces the BAD width only. + expect(resolveJudgeWindowsMsForRuleset(json, 'iidx', 300).bad).toBe(300); + expect(resolveJudgeWindowsMsForRuleset(json, 'iidx', 300).pgreat).toBeCloseTo(16.67, 6); + }); + + test("resolveJudgeWindowsMsForRuleset: 'beatoraja' scales the SEVENKEYS windows by judgerank", () => { + const json = createEmptyJson('bms'); + json.metadata.rank = 2; // beatoraja NORMAL rule → judgerank 75 % + const windows = resolveJudgeWindowsMsForRuleset(json, 'beatoraja'); + expect(windows.pgreat).toBeCloseTo(15, 6); // 20 × 0.75 + expect(windows.great).toBeCloseTo(45, 6); // 60 × 0.75 + expect(windows.good).toBeCloseTo(112.5, 6); // 150 × 0.75 + expect(windows.bad).toBeCloseTo(187.5, 6); // symmetric ±250 stand-in × 0.75 + + // #RANK 4 (VERY EASY) is 125 % under beatoraja — unlike LR2's 75 %. + json.metadata.rank = 4; + expect(resolveBeatorajaJudgeRankPercent(json)).toBe(125); + expect(resolveJudgeWindowsMsForRuleset(json, 'beatoraja').pgreat).toBeCloseTo(25, 6); + }); + + test("resolveJudgeWindowsMsForRuleset: 'beatoraja' judgerank sources — #DEFEXRANK × 0.75, bmson judge_rank as-is", () => { + const bms = createEmptyJson('bms'); + bms.bms.defExRank = 100; + expect(resolveBeatorajaJudgeRankPercent(bms)).toBeCloseTo(75, 9); + + const bmson = createEmptyJson('bmson'); + bmson.bmson.info.judgeRank = 130; + expect(resolveBeatorajaJudgeRankPercent(bmson)).toBe(130); + bmson.bmson.info.judgeRank = undefined; + expect(resolveBeatorajaJudgeRankPercent(bmson)).toBe(100); + }); + + test("resolveJudgeWindowsMsForRuleset: 'lr2' matches resolveJudgeWindowsMs", () => { + const json = createEmptyJson('bms'); + json.metadata.rank = 3; + expect(resolveJudgeWindowsMsForRuleset(json, 'lr2')).toEqual(resolveJudgeWindowsMs(json)); + }); }); diff --git a/packages/player/src/core/judge-window.ts b/packages/player/src/core/judge-window.ts index b7127436..bae96054 100644 --- a/packages/player/src/core/judge-window.ts +++ b/packages/player/src/core/judge-window.ts @@ -98,9 +98,106 @@ export function resolveBmsJudgeWindowsMsForExRankValue(exRankValue: number, debu return resolveBmsJudgeWindowsMsForPercent(bmsExRankValueToJudgeRankPercent(exRankValue), debugBadWindowMs); } +/** + * Resolves the chart's initial judgerank on the internal percent axis (VERY HARD = 25 / HARD = 50 / NORMAL = 75 / + * EASY = 100) — the same value {@link resolveJudgeWindowsMs} scales its windows from. Exposed so consumers that + * persist the resolved rank (e.g. the play-log recorder) share the exact resolution chain (`#DEFEXRANK` → + * `metadata.rank` → default for BMS; `info.judge_rank` → `metadata.rank` → default for bmson). + */ +export function resolveJudgeRankPercent(json: BeMusicJson): number { + return json.sourceFormat === 'bmson' ? resolveBmsonJudgeRankPercent(json) : resolveBmsJudgeRankPercent(json); +} + export function resolveJudgeWindowsMs(json: BeMusicJson, debugBadWindowMs?: number): JudgeWindowsMs { - const judgeRank = json.sourceFormat === 'bmson' ? resolveBmsonJudgeRankPercent(json) : resolveBmsJudgeRankPercent(json); - return scaleJudgeWindowsMs(judgeRank, debugBadWindowMs); + return scaleJudgeWindowsMs(resolveJudgeRankPercent(json), debugBadWindowMs); +} + +/** + * Which player's judge WINDOW model the live engine applies. Only the window widths switch — note selection, + * empty-POOR behavior, long-note mechanics, and the groove gauge stay on the engine's LR2-aligned semantics + * regardless (the playlog simulators in `@be-music/player/playlog` are the full per-ruleset reproduction). + */ +export type JudgeWindowRuleset = 'lr2' | 'beatoraja' | 'iidx'; + +export const JUDGE_WINDOW_RULESETS: readonly JudgeWindowRuleset[] = ['lr2', 'beatoraja', 'iidx']; + +/** + * IIDX judge windows (current AC, community measurement — iidx.org): ±1F / ±2F / ±7F / ±15F at 60 fps. Rank + * independent — IIDX ignores the BMS `#RANK` axis. + */ +const IIDX_JUDGE_WINDOWS_MS: JudgeWindowsMs = { pgreat: 16.67, great: 33.33, good: 116.67, bad: 250 }; + +/** beatoraja `#RANK 0..4` → judgerank percent (`JudgeWindowRule.NORMAL`; VERY EASY = 125, unlike LR2's 75). */ +const BEATORAJA_BMS_RANK_JUDGERANK_PERCENTS = [25, 50, 75, 100, 125] as const; +const BEATORAJA_NORMAL_JUDGERANK_PERCENT = BEATORAJA_BMS_RANK_JUDGERANK_PERCENTS[2]; +/** + * beatoraja SEVENKEYS base note windows at judgerank 100 (`JudgeProperty.java`). The real BAD window is asymmetric + * (late 280 ms / early 220 ms); the engine's judge pipeline is symmetric, so the midpoint ±250 ms stands in. + */ +const BEATORAJA_BASE_WINDOWS_MS = { pgreat: 20, great: 60, good: 150, bad: 250 } as const; + +/** + * beatoraja judgerank percent for a chart (`BMSPlayerRule.validate`, NORMAL window rule): BMS `#DEFEXRANK` is + * `value × 75 / 100`, bmson `judge_rank` is used as-is, and `#RANK 0..4` maps through + * {@link BEATORAJA_BMS_RANK_JUDGERANK_PERCENTS} (default NORMAL = 75). + */ +export function resolveBeatorajaJudgeRankPercent(json: BeMusicJson): number { + if (json.sourceFormat === 'bmson') { + const judgeRank = json.bmson.info.judgeRank; + if (Number.isFinite(judgeRank) && (judgeRank ?? 0) > 0) { + return judgeRank!; + } + return 100; + } + const defExRank = json.bms.defExRank; + if (typeof defExRank === 'number' && Number.isFinite(defExRank) && defExRank > 0) { + return (defExRank * BEATORAJA_NORMAL_JUDGERANK_PERCENT) / 100; + } + const rankValue = Number.isFinite(json.metadata.rank) ? Math.trunc(json.metadata.rank!) : Number.NaN; + if (Number.isFinite(rankValue) && rankValue >= 0 && rankValue < BEATORAJA_BMS_RANK_JUDGERANK_PERCENTS.length) { + return BEATORAJA_BMS_RANK_JUDGERANK_PERCENTS[rankValue as 0 | 1 | 2 | 3 | 4]; + } + return BEATORAJA_NORMAL_JUDGERANK_PERCENT; +} + +/** + * Resolves the live engine's judge windows under the selected ruleset. `'lr2'` is the engine default + * ({@link resolveJudgeWindowsMs}); `'beatoraja'` scales the SEVENKEYS windows linearly by beatoraja's judgerank; + * `'iidx'` uses the fixed IIDX widths. The `debugBadWindowMs` override replaces the BAD width in every ruleset, + * mirroring {@link resolveJudgeWindowsMs}'s debug semantics. + */ +export function resolveJudgeWindowsMsForRuleset( + json: BeMusicJson, + ruleset: JudgeWindowRuleset, + debugBadWindowMs?: number, +): JudgeWindowsMs { + if (ruleset === 'iidx') { + return overrideBadWindow(IIDX_JUDGE_WINDOWS_MS, debugBadWindowMs); + } + if (ruleset === 'beatoraja') { + const scale = Math.max(0, resolveBeatorajaJudgeRankPercent(json)) / 100; + const windows: JudgeWindowsMs = { + pgreat: BEATORAJA_BASE_WINDOWS_MS.pgreat * scale, + great: BEATORAJA_BASE_WINDOWS_MS.great * scale, + good: BEATORAJA_BASE_WINDOWS_MS.good * scale, + bad: BEATORAJA_BASE_WINDOWS_MS.bad * scale, + }; + const result = overrideBadWindow(windows, debugBadWindowMs); + return { + pgreat: clampWindow(result.pgreat, result.bad), + great: clampWindow(result.great, result.bad), + good: clampWindow(result.good, result.bad), + bad: result.bad, + }; + } + return resolveJudgeWindowsMs(json, debugBadWindowMs); +} + +function overrideBadWindow(windows: JudgeWindowsMs, debugBadWindowMs?: number): JudgeWindowsMs { + if (typeof debugBadWindowMs === 'number' && Number.isFinite(debugBadWindowMs) && debugBadWindowMs > 0) { + return { ...windows, bad: debugBadWindowMs }; + } + return windows; } function scaleJudgeWindowsMs(judgeRankPercent: number, debugBadWindowMs?: number): JudgeWindowsMs { diff --git a/packages/player/src/core/landmine.test.ts b/packages/player/src/core/landmine.test.ts new file mode 100644 index 00000000..574558e2 --- /dev/null +++ b/packages/player/src/core/landmine.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from 'vitest'; +import { DEFAULT_LANDMINE_GAUGE_DAMAGE, resolveLandmineGaugeEffect } from './landmine.ts'; + +describe('landmine', () => { + test('reads the BMS mine value directly as base-36 gauge damage', () => { + expect(resolveLandmineGaugeEffect({ value: '0A' })).toEqual({ objectValue: '0A', damage: 10, gaugeDelta: -10 }); + expect(resolveLandmineGaugeEffect({ value: '01' })).toEqual({ objectValue: '01', damage: 1, gaugeDelta: -1 }); + // Lowercase input normalizes under base 36 before decoding. + expect(resolveLandmineGaugeEffect({ value: '0a' }).damage).toBe(10); + }); + + test('ZZ decodes to 1295 — enough to wipe any gauge', () => { + expect(resolveLandmineGaugeEffect({ value: 'ZZ' })).toEqual({ + objectValue: 'ZZ', + damage: 1295, + gaugeDelta: -1295, + }); + }); + + test('bmson damage wins over the value rule, including an authored 0', () => { + expect(resolveLandmineGaugeEffect({ value: '0A', bmson: { damage: 25 } })).toEqual({ + objectValue: '0A', + damage: 25, + gaugeDelta: -25, + }); + + const decorative = resolveLandmineGaugeEffect({ value: 'ZZ', bmson: { damage: 0 } }); + expect(decorative.damage).toBe(0); + expect(decorative.gaugeDelta).toBe(-0); + }); + + test('invalid values fall back to the default damage of 4', () => { + expect(DEFAULT_LANDMINE_GAUGE_DAMAGE).toBe(4); + expect(resolveLandmineGaugeEffect({ value: '!!' })).toEqual({ + objectValue: '!!', + damage: DEFAULT_LANDMINE_GAUGE_DAMAGE, + gaugeDelta: -DEFAULT_LANDMINE_GAUGE_DAMAGE, + }); + // Parses fine but decodes to 0 — not a usable damage value. + expect(resolveLandmineGaugeEffect({ value: '00' }).damage).toBe(DEFAULT_LANDMINE_GAUGE_DAMAGE); + // Base-62 keeps lowercase IDs, which are not base-36 damage encodings. + expect(resolveLandmineGaugeEffect({ value: '0a' }, 62).damage).toBe(DEFAULT_LANDMINE_GAUGE_DAMAGE); + // A negative bmson damage is invalid and falls through to the value rule. + expect(resolveLandmineGaugeEffect({ value: '0A', bmson: { damage: -5 } }).damage).toBe(10); + }); +}); diff --git a/packages/player/src/core/landmine.ts b/packages/player/src/core/landmine.ts new file mode 100644 index 00000000..d5aaaf7c --- /dev/null +++ b/packages/player/src/core/landmine.ts @@ -0,0 +1,60 @@ +import { normalizeObjectKey, type BeMusicEvent } from '@be-music/json'; + +export const DEFAULT_LANDMINE_GAUGE_DAMAGE = 4; +const BASE36_OBJECT_KEY_PATTERN = /^[0-9A-Z]{2}$/; + +export interface LandmineGaugeEffect { + objectValue: string; + damage: number; + gaugeDelta: number; +} + +/** + * Resolves the gauge damage a landmine object deals when hit. + * + * Mine damage encodes the value in base-36 regardless of the chart's `#BASE` setting (the damage encoding is a + * chart-format constant, not an indexed-resource lookup), so the ID is normalized under the chart's base only to + * keep the returned `objectValue` in sync with the rest of the resource-key reporting. LR2 and beatoraja both + * interpret the value DIRECTLY as the gauge-damage percentage (losak's LR2 mine writeup; jbms-parser passes the raw + * base-36 value into `MineNote`) — the nanasi-era `value / 2` rule in hitkey's memo is a different lineage and is + * NOT what LR2 does. `ZZ` (= 1295) therefore wipes any gauge: survival gauges die instantly, GROOVE / EASY hit + * their 2 % floor. + * + * bmson `key_channels[].notes[].damage` is an explicit per-mine gauge percentage; when present it wins over the BMS + * value rule because the event value there is the WAV slot, not a damage encoding. `damage: 0` is a valid + * authored value (a no-damage decoration mine), so the guard checks finiteness rather than truthiness. + */ +export function resolveLandmineGaugeEffect( + landmineEvent: Pick<BeMusicEvent, 'value' | 'bmson'>, + base: 36 | 62 = 36, +): LandmineGaugeEffect { + const objectValue = normalizeObjectKey(landmineEvent.value, base); + const bmsonDamage = landmineEvent.bmson?.damage; + if (typeof bmsonDamage === 'number' && Number.isFinite(bmsonDamage) && bmsonDamage >= 0) { + return { + objectValue, + damage: bmsonDamage, + gaugeDelta: -bmsonDamage, + }; + } + if (!BASE36_OBJECT_KEY_PATTERN.test(objectValue)) { + return { + objectValue, + damage: DEFAULT_LANDMINE_GAUGE_DAMAGE, + gaugeDelta: -DEFAULT_LANDMINE_GAUGE_DAMAGE, + }; + } + const parsedDamage = Number.parseInt(objectValue, 36); + if (!Number.isFinite(parsedDamage) || parsedDamage <= 0) { + return { + objectValue, + damage: DEFAULT_LANDMINE_GAUGE_DAMAGE, + gaugeDelta: -DEFAULT_LANDMINE_GAUGE_DAMAGE, + }; + } + return { + objectValue, + damage: parsedDamage, + gaugeDelta: -parsedDamage, + }; +} diff --git a/packages/player/src/core/scoring.test.ts b/packages/player/src/core/scoring.test.ts index 83301d1d..c6dcd930 100644 --- a/packages/player/src/core/scoring.test.ts +++ b/packages/player/src/core/scoring.test.ts @@ -66,4 +66,35 @@ describe('scoring', () => { applyJudgeToSummary(summary, 'GREAT', tracker); expect(summary.score).toBeLessThan(IIDX_SCORE_MAX); }); + + test('latches maxCombo across combo breaks', () => { + const summary = createSummary(20); + const tracker = createScoreTracker(); + + applyJudgeToSummary(summary, 'PERFECT', tracker); + applyJudgeToSummary(summary, 'GREAT', tracker); + applyJudgeToSummary(summary, 'GOOD', tracker); + expect(tracker.combo).toBe(3); + expect(tracker.maxCombo).toBe(3); + + applyJudgeToSummary(summary, 'BAD', tracker); + expect(tracker.combo).toBe(0); + expect(tracker.maxCombo).toBe(3); // BAD breaks the combo but never the latch + + applyJudgeToSummary(summary, 'PERFECT', tracker); + applyJudgeToSummary(summary, 'PERFECT', tracker); + expect(tracker.combo).toBe(2); + expect(tracker.maxCombo).toBe(3); // a shorter rebuild does not move the latch + + applyJudgeToSummary(summary, 'POOR', tracker); + expect(tracker.combo).toBe(0); + expect(tracker.maxCombo).toBe(3); + + applyJudgeToSummary(summary, 'PERFECT', tracker); + applyJudgeToSummary(summary, 'GREAT', tracker); + applyJudgeToSummary(summary, 'GOOD', tracker); + applyJudgeToSummary(summary, 'PERFECT', tracker); + expect(tracker.combo).toBe(4); + expect(tracker.maxCombo).toBe(4); // only a longer streak advances it + }); }); diff --git a/packages/player/src/core/scoring.ts b/packages/player/src/core/scoring.ts index f1ddfa3d..c00946a9 100644 --- a/packages/player/src/core/scoring.ts +++ b/packages/player/src/core/scoring.ts @@ -13,6 +13,8 @@ export interface ScoreSummary { export interface ScoreTracker { combo: number; + /** Highest combo reached so far — latched by {@link applyJudgeToSummary}, never reset by BAD / POOR. */ + maxCombo: number; scoreAccumulator: number; scoreMaxAccumulator: number; } @@ -25,6 +27,7 @@ const IIDX_SCORE_COMBO_BONUS_MAX = 50000; export function createScoreTracker(): ScoreTracker { return { combo: 0, + maxCombo: 0, scoreAccumulator: 0, scoreMaxAccumulator: Number.NaN, }; @@ -91,6 +94,9 @@ export function applyJudgeToSummary(summary: ScoreSummary, judge: JudgeKind, tra if (judge === 'PERFECT' || judge === 'GREAT' || judge === 'GOOD') { tracker.combo += 1; + if (tracker.combo > tracker.maxCombo) { + tracker.maxCombo = tracker.combo; + } } else { tracker.combo = 0; } diff --git a/packages/player/src/index.test.ts b/packages/player/src/index.test.ts index 9af0d814..a49177fa 100644 --- a/packages/player/src/index.test.ts +++ b/packages/player/src/index.test.ts @@ -95,6 +95,7 @@ import { } from './index.ts'; import type { PlayerInputCommand } from './core/input-signal-bus.ts'; import { resolveChartVolWavGain, resolveDisplayedJudgeRankLabel, resolveDisplayedJudgeRankValue } from './utils.ts'; +import { parsePlaylog, serializePlaylog, simulatePlaylog, type BeMusicPlaylog } from './playlog/index.ts'; const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); const unifiedBmsChartPath = resolve(rootDir, 'examples/test/four-measure-command-combo-test.bms'); @@ -969,6 +970,194 @@ describe('player', () => { expect(output.some((line) => line.includes('result:EMPTY_POOR'))).toBe(false); }); + test('player: onPlaylogRecorded captures the resolved chart, raw inputs, and native result cache', async () => { + // BPM 240 → measure 1 starts at chart 1.0 s. One press near the note, then natural chart end. + const json = createEmptyJson('bms'); + json.metadata.bpm = 240; + json.metadata.title = 'Playlog Test'; + json.metadata.total = 300; + json.events = [{ measure: 1, channel: '11', position: [0, 1], value: '01' }]; + + let playlog: BeMusicPlaylog | undefined; + const summary = await manualPlay(json, { + speed: 1, + leadInMs: 0, + audio: false, + tui: false, + recordPlaylog: { gauge: 'GROOVE', randomLane: { p1: 'MIRROR' } }, + onPlaylogRecorded: (recorded) => { + playlog = recorded; + }, + createInputRuntime: createScheduledInputRuntime([ + { delayMs: 1000, command: { kind: 'lane-input', tokens: ['z'] } }, + ]), + }); + + expect(playlog).toBeDefined(); + expect(playlog!.format).toBe('be-music-playlog'); + expect(playlog!.version).toBe(1); + expect(playlog!.clock).toEqual({ unit: 'us', origin: 'chart-zero' }); + expect(playlog!.chart.title).toBe('Playlog Test'); + expect(playlog!.chart.total).toBe(300); + expect(playlog!.chart.noteCount).toBe(1); + expect(playlog!.chart.notes).toHaveLength(1); + expect(playlog!.chart.notes[0]).toMatchObject({ id: 0, channel: '11', type: 'normal', timeUs: 1_000_000 }); + expect(playlog!.play).toMatchObject({ mode: 'manual', autoScratch: false, gauge: 'GROOVE' }); + expect(playlog!.play.randomLane).toEqual({ p1: 'MIRROR' }); + expect(playlog!.play.aborted).toBeUndefined(); + // The single scheduled press resolves against lane channel 11 with a chart-relative µs timestamp near the note. + expect(playlog!.inputs).toHaveLength(1); + expect(playlog!.inputs[0]).toMatchObject({ seq: 0, action: 'down', channels: ['11'] }); + expect(Math.abs(playlog!.inputs[0]!.timeUs - 1_000_000)).toBeLessThan(250_000); + // The native cache mirrors the engine's own summary. + const native = playlog!.results?.native; + expect(native).toBeDefined(); + expect(native!.exScore).toBe(summary.exScore); + expect(native!.judge.pgreat).toBe(summary.perfect); + expect(native!.judge.poor).toBe(summary.poor); + expect(native!.gauge.final).toBeCloseTo(summary.gauge?.current ?? -1, 6); + // The recorded log feeds the ruleset simulators without further conversion. + const lr2 = simulatePlaylog(playlog!, { ruleset: 'lr2' }); + expect(lr2.noteCount).toBe(1); + expect(lr2.judge.pgreat + lr2.judge.great + lr2.judge.good + lr2.judge.bad + lr2.judge.poor).toBe(1); + }); + + test('player: replayInputs re-drives a recorded playlog deterministically', async () => { + // BPM 240 → notes at 1.0 s ('11') and 1.5 s ('12'). Record a play with two slightly-off presses, then replay + // the recorded input stream — the judgments must reproduce exactly, even at a different engine speed. + const json = createEmptyJson('bms'); + json.metadata.bpm = 240; + json.metadata.total = 300; + json.events = [ + { measure: 1, channel: '11', position: [0, 2], value: '01' }, + { measure: 1, channel: '12', position: [1, 2], value: '01' }, + ]; + + let recorded: BeMusicPlaylog | undefined; + const original = await manualPlay(json, { + speed: 1, + leadInMs: 0, + audio: false, + tui: false, + onPlaylogRecorded: (playlog) => { + recorded = playlog; + }, + createInputRuntime: createScheduledInputRuntime([ + { delayMs: 990, command: { kind: 'lane-input', tokens: ['z'] } }, + { delayMs: 1540, command: { kind: 'lane-input', tokens: ['s'] } }, + ]), + }); + expect(recorded).toBeDefined(); + expect(recorded!.inputs).toHaveLength(2); + const originalJudged = original.perfect + original.great + original.good + original.bad; + expect(originalJudged).toBeGreaterThan(0); + + let replayed: BeMusicPlaylog | undefined; + const replaySummary = await manualPlay(json, { + // Chart-relative replay timestamps are speed-independent — run the replay fast to keep the test quick. + speed: 8, + leadInMs: 0, + audio: false, + tui: false, + replayInputs: recorded!.inputs, + onPlaylogRecorded: (playlog) => { + replayed = playlog; + }, + }); + + expect(replaySummary.perfect).toBe(original.perfect); + expect(replaySummary.great).toBe(original.great); + expect(replaySummary.good).toBe(original.good); + expect(replaySummary.bad).toBe(original.bad); + expect(replaySummary.poor).toBe(original.poor); + expect(replaySummary.exScore).toBe(original.exScore); + expect(replaySummary.score).toBe(original.score); + expect(replaySummary.fast).toBe(original.fast); + expect(replaySummary.slow).toBe(original.slow); + expect(replaySummary.gauge?.current).toBeCloseTo(original.gauge?.current ?? -1, 6); + // The replayed run re-records an equivalent input stream (same actions, channels, and µs timestamps). + expect( + replayed!.inputs.map((input) => ({ action: input.action, timeUs: input.timeUs, channels: input.channels })), + ).toEqual( + recorded!.inputs.map((input) => ({ action: input.action, timeUs: input.timeUs, channels: input.channels })), + ); + }); + + test('player: judgeRuleset switches the manual judge windows (LR2 / beatoraja / IIDX)', async () => { + // BPM 240 → one note at 1.0 s; the replayed press lands 35 ms LATE. Under the default (LR2, RANK 2 → GREAT + // ±40 ms) and beatoraja (judgerank 75 % → GREAT ±45 ms) windows that is a GREAT; under IIDX (GREAT ±33.33 ms, + // GOOD ±116.67 ms) it is a GOOD. + const json = createEmptyJson('bms'); + json.metadata.bpm = 240; + json.metadata.rank = 2; + json.events = [{ measure: 1, channel: '11', position: [0, 1], value: '01' }]; + const replayInputs = [{ seq: 0, timeUs: 1_035_000, action: 'down' as const, channels: ['11'] }]; + const base = { speed: 8, leadInMs: 0, audio: false, tui: false, replayInputs } as const; + + const lr2 = await manualPlay(json, { ...base }); + const beatoraja = await manualPlay(json, { ...base, judgeRuleset: 'beatoraja' }); + const iidx = await manualPlay(json, { ...base, judgeRuleset: 'iidx' }); + + expect(lr2.great).toBe(1); + expect(lr2.good).toBe(0); + expect(beatoraja.great).toBe(1); + expect(beatoraja.good).toBe(0); + expect(iidx.great).toBe(0); + expect(iidx.good).toBe(1); + }); + + test('player: recordPlaylog stamps the chart hash and judge ruleset into the playlog', async () => { + const json = createEmptyJson('bms'); + json.metadata.bpm = 240; + json.events = [{ measure: 1, channel: '11', position: [0, 1], value: '01' }]; + + let playlog: BeMusicPlaylog | undefined; + await manualPlay(json, { + speed: 8, + leadInMs: 0, + audio: false, + tui: false, + judgeRuleset: 'iidx', + recordPlaylog: { chartSha256: 'ABCDEF0123456789' }, + onPlaylogRecorded: (recorded) => { + playlog = recorded; + }, + createInputRuntime: createScheduledInputRuntime([ + { delayMs: 50, command: { kind: 'interrupt', reason: 'escape' } }, + ]), + }); + + expect(playlog!.chart.sha256).toBe('abcdef0123456789'); + expect(playlog!.play.judgeRuleset).toBe('iidx'); + const parsed = parsePlaylog(serializePlaylog(playlog!)); + expect(parsed.chart.sha256).toBe('abcdef0123456789'); + expect(parsed.play.judgeRuleset).toBe('iidx'); + }); + + test('player: ESC-interrupted play records an aborted playlog', async () => { + const json = createEmptyJson('bms'); + json.metadata.bpm = 240; + json.events = [{ measure: 1, channel: '11', position: [0, 1], value: '01' }]; + + let playlog: BeMusicPlaylog | undefined; + await manualPlay(json, { + speed: 1, + leadInMs: 0, + audio: false, + tui: false, + onPlaylogRecorded: (recorded) => { + playlog = recorded; + }, + createInputRuntime: createScheduledInputRuntime([ + { delayMs: 100, command: { kind: 'interrupt', reason: 'escape' } }, + ]), + }); + + expect(playlog).toBeDefined(); + expect(playlog!.play.aborted).toBe(true); + expect(playlog!.inputs).toHaveLength(0); + }); + test('player: blank press between same-lane notes plays the previous keysound, not the next pending keysound', async () => { const json = createEmptyJson('bms'); json.metadata.bpm = 240; diff --git a/packages/player/src/playlog/format.test.ts b/packages/player/src/playlog/format.test.ts new file mode 100644 index 00000000..a6eb00b9 --- /dev/null +++ b/packages/player/src/playlog/format.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, test } from 'vitest'; +import { + BE_MUSIC_PLAYLOG_FORMAT, + BE_MUSIC_PLAYLOG_VERSION, + parsePlaylog, + PLAYLOG_FILE_SUFFIX, + PlaylogParseError, + resolvePlaylogFilename, + serializePlaylog, + type BeMusicPlaylog, +} from './format.ts'; + +function makePlaylog(): BeMusicPlaylog { + return { + format: BE_MUSIC_PLAYLOG_FORMAT, + version: BE_MUSIC_PLAYLOG_VERSION, + createdAt: '2026-08-17T01:02:03.456Z', + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + title: 'Song / Title: Test', + subtitle: 'ANOTHER', + artist: 'composer', + genre: 'genre', + sourceFormat: 'bms', + laneMode: '7keys', + total: 300, + lnMode: 2, + judgeRank: { + percent: 75, + sourceRank: 2, + sourceExRank: 100, + timeline: [{ timeUs: 4_000_000, exRankValue: 48 }], + }, + noteCount: 3, + notes: [ + { id: 0, channel: '11', type: 'normal', timeUs: 500_000 }, + { id: 1, channel: '12', type: 'long', timeUs: 1_000_000, endTimeUs: 2_000_000, lnMode: 2 }, + { id: 2, channel: '13', type: 'mine', timeUs: 1_500_000, damage: 10 }, + { id: 3, channel: '14', type: 'invisible', timeUs: 1_600_000 }, + { id: 4, channel: '17', type: 'freezone', timeUs: 1_700_000, endTimeUs: 1_900_000 }, + ], + }, + inputs: [ + { seq: 0, timeUs: 499_000, action: 'down', channels: ['11'], tokens: ['z'] }, + { seq: 1, timeUs: 520_000, action: 'up', channels: ['11', '12'] }, + ], + play: { + mode: 'manual', + autoScratch: true, + gauge: 'HARD', + randomLane: { p1: 'MIRROR', p2: 'RANDOM' }, + dpFlip: true, + judgeWindowOverrideMs: 500, + aborted: true, + native: { hiSpeed: 2.5, skin: 'default', assist: false, memo: null }, + }, + results: { + native: { + ruleset: 'be-music/native', + judge: { pgreat: 1, great: 1, good: 0, bad: 0, poor: 1, emptyPoor: 2 }, + fast: 1, + slow: 0, + exScore: 3, + noteCount: 3, + maxCombo: 2, + score: 100000, + djLevel: 'AA', + gauge: { type: 'HARD', final: 0, cleared: false, failedMidPlay: true }, + }, + }, + }; +} + +function corrupt(mutate: (value: any) => void): unknown { + const value = JSON.parse(serializePlaylog(makePlaylog())); + mutate(value); + return value; +} + +describe('playlog format', () => { + test('serializePlaylog → parsePlaylog round-trips every field', () => { + const playlog = makePlaylog(); + expect(parsePlaylog(serializePlaylog(playlog))).toEqual(playlog); + // An already-parsed value is accepted too. + expect(parsePlaylog(JSON.parse(serializePlaylog(playlog)))).toEqual(playlog); + }); + + test('round-trips a minimal playlog without any optional field', () => { + const minimal: BeMusicPlaylog = { + format: BE_MUSIC_PLAYLOG_FORMAT, + version: BE_MUSIC_PLAYLOG_VERSION, + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + sourceFormat: 'bmson', + laneMode: '7keys', + lnMode: 1, + judgeRank: { percent: 75 }, + noteCount: 0, + notes: [], + }, + inputs: [], + play: { mode: 'auto', autoScratch: false, gauge: 'GROOVE' }, + }; + const parsed = parsePlaylog(serializePlaylog(minimal)); + expect(parsed).toEqual(minimal); + expect(parsed.results).toBeUndefined(); + }); + + test('rejects invalid JSON and wrong format / version / clock markers', () => { + expect(() => parsePlaylog('{oops')).toThrow(PlaylogParseError); + expect(() => parsePlaylog('{oops')).toThrow(/invalid JSON/); + expect(() => + parsePlaylog( + corrupt((value) => { + value.format = 'other-format'; + }), + ), + ).toThrow(/^format:/); + expect(() => + parsePlaylog( + corrupt((value) => { + value.version = 2; + }), + ), + ).toThrow(/^version:/); + expect(() => + parsePlaylog( + corrupt((value) => { + value.clock.unit = 'ms'; + }), + ), + ).toThrow(/^clock:/); + }); + + test('reports the field path of structural problems', () => { + expect(() => + parsePlaylog( + corrupt((value) => { + value.inputs = {}; + }), + ), + ).toThrow('inputs: expected an array'); + expect(() => + parsePlaylog( + corrupt((value) => { + value.inputs[0].action = 'press'; + }), + ), + ).toThrow("inputs[0].action: expected 'down' | 'up'"); + expect(() => + parsePlaylog( + corrupt((value) => { + value.inputs[0].channels = [11]; + }), + ), + ).toThrow('inputs[0].channels: expected string[]'); + expect(() => + parsePlaylog( + corrupt((value) => { + value.chart.notes[1].type = 'weird'; + }), + ), + ).toThrow(/chart\.notes\[1\]\.type/); + expect(() => + parsePlaylog( + corrupt((value) => { + delete value.chart.notes[0].timeUs; + }), + ), + ).toThrow('chart.notes[0].timeUs: expected a finite number'); + expect(() => + parsePlaylog( + corrupt((value) => { + delete value.chart.judgeRank.percent; + }), + ), + ).toThrow('chart.judgeRank.percent: expected a finite number'); + expect(() => + parsePlaylog( + corrupt((value) => { + value.chart.lnMode = 5; + }), + ), + ).toThrow('chart.lnMode: expected 1 | 2 | 3'); + expect(() => + parsePlaylog( + corrupt((value) => { + value.play.gauge = 'SUPER'; + }), + ), + ).toThrow(/play\.gauge/); + expect(() => + parsePlaylog( + corrupt((value) => { + delete value.results.native.judge.pgreat; + }), + ), + ).toThrow('results.native.judge.pgreat: expected a finite number'); + }); + + test('ignores unknown fields for forward compatibility', () => { + const parsed = parsePlaylog( + corrupt((value) => { + value.futureRootField = true; + value.chart.futureChartField = 'x'; + value.chart.notes[0].futureNoteField = 1; + value.inputs[0].futureInputField = 1; + value.play.futurePlayField = 1; + value.results.native.futureResultField = 1; + }), + ); + expect(parsed).toEqual(makePlaylog()); + expect(parsed).not.toHaveProperty('futureRootField'); + expect(parsed.chart.notes[0]).not.toHaveProperty('futureNoteField'); + }); + + test('resolvePlaylogFilename sanitizes the title and stamps createdAt', () => { + const playlog = makePlaylog(); + expect(resolvePlaylogFilename(playlog)).toBe('Song Title Test-2026-08-17T01-02-03-456Z.bmplay.json'); + // An explicit `when` wins over the playlog's own createdAt. + expect(resolvePlaylogFilename(playlog, new Date('2027-02-03T04:05:06.007Z'))).toBe( + 'Song Title Test-2027-02-03T04-05-06-007Z.bmplay.json', + ); + }); + + test("resolvePlaylogFilename falls back to 'play' / 'unknown-time'", () => { + const playlog = makePlaylog(); + delete playlog.chart.title; + delete playlog.createdAt; + expect(resolvePlaylogFilename(playlog)).toBe(`play-unknown-time${PLAYLOG_FILE_SUFFIX}`); + + // A title made entirely of filesystem-hostile characters sanitizes to nothing → 'play'. + const hostile = makePlaylog(); + hostile.chart.title = '<>:"/\\|?*'; + expect(resolvePlaylogFilename(hostile)).toBe('play-2026-08-17T01-02-03-456Z.bmplay.json'); + + // A createdAt that does not parse as a date is reported as unknown-time. + const badDate = makePlaylog(); + badDate.createdAt = 'not-a-date'; + expect(resolvePlaylogFilename(badDate)).toBe(`Song Title Test-unknown-time${PLAYLOG_FILE_SUFFIX}`); + expect(resolvePlaylogFilename(badDate).endsWith('.bmplay.json')).toBe(true); + }); +}); diff --git a/packages/player/src/playlog/format.ts b/packages/player/src/playlog/format.ts new file mode 100644 index 00000000..56faf921 --- /dev/null +++ b/packages/player/src/playlog/format.ts @@ -0,0 +1,467 @@ +import type { GrooveGaugeType } from '../core/groove-gauge.ts'; +import type { LongNoteMode } from '../playable-notes.ts'; + +/** + * be-music play-log ("playlog") format. + * + * A playlog is an INPUT REPLAY, not a result log: the canonical payload is the resolved chart that actually + * scrolled past the player (post `#RANDOM`, post lane-shuffle / DP-flip), the raw key press / release stream, and + * the play settings. Judgments, EX-SCORE, combo, and gauge values are deliberately NOT part of the canonical data — + * they are re-derived by ruleset simulators (`simulate.ts`), so a later fix to a ruleset re-scores every past play + * without re-recording anything. `results` is a regenerable cache, never the source of truth. + * + * Times are integer microseconds relative to chart zero (the same t=0 the engine's note `seconds` axis uses). + * Events that share a timestamp are ordered by `seq` — sort key is always `(timeUs, seq)`. + */ +export const BE_MUSIC_PLAYLOG_FORMAT = 'be-music-playlog'; +export const BE_MUSIC_PLAYLOG_VERSION = 1; + +/** Recommended file suffix for serialized playlogs (JSON payload). */ +export const PLAYLOG_FILE_SUFFIX = '.bmplay.json'; + +export type PlaylogNoteType = 'normal' | 'long' | 'mine' | 'invisible' | 'freezone'; + +export interface PlaylogNote { + /** Stable note id — index into the chart's note array at record time. */ + id: number; + /** Normalized playable channel AFTER every lane transform (`11`..`19` / `21`..`29`), i.e. the lane the player saw. */ + channel: string; + type: PlaylogNoteType; + timeUs: number; + /** Long-note / freezone tail. Present only when `type` is `'long'` or `'freezone'`. */ + endTimeUs?: number; + /** `#LNMODE`-resolved mode for `type: 'long'` notes (1: LN / 2: CN / 3: HCN). */ + lnMode?: LongNoteMode; + /** Gauge damage percentage for `type: 'mine'` notes (base-36 value / bmson `damage`, already resolved). */ + damage?: number; +} + +export type PlaylogInputAction = 'down' | 'up'; + +export interface PlaylogInputEvent { + /** Monotonic sequence number — tie-breaker for events that share `timeUs`. */ + seq: number; + timeUs: number; + action: PlaylogInputAction; + /** + * Playable channels the physical input resolved to under the play session's lane bindings (auto-scratch lanes + * already filtered out). One press can cover several channels (e.g. a token bound to multiple lanes); simulators + * search all of them for a judgable note, mirroring the live engine. + */ + channels: string[]; + /** Raw input tokens (`'z'`, `'shift-left'`, ...) — diagnostic only; simulators use `channels`. */ + tokens?: string[]; +} + +export interface PlaylogJudgeRank { + /** + * Initial judgerank on the internal LR2 percent axis (VERY HARD = 25 / HARD = 50 / NORMAL = 75 / EASY = 100), + * resolved through `resolveJudgeRankPercent` at record time. + */ + percent: number; + /** Raw `#RANK` (`metadata.rank`) when the chart specified one. */ + sourceRank?: number; + /** Raw `#DEFEXRANK` (BMS) / `info.judge_rank` (bmson) when specified (`100 = NORMAL` unit). */ + sourceExRank?: number; + /** Dynamic `#EXRANKxx` changes (channel `A0`), in chart order. Values share the `100 = NORMAL` unit. */ + timeline?: Array<{ timeUs: number; exRankValue: number }>; +} + +export interface PlaylogChart { + title?: string; + subtitle?: string; + artist?: string; + genre?: string; + /** Lowercase-hex SHA-256 of the source chart FILE bytes — the primary key for matching a log back to its chart. */ + sha256?: string; + sourceFormat: 'bms' | 'bmson'; + /** Engine lane display mode (`'7keys'` / `'14keys'` / ... — `resolveLaneDisplayMode` output). */ + laneMode: string; + /** Raw `#TOTAL` / bmson `info.total`. Omitted when the chart did not specify one — each ruleset applies its own default. */ + total?: number; + /** Chart-level long-note mode (1: LN / 2: CN / 3: HCN) after `#LNMODE` / `info.ln_type` resolution. */ + lnMode: LongNoteMode; + judgeRank: PlaylogJudgeRank; + /** Number of scorable notes (TOTAL / EX-SCORE denominator — mines / invisibles / freezones excluded). */ + noteCount: number; + notes: PlaylogNote[]; +} + +export interface PlaylogPlay { + mode: 'manual' | 'auto'; + autoScratch: boolean; + /** Gauge the player selected for the session (declared by the host; LR2-family gauge id). */ + gauge: GrooveGaugeType; + /** Lane-arrangement option per side, when the host applied one (`'OFF'` / `'MIRROR'` / `'RANDOM'` / ...). */ + randomLane?: { p1?: string; p2?: string }; + dpFlip?: boolean; + /** Debug BAD-window override (`PlayerOptions.judgeWindowMs`) — simulations of such plays are non-standard. */ + judgeWindowOverrideMs?: number; + /** Judge-window ruleset the live engine ran (`PlayerOptions.judgeRuleset`). Absent = `'lr2'` (the default). */ + judgeRuleset?: 'lr2' | 'beatoraja' | 'iidx'; + /** True when the play ended early (ESC). The input stream stops at the abort point. */ + aborted?: boolean; + /** Lossless bag for host-specific settings that don't affect simulation (hi-speed, skin, ...). */ + native?: Record<string, string | number | boolean | null>; +} + +export interface PlaylogJudgeCounts { + pgreat: number; + great: number; + good: number; + bad: number; + poor: number; + /** LR2-style empty POOR count — never part of the judge counters above. */ + emptyPoor: number; +} + +export interface PlaylogGaugeResult { + /** Ruleset-scoped gauge id (`'GROOVE'`, `'HARD'`, `'NORMAL'`, `'EX-HARD'`, ...). */ + type: string; + /** Final gauge value in percent. */ + final: number; + cleared: boolean; + /** True when a survival gauge bottomed out mid-play (the simulator keeps judging to the end regardless). */ + failedMidPlay?: boolean; +} + +export interface PlaylogRulesetResult { + /** Ruleset identifier + revision (e.g. `'lr2/1'`, `'beatoraja/1'`, `'iidx/1'`, `'be-music/native'`). */ + ruleset: string; + judge: PlaylogJudgeCounts; + fast: number; + slow: number; + exScore: number; + /** + * The ruleset's judgment-note count (EX-SCORE denominator ÷ 2). Differs per ruleset: charge-note styles + * (beatoraja CN/HCN, IIDX CN) count a long note's head and tail as two judgments, LN styles as one. + */ + noteCount: number; + maxCombo: number; + /** 200000-max money score where the ruleset defines one. */ + score?: number; + /** IIDX DJ LEVEL label (`'AAA'`..`'F'`) where the ruleset defines one. */ + djLevel?: string; + gauge: PlaylogGaugeResult; +} + +export interface BeMusicPlaylog { + format: typeof BE_MUSIC_PLAYLOG_FORMAT; + version: typeof BE_MUSIC_PLAYLOG_VERSION; + /** ISO 8601 timestamp of the recording, when the recorder had a clock available. */ + createdAt?: string; + clock: { unit: 'us'; origin: 'chart-zero' }; + chart: PlaylogChart; + inputs: PlaylogInputEvent[]; + play: PlaylogPlay; + /** Regenerable result cache keyed by ruleset id. Never authoritative — delete freely. */ + results?: Record<string, PlaylogRulesetResult>; +} + +export function serializePlaylog(playlog: BeMusicPlaylog): string { + return JSON.stringify(playlog); +} + +/** + * Suggested download / save filename for a playlog: `<title>-<timestamp>.bmplay.json` with + * filesystem-hostile characters stripped. `when` defaults to the playlog's own `createdAt`. + */ +export function resolvePlaylogFilename(playlog: BeMusicPlaylog, when?: Date): string { + const stemSource = playlog.chart.title ?? 'play'; + const stem = + stemSource + .normalize('NFKC') + .replace(/[\\/:*?"<>|]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .slice(0, 60) || 'play'; + const timestampSource = when ?? (playlog.createdAt !== undefined ? new Date(playlog.createdAt) : undefined); + const timestamp = + timestampSource !== undefined && Number.isFinite(timestampSource.getTime()) + ? timestampSource.toISOString().replace(/[:.]/g, '-') + : 'unknown-time'; + return `${stem}-${timestamp}${PLAYLOG_FILE_SUFFIX}`; +} + +export class PlaylogParseError extends Error { + constructor(message: string) { + super(message); + this.name = 'PlaylogParseError'; + } +} + +/** + * Parses (and defensively validates) a serialized playlog. Accepts either the JSON text or an already-parsed + * value. Throws {@link PlaylogParseError} with a field-path message on structural problems; unknown extra fields + * are preserved-by-ignoring so future minor additions stay readable. + */ +export function parsePlaylog(source: string | unknown): BeMusicPlaylog { + let value: unknown = source; + if (typeof source === 'string') { + try { + value = JSON.parse(source); + } catch (error) { + throw new PlaylogParseError(`invalid JSON: ${(error as Error).message}`); + } + } + const root = expectRecord(value, 'playlog'); + if (root.format !== BE_MUSIC_PLAYLOG_FORMAT) { + throw new PlaylogParseError(`format: expected '${BE_MUSIC_PLAYLOG_FORMAT}', got ${JSON.stringify(root.format)}`); + } + if (root.version !== BE_MUSIC_PLAYLOG_VERSION) { + throw new PlaylogParseError(`version: unsupported version ${JSON.stringify(root.version)}`); + } + const clock = expectRecord(root.clock, 'clock'); + if (clock.unit !== 'us' || clock.origin !== 'chart-zero') { + throw new PlaylogParseError(`clock: expected { unit: 'us', origin: 'chart-zero' }`); + } + + const chart = parseChart(expectRecord(root.chart, 'chart')); + const inputs = parseInputs(root.inputs); + const play = parsePlay(expectRecord(root.play, 'play')); + const results = root.results === undefined ? undefined : parseResults(expectRecord(root.results, 'results')); + + return { + format: BE_MUSIC_PLAYLOG_FORMAT, + version: BE_MUSIC_PLAYLOG_VERSION, + createdAt: optionalString(root.createdAt, 'createdAt'), + clock: { unit: 'us', origin: 'chart-zero' }, + chart, + inputs, + play, + ...(results !== undefined ? { results } : {}), + }; +} + +function parseChart(chart: Record<string, unknown>): PlaylogChart { + const sourceFormat = chart.sourceFormat; + if (sourceFormat !== 'bms' && sourceFormat !== 'bmson') { + throw new PlaylogParseError(`chart.sourceFormat: expected 'bms' | 'bmson'`); + } + const judgeRankRaw = expectRecord(chart.judgeRank, 'chart.judgeRank'); + const judgeRank: PlaylogJudgeRank = { + percent: expectFiniteNumber(judgeRankRaw.percent, 'chart.judgeRank.percent'), + sourceRank: optionalFiniteNumber(judgeRankRaw.sourceRank, 'chart.judgeRank.sourceRank'), + sourceExRank: optionalFiniteNumber(judgeRankRaw.sourceExRank, 'chart.judgeRank.sourceExRank'), + timeline: parseJudgeRankTimeline(judgeRankRaw.timeline), + }; + if (judgeRank.sourceRank === undefined) delete judgeRank.sourceRank; + if (judgeRank.sourceExRank === undefined) delete judgeRank.sourceExRank; + if (judgeRank.timeline === undefined) delete judgeRank.timeline; + + const notesRaw = chart.notes; + if (!Array.isArray(notesRaw)) { + throw new PlaylogParseError('chart.notes: expected an array'); + } + const notes = notesRaw.map((note, index) => parseNote(note, index)); + + const lnMode = chart.lnMode; + if (lnMode !== 1 && lnMode !== 2 && lnMode !== 3) { + throw new PlaylogParseError('chart.lnMode: expected 1 | 2 | 3'); + } + + const parsed: PlaylogChart = { + title: optionalString(chart.title, 'chart.title'), + subtitle: optionalString(chart.subtitle, 'chart.subtitle'), + artist: optionalString(chart.artist, 'chart.artist'), + genre: optionalString(chart.genre, 'chart.genre'), + sha256: optionalString(chart.sha256, 'chart.sha256'), + sourceFormat, + laneMode: expectString(chart.laneMode, 'chart.laneMode'), + total: optionalFiniteNumber(chart.total, 'chart.total'), + lnMode, + judgeRank, + noteCount: expectFiniteNumber(chart.noteCount, 'chart.noteCount'), + notes, + }; + for (const key of ['title', 'subtitle', 'artist', 'genre', 'sha256', 'total'] as const) { + if (parsed[key] === undefined) delete parsed[key]; + } + return parsed; +} + +function parseJudgeRankTimeline(value: unknown): PlaylogJudgeRank['timeline'] { + if (value === undefined) return undefined; + if (!Array.isArray(value)) { + throw new PlaylogParseError('chart.judgeRank.timeline: expected an array'); + } + return value.map((entry, index) => { + const record = expectRecord(entry, `chart.judgeRank.timeline[${index}]`); + return { + timeUs: expectFiniteNumber(record.timeUs, `chart.judgeRank.timeline[${index}].timeUs`), + exRankValue: expectFiniteNumber(record.exRankValue, `chart.judgeRank.timeline[${index}].exRankValue`), + }; + }); +} + +const PLAYLOG_NOTE_TYPES: ReadonlySet<string> = new Set(['normal', 'long', 'mine', 'invisible', 'freezone']); + +function parseNote(value: unknown, index: number): PlaylogNote { + const record = expectRecord(value, `chart.notes[${index}]`); + const type = record.type; + if (typeof type !== 'string' || !PLAYLOG_NOTE_TYPES.has(type)) { + throw new PlaylogParseError(`chart.notes[${index}].type: expected one of ${[...PLAYLOG_NOTE_TYPES].join(' | ')}`); + } + const note: PlaylogNote = { + id: expectFiniteNumber(record.id, `chart.notes[${index}].id`), + channel: expectString(record.channel, `chart.notes[${index}].channel`), + type: type as PlaylogNoteType, + timeUs: expectFiniteNumber(record.timeUs, `chart.notes[${index}].timeUs`), + }; + const endTimeUs = optionalFiniteNumber(record.endTimeUs, `chart.notes[${index}].endTimeUs`); + if (endTimeUs !== undefined) note.endTimeUs = endTimeUs; + if (record.lnMode !== undefined) { + if (record.lnMode !== 1 && record.lnMode !== 2 && record.lnMode !== 3) { + throw new PlaylogParseError(`chart.notes[${index}].lnMode: expected 1 | 2 | 3`); + } + note.lnMode = record.lnMode; + } + const damage = optionalFiniteNumber(record.damage, `chart.notes[${index}].damage`); + if (damage !== undefined) note.damage = damage; + return note; +} + +function parseInputs(value: unknown): PlaylogInputEvent[] { + if (!Array.isArray(value)) { + throw new PlaylogParseError('inputs: expected an array'); + } + return value.map((entry, index) => { + const record = expectRecord(entry, `inputs[${index}]`); + const action = record.action; + if (action !== 'down' && action !== 'up') { + throw new PlaylogParseError(`inputs[${index}].action: expected 'down' | 'up'`); + } + const channelsRaw = record.channels; + if (!Array.isArray(channelsRaw) || channelsRaw.some((channel) => typeof channel !== 'string')) { + throw new PlaylogParseError(`inputs[${index}].channels: expected string[]`); + } + const event: PlaylogInputEvent = { + seq: expectFiniteNumber(record.seq, `inputs[${index}].seq`), + timeUs: expectFiniteNumber(record.timeUs, `inputs[${index}].timeUs`), + action, + channels: channelsRaw as string[], + }; + if (record.tokens !== undefined) { + if (!Array.isArray(record.tokens) || record.tokens.some((token) => typeof token !== 'string')) { + throw new PlaylogParseError(`inputs[${index}].tokens: expected string[]`); + } + event.tokens = record.tokens as string[]; + } + return event; + }); +} + +const GAUGE_TYPES: ReadonlySet<string> = new Set(['GROOVE', 'HARD', 'DEATH', 'EASY']); + +function parsePlay(play: Record<string, unknown>): PlaylogPlay { + const mode = play.mode; + if (mode !== 'manual' && mode !== 'auto') { + throw new PlaylogParseError(`play.mode: expected 'manual' | 'auto'`); + } + const gauge = play.gauge; + if (typeof gauge !== 'string' || !GAUGE_TYPES.has(gauge)) { + throw new PlaylogParseError(`play.gauge: expected one of ${[...GAUGE_TYPES].join(' | ')}`); + } + const parsed: PlaylogPlay = { + mode, + autoScratch: play.autoScratch === true, + gauge: gauge as GrooveGaugeType, + }; + if (play.randomLane !== undefined) { + const randomLane = expectRecord(play.randomLane, 'play.randomLane'); + parsed.randomLane = {}; + const p1 = optionalString(randomLane.p1, 'play.randomLane.p1'); + const p2 = optionalString(randomLane.p2, 'play.randomLane.p2'); + if (p1 !== undefined) parsed.randomLane.p1 = p1; + if (p2 !== undefined) parsed.randomLane.p2 = p2; + } + if (play.dpFlip !== undefined) parsed.dpFlip = play.dpFlip === true; + const judgeWindowOverrideMs = optionalFiniteNumber(play.judgeWindowOverrideMs, 'play.judgeWindowOverrideMs'); + if (judgeWindowOverrideMs !== undefined) parsed.judgeWindowOverrideMs = judgeWindowOverrideMs; + if (play.judgeRuleset !== undefined) { + if (play.judgeRuleset !== 'lr2' && play.judgeRuleset !== 'beatoraja' && play.judgeRuleset !== 'iidx') { + throw new PlaylogParseError(`play.judgeRuleset: expected 'lr2' | 'beatoraja' | 'iidx'`); + } + parsed.judgeRuleset = play.judgeRuleset; + } + if (play.aborted !== undefined) parsed.aborted = play.aborted === true; + if (play.native !== undefined) { + const native = expectRecord(play.native, 'play.native'); + const copied: Record<string, string | number | boolean | null> = {}; + for (const [key, raw] of Object.entries(native)) { + if (raw === null || typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') { + copied[key] = raw; + } + } + parsed.native = copied; + } + return parsed; +} + +function parseResults(results: Record<string, unknown>): Record<string, PlaylogRulesetResult> { + const parsed: Record<string, PlaylogRulesetResult> = {}; + for (const [key, raw] of Object.entries(results)) { + const record = expectRecord(raw, `results.${key}`); + const judge = expectRecord(record.judge, `results.${key}.judge`); + const gauge = expectRecord(record.gauge, `results.${key}.gauge`); + const result: PlaylogRulesetResult = { + ruleset: expectString(record.ruleset, `results.${key}.ruleset`), + judge: { + pgreat: expectFiniteNumber(judge.pgreat, `results.${key}.judge.pgreat`), + great: expectFiniteNumber(judge.great, `results.${key}.judge.great`), + good: expectFiniteNumber(judge.good, `results.${key}.judge.good`), + bad: expectFiniteNumber(judge.bad, `results.${key}.judge.bad`), + poor: expectFiniteNumber(judge.poor, `results.${key}.judge.poor`), + emptyPoor: expectFiniteNumber(judge.emptyPoor, `results.${key}.judge.emptyPoor`), + }, + fast: expectFiniteNumber(record.fast, `results.${key}.fast`), + slow: expectFiniteNumber(record.slow, `results.${key}.slow`), + exScore: expectFiniteNumber(record.exScore, `results.${key}.exScore`), + noteCount: expectFiniteNumber(record.noteCount, `results.${key}.noteCount`), + maxCombo: expectFiniteNumber(record.maxCombo, `results.${key}.maxCombo`), + gauge: { + type: expectString(gauge.type, `results.${key}.gauge.type`), + final: expectFiniteNumber(gauge.final, `results.${key}.gauge.final`), + cleared: gauge.cleared === true, + }, + }; + if (gauge.failedMidPlay !== undefined) result.gauge.failedMidPlay = gauge.failedMidPlay === true; + const score = optionalFiniteNumber(record.score, `results.${key}.score`); + if (score !== undefined) result.score = score; + const djLevel = optionalString(record.djLevel, `results.${key}.djLevel`); + if (djLevel !== undefined) result.djLevel = djLevel; + parsed[key] = result; + } + return parsed; +} + +function expectRecord(value: unknown, path: string): Record<string, unknown> { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new PlaylogParseError(`${path}: expected an object`); + } + return value as Record<string, unknown>; +} + +function expectString(value: unknown, path: string): string { + if (typeof value !== 'string') { + throw new PlaylogParseError(`${path}: expected a string`); + } + return value; +} + +function optionalString(value: unknown, path: string): string | undefined { + if (value === undefined) return undefined; + return expectString(value, path); +} + +function expectFiniteNumber(value: unknown, path: string): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new PlaylogParseError(`${path}: expected a finite number`); + } + return value; +} + +function optionalFiniteNumber(value: unknown, path: string): number | undefined { + if (value === undefined) return undefined; + return expectFiniteNumber(value, path); +} diff --git a/packages/player/src/playlog/index.ts b/packages/player/src/playlog/index.ts new file mode 100644 index 00000000..e2373602 --- /dev/null +++ b/packages/player/src/playlog/index.ts @@ -0,0 +1,3 @@ +export * from './format.ts'; +export * from './recorder.ts'; +export * from './simulate.ts'; diff --git a/packages/player/src/playlog/recorder.test.ts b/packages/player/src/playlog/recorder.test.ts new file mode 100644 index 00000000..5b1e3286 --- /dev/null +++ b/packages/player/src/playlog/recorder.test.ts @@ -0,0 +1,222 @@ +import { createEmptyJson, type BeMusicEvent, type BeMusicJson } from '@be-music/json'; +import { describe, expect, test } from 'vitest'; +import type { PlayerSummary } from '../core/engine.ts'; +import { resolveLandmineGaugeEffect } from '../core/landmine.ts'; +import type { TimedLandmineNote, TimedPlayableNote } from '../playable-notes.ts'; +import { BE_MUSIC_PLAYLOG_FORMAT, BE_MUSIC_PLAYLOG_VERSION } from './format.ts'; +import { + createPlaylogRecorder, + PLAYLOG_NATIVE_RULESET_ID, + type PlaylogRecorderChartData, + type PlaylogRecorderOptions, +} from './recorder.ts'; + +function makeEvent(channel: string, value = '01'): BeMusicEvent { + return { measure: 0, channel, position: [0, 1], value }; +} + +function playable(channel: string, seconds: number, overrides: Partial<TimedPlayableNote> = {}): TimedPlayableNote { + return { event: makeEvent(channel), channel, beat: 0, seconds, judged: false, ...overrides }; +} + +function landmine(channel: string, seconds: number, value: string): TimedLandmineNote { + return { event: makeEvent(channel, value), channel, beat: 0, seconds, judged: false, mine: true }; +} + +function makeJson(): BeMusicJson { + const json = createEmptyJson('bms'); + json.metadata.title = 'My Song'; + json.metadata.artist = 'composer'; + json.metadata.total = 300; + json.metadata.rank = 2; + json.bms.lnMode = 2; + return json; +} + +function makeChartData(): PlaylogRecorderChartData { + const scorable = [ + playable('13', 0.25), + playable('11', 0.5), + playable('12', 1, { endSeconds: 2 }), + playable('18', 3, { endSeconds: 4, longNoteMode: 3 }), + ]; + return { + notes: [...scorable, playable('17', 1.5, { endSeconds: 2.5 })], + landmineNotes: [landmine('14', 0.75, '0A')], + invisibleNotes: [playable('15', 0.5, { invisible: true })], + activeFreeZoneChannels: new Set(['17']), + scorableNotes: scorable, + laneDisplayMode: '7keys', + }; +} + +function makeSummary(overrides: Partial<PlayerSummary> = {}): PlayerSummary { + return { + total: 4, + perfect: 2, + fast: 1, + slow: 1, + great: 1, + good: 1, + bad: 0, + poor: 0, + exScore: 5, + score: 123456, + ...overrides, + }; +} + +function makeRecorderOptions(overrides: Partial<PlaylogRecorderOptions> = {}): PlaylogRecorderOptions { + return { + json: makeJson(), + chart: makeChartData(), + play: { mode: 'manual', autoScratch: false }, + now: () => new Date('2026-08-17T00:00:00.000Z'), + ...overrides, + }; +} + +describe('playlog recorder', () => { + test('snapshots chart notes in (timeUs, channel, type) order with stable ids and resolved types', () => { + const recorder = createPlaylogRecorder(makeRecorderOptions()); + const playlog = recorder.finalize({ summary: makeSummary() }); + + expect(playlog.format).toBe(BE_MUSIC_PLAYLOG_FORMAT); + expect(playlog.version).toBe(BE_MUSIC_PLAYLOG_VERSION); + expect(playlog.createdAt).toBe('2026-08-17T00:00:00.000Z'); + expect(playlog.clock).toEqual({ unit: 'us', origin: 'chart-zero' }); + + expect(playlog.chart.notes).toEqual([ + { id: 0, channel: '13', type: 'normal', timeUs: 250_000 }, + { id: 1, channel: '11', type: 'normal', timeUs: 500_000 }, + { id: 2, channel: '15', type: 'invisible', timeUs: 500_000 }, + { id: 3, channel: '14', type: 'mine', timeUs: 750_000, damage: 10 }, + // The chart-level #LNMODE 2 fills in when the note has no own long-note mode. + { id: 4, channel: '12', type: 'long', timeUs: 1_000_000, endTimeUs: 2_000_000, lnMode: 2 }, + // Free-zone notes keep their tail but never carry an lnMode. + { id: 5, channel: '17', type: 'freezone', timeUs: 1_500_000, endTimeUs: 2_500_000 }, + { id: 6, channel: '18', type: 'long', timeUs: 3_000_000, endTimeUs: 4_000_000, lnMode: 3 }, + ]); + + const mineNote = playlog.chart.notes.find((note) => note.type === 'mine'); + expect(mineNote?.damage).toBe(resolveLandmineGaugeEffect({ value: '0A' }).damage); + }); + + test('captures chart metadata, noteCount, TOTAL, judge rank, and lnMode from the JSON', () => { + const recorder = createPlaylogRecorder(makeRecorderOptions()); + const playlog = recorder.finalize({ summary: makeSummary() }); + + expect(playlog.chart.sourceFormat).toBe('bms'); + expect(playlog.chart.laneMode).toBe('7keys'); + expect(playlog.chart.title).toBe('My Song'); + expect(playlog.chart.artist).toBe('composer'); + expect(playlog.chart.subtitle).toBeUndefined(); + expect(playlog.chart.total).toBe(300); + expect(playlog.chart.lnMode).toBe(2); + expect(playlog.chart.noteCount).toBe(4); // scorableNotes.length — mines / invisibles / freezones excluded + expect(playlog.chart.judgeRank).toEqual({ percent: 75, sourceRank: 2 }); + }); + + test('captures #DEFEXRANK and dynamic #EXRANK changes into judgeRank', () => { + const json = makeJson(); + json.metadata.rank = 1; + json.bms.defExRank = 120; + const recorder = createPlaylogRecorder( + makeRecorderOptions({ + json, + dynamicJudgeRankChanges: [{ seconds: 12.5, exRankValue: 48 }], + }), + ); + const playlog = recorder.finalize({ summary: makeSummary() }); + + expect(playlog.chart.judgeRank).toEqual({ + percent: 90, // #DEFEXRANK 120 × 75 / 100 — wins over #RANK 1 + sourceRank: 1, + sourceExRank: 120, + timeline: [{ timeUs: 12_500_000, exRankValue: 48 }], + }); + }); + + test('records inputs with microsecond timestamps, sequential seq, and copied channels', () => { + const recorder = createPlaylogRecorder(makeRecorderOptions()); + + // Inputs that resolve to no playable channel are not recorded (and consume no seq). + recorder.recordInput('down', 0.5, ['q'], new Set()); + + const sourceChannels = ['11']; + recorder.recordInput('down', 1.234567, ['z'], sourceChannels); + sourceChannels.push('19'); // later mutation must not leak into the recorded event + recorder.recordInput('up', 1.3, [], new Set(['11', '12'])); + + const playlog = recorder.finalize({ summary: makeSummary() }); + expect(playlog.inputs).toEqual([ + { seq: 0, timeUs: 1_234_567, action: 'down', channels: ['11'], tokens: ['z'] }, + { seq: 1, timeUs: 1_300_000, action: 'up', channels: ['11', '12'] }, + ]); + }); + + test('finalize caches the native result with judge counts, empty POORs, maxCombo, and gauge', () => { + const recorder = createPlaylogRecorder( + makeRecorderOptions({ + play: { + mode: 'manual', + autoScratch: true, + gauge: 'HARD', + randomLane: { p1: 'MIRROR' }, + dpFlip: true, + native: { hiSpeed: 2 }, + }, + }), + ); + recorder.recordEmptyPoor(); + recorder.recordEmptyPoor(); + + const summary = makeSummary({ + gauge: { + current: 88.5, + max: 100, + clearThreshold: 80, + initial: 20, + effectiveTotal: 300, + cleared: true, + type: 'GROOVE', + }, + }); + const playlog = recorder.finalize({ summary, maxCombo: 5, aborted: true }); + + expect(playlog.play).toEqual({ + mode: 'manual', + autoScratch: true, + gauge: 'HARD', + randomLane: { p1: 'MIRROR' }, + dpFlip: true, + aborted: true, + native: { hiSpeed: 2 }, + }); + expect(playlog.results).toEqual({ + native: { + ruleset: PLAYLOG_NATIVE_RULESET_ID, + judge: { pgreat: 2, great: 1, good: 1, bad: 0, poor: 0, emptyPoor: 2 }, + fast: 1, + slow: 1, + exScore: 5, + noteCount: 4, + maxCombo: 5, + score: 123456, + gauge: { type: 'GROOVE', final: 88.5, cleared: true }, + }, + }); + }); + + test('finalize defaults: GROOVE gauge, maxCombo 0, declared gauge when the summary has none', () => { + const recorder = createPlaylogRecorder(makeRecorderOptions()); + const playlog = recorder.finalize({ summary: makeSummary() }); + + expect(playlog.play).toEqual({ mode: 'manual', autoScratch: false, gauge: 'GROOVE' }); + expect(playlog.play.aborted).toBeUndefined(); + expect(playlog.results?.native).toMatchObject({ + maxCombo: 0, + gauge: { type: 'GROOVE', final: 0, cleared: false }, + }); + }); +}); diff --git a/packages/player/src/playlog/recorder.ts b/packages/player/src/playlog/recorder.ts new file mode 100644 index 00000000..ce7e3411 --- /dev/null +++ b/packages/player/src/playlog/recorder.ts @@ -0,0 +1,289 @@ +import { resolveBmsBase, type BeMusicJson } from '@be-music/json'; +import type { PlayerSummary } from '../core/engine.ts'; +import type { PreparedPlaybackChartData } from '../core/bootstrap.ts'; +import { resolveJudgeRankPercent } from '../core/judge-window.ts'; +import { resolveLandmineGaugeEffect } from '../core/landmine.ts'; +import type { GrooveGaugeType } from '../core/groove-gauge.ts'; +import type { LongNoteMode } from '../playable-notes.ts'; +import { + BE_MUSIC_PLAYLOG_FORMAT, + BE_MUSIC_PLAYLOG_VERSION, + type BeMusicPlaylog, + type PlaylogChart, + type PlaylogInputAction, + type PlaylogInputEvent, + type PlaylogNote, + type PlaylogPlay, + type PlaylogRulesetResult, +} from './format.ts'; + +/** Ruleset id used for the engine's own summary cached into `results` at record time. */ +export const PLAYLOG_NATIVE_RULESET_ID = 'be-music/native'; + +/** + * Chart data the recorder snapshots — structurally a subset of the engine's `PreparedPlaybackChartData`, so the + * engine can hand its prepared bundle over verbatim. + */ +export type PlaylogRecorderChartData = Pick< + PreparedPlaybackChartData, + 'notes' | 'landmineNotes' | 'invisibleNotes' | 'activeFreeZoneChannels' | 'scorableNotes' | 'laneDisplayMode' +>; + +export interface PlaylogRecorderPlaySettings { + mode: 'manual' | 'auto'; + autoScratch: boolean; + /** Selected gauge — declared by the host (the engine's own summary path always runs GROOVE today). */ + gauge?: GrooveGaugeType; + randomLane?: { p1?: string; p2?: string }; + dpFlip?: boolean; + judgeWindowOverrideMs?: number; + /** Judge-window ruleset the engine ran (`PlayerOptions.judgeRuleset`). Absent = `'lr2'`. */ + judgeRuleset?: 'lr2' | 'beatoraja' | 'iidx'; + native?: Record<string, string | number | boolean | null>; +} + +/** + * Host-declared subset of the recording inputs — the fields the engine cannot know by itself. Passed through + * `PlayerOptions.recordPlaylog`. `chartSha256` is the SHA-256 (lowercase hex) of the source chart FILE bytes; + * hosts that have the raw file compute it so dropped logs can be matched back to their chart by content. + */ +export type PlaylogRecordingOptions = Pick< + PlaylogRecorderPlaySettings, + 'gauge' | 'randomLane' | 'dpFlip' | 'native' +> & { + chartSha256?: string; +}; + +export interface PlaylogRecorderOptions { + /** Resolved chart JSON (post `#RANDOM` control flow) — metadata source. */ + json: BeMusicJson; + chart: PlaylogRecorderChartData; + /** SHA-256 (lowercase hex) of the source chart file bytes, when the host computed one. */ + chartSha256?: string; + play: PlaylogRecorderPlaySettings; + /** Dynamic `#EXRANKxx` changes the engine collected (chart order). */ + dynamicJudgeRankChanges?: ReadonlyArray<{ seconds: number; exRankValue: number }>; + /** Clock source for `createdAt` — injectable for tests. Defaults to `Date`. */ + now?: () => Date; +} + +export interface PlaylogRecorderFinalizeInput { + summary: PlayerSummary; + maxCombo?: number; + aborted?: boolean; +} + +export interface PlaylogRecorder { + recordInput( + action: PlaylogInputAction, + timeSeconds: number, + tokens: readonly string[], + channels: Iterable<string>, + ): void; + /** Counts an LR2-style empty POOR — cached into the native result for diagnostics. */ + recordEmptyPoor(): void; + finalize(input: PlaylogRecorderFinalizeInput): BeMusicPlaylog; +} + +export function createPlaylogRecorder(options: PlaylogRecorderOptions): PlaylogRecorder { + const chart = buildPlaylogChart(options); + const inputs: PlaylogInputEvent[] = []; + let seq = 0; + let emptyPoorCount = 0; + + return { + recordInput: (action, timeSeconds, tokens, channels): void => { + const channelList = [...channels]; + if (channelList.length === 0) { + return; + } + const event: PlaylogInputEvent = { + seq: seq++, + timeUs: secondsToMicroseconds(timeSeconds), + action, + channels: channelList, + }; + if (tokens.length > 0) { + event.tokens = [...tokens]; + } + inputs.push(event); + }, + recordEmptyPoor: (): void => { + emptyPoorCount += 1; + }, + finalize: ({ summary, maxCombo, aborted }): BeMusicPlaylog => { + const play: PlaylogPlay = { + mode: options.play.mode, + autoScratch: options.play.autoScratch, + gauge: options.play.gauge ?? 'GROOVE', + }; + if (options.play.randomLane !== undefined) play.randomLane = options.play.randomLane; + if (options.play.dpFlip !== undefined) play.dpFlip = options.play.dpFlip; + if (options.play.judgeWindowOverrideMs !== undefined) { + play.judgeWindowOverrideMs = options.play.judgeWindowOverrideMs; + } + if (options.play.judgeRuleset !== undefined) play.judgeRuleset = options.play.judgeRuleset; + if (aborted === true) play.aborted = true; + if (options.play.native !== undefined) play.native = options.play.native; + + const createdAt = (options.now?.() ?? new Date()).toISOString(); + return { + format: BE_MUSIC_PLAYLOG_FORMAT, + version: BE_MUSIC_PLAYLOG_VERSION, + createdAt, + clock: { unit: 'us', origin: 'chart-zero' }, + chart, + inputs, + play, + results: { + native: buildNativeResult(summary, maxCombo, emptyPoorCount, play.gauge, chart.noteCount), + }, + }; + }, + }; +} + +function buildPlaylogChart(options: PlaylogRecorderOptions): PlaylogChart { + const { json, chart } = options; + const idBase = resolveBmsBase(json); + const chartLnMode = resolveChartLongNoteMode(json); + const notes: PlaylogNote[] = []; + + for (const note of chart.notes) { + const isFreeZone = chart.activeFreeZoneChannels.has(note.channel); + const hasTail = + typeof note.endSeconds === 'number' && Number.isFinite(note.endSeconds) && note.endSeconds > note.seconds; + const entry: PlaylogNote = { + id: 0, + channel: note.channel, + type: isFreeZone ? 'freezone' : hasTail ? 'long' : 'normal', + timeUs: secondsToMicroseconds(note.seconds), + }; + if (hasTail) { + entry.endTimeUs = secondsToMicroseconds(note.endSeconds!); + if (!isFreeZone) { + entry.lnMode = note.longNoteMode ?? chartLnMode; + } + } + notes.push(entry); + } + for (const mine of chart.landmineNotes) { + notes.push({ + id: 0, + channel: mine.channel, + type: 'mine', + timeUs: secondsToMicroseconds(mine.seconds), + damage: resolveLandmineGaugeEffect(mine.event, idBase).damage, + }); + } + for (const invisible of chart.invisibleNotes) { + notes.push({ + id: 0, + channel: invisible.channel, + type: 'invisible', + timeUs: secondsToMicroseconds(invisible.seconds), + }); + } + notes.sort(comparePlaylogNotes); + for (let index = 0; index < notes.length; index += 1) { + notes[index]!.id = index; + } + + const judgeRank: PlaylogChart['judgeRank'] = { + percent: resolveJudgeRankPercent(json), + }; + const sourceRank = json.metadata.rank; + if (typeof sourceRank === 'number' && Number.isFinite(sourceRank)) { + judgeRank.sourceRank = sourceRank; + } + const sourceExRank = json.sourceFormat === 'bmson' ? json.bmson.info.judgeRank : json.bms.defExRank; + if (typeof sourceExRank === 'number' && Number.isFinite(sourceExRank)) { + judgeRank.sourceExRank = sourceExRank; + } + const timelineSource = options.dynamicJudgeRankChanges ?? []; + if (timelineSource.length > 0) { + judgeRank.timeline = timelineSource.map((change) => ({ + timeUs: secondsToMicroseconds(change.seconds), + exRankValue: change.exRankValue, + })); + } + + const result: PlaylogChart = { + sourceFormat: json.sourceFormat === 'bmson' ? 'bmson' : 'bms', + laneMode: chart.laneDisplayMode, + lnMode: chartLnMode, + judgeRank, + noteCount: chart.scorableNotes.length, + notes, + }; + if (typeof options.chartSha256 === 'string' && options.chartSha256.length > 0) { + result.sha256 = options.chartSha256.toLowerCase(); + } + if (typeof json.metadata.title === 'string' && json.metadata.title.length > 0) result.title = json.metadata.title; + if (typeof json.metadata.subtitle === 'string' && json.metadata.subtitle.length > 0) { + result.subtitle = json.metadata.subtitle; + } + if (typeof json.metadata.artist === 'string' && json.metadata.artist.length > 0) result.artist = json.metadata.artist; + if (typeof json.metadata.genre === 'string' && json.metadata.genre.length > 0) result.genre = json.metadata.genre; + if (typeof json.metadata.total === 'number' && Number.isFinite(json.metadata.total)) { + result.total = json.metadata.total; + } + return result; +} + +function buildNativeResult( + summary: PlayerSummary, + maxCombo: number | undefined, + emptyPoorCount: number, + declaredGauge: GrooveGaugeType, + noteCount: number, +): PlaylogRulesetResult { + const result: PlaylogRulesetResult = { + ruleset: PLAYLOG_NATIVE_RULESET_ID, + judge: { + pgreat: summary.perfect, + great: summary.great, + good: summary.good, + bad: summary.bad, + poor: summary.poor, + emptyPoor: emptyPoorCount, + }, + fast: summary.fast, + slow: summary.slow, + exScore: summary.exScore, + noteCount, + maxCombo: maxCombo ?? 0, + score: summary.score, + gauge: { + type: summary.gauge?.type ?? declaredGauge, + final: summary.gauge?.current ?? 0, + cleared: summary.gauge?.cleared ?? false, + }, + }; + return result; +} + +function resolveChartLongNoteMode(json: BeMusicJson): LongNoteMode { + if (json.sourceFormat === 'bms') { + return json.bms.lnMode === 2 || json.bms.lnMode === 3 ? json.bms.lnMode : 1; + } + const lnType = json.bmson.info.lnType; + return lnType === 2 || lnType === 3 ? lnType : 1; +} + +function comparePlaylogNotes(left: PlaylogNote, right: PlaylogNote): number { + if (left.timeUs !== right.timeUs) { + return left.timeUs - right.timeUs; + } + if (left.channel !== right.channel) { + return left.channel < right.channel ? -1 : 1; + } + if (left.type !== right.type) { + return left.type < right.type ? -1 : 1; + } + return 0; +} + +function secondsToMicroseconds(seconds: number): number { + return Math.round(seconds * 1_000_000); +} diff --git a/packages/player/src/playlog/rulesets.ts b/packages/player/src/playlog/rulesets.ts new file mode 100644 index 00000000..dd6b8673 --- /dev/null +++ b/packages/player/src/playlog/rulesets.ts @@ -0,0 +1,722 @@ +import { resolveBmsJudgeWindowsMsForPercent, bmsExRankValueToJudgeRankPercent } from '../core/judge-window.ts'; +import type { BeMusicPlaylog, PlaylogChart } from './format.ts'; + +/** + * Ruleset tables for the playlog simulators. + * + * Every numeric constant here was read from primary sources (2026-08 HEAD): + * - beatoraja: `exch-bms2/beatoraja` — `play/JudgeProperty.java`, `play/JudgeAlgorithm.java`, + * `play/JudgeManager.java`, `play/GaugeProperty.java`, `play/GrooveGauge.java`, `play/BMSPlayerRule.java`. + * - LR2: `wcko87/lr2oraja` / `seraxis/lr2oraja-endlessdream` (`JudgeProperty.LR2`, `JudgeWindowRule.LR2`, + * `GaugeProperty` LR2 variants with `death = 2` / guts `{32, 0.6}`, `MultiBadCollector`) cross-checked against + * the OpenLR2 transcription (`GOMazk/OpenLR2` — `Scene04_Play.cpp`, `LR2_bmsload.cpp`). + * - IIDX: community measurements (iidx.org compendium; dbm_capture / leisurely1 measurements). IIDX internals are + * not public — those values are the current community consensus, not vendor data. + * + * Window convention (borrowed from beatoraja): `dmTimeUs = noteTimeUs - inputTimeUs`, so POSITIVE deltas are + * EARLY (FAST) presses. A window is a `[lateBoundUs, earlyBoundUs]` pair with `lateBoundUs <= 0 <= earlyBoundUs` + * (except the LR2 empty-POOR window, which is early-only). + */ + +export type WindowPairUs = readonly [number, number]; + +export interface JudgeWindowSetUs { + /** PGREAT / GREAT / GOOD / BAD windows, inner to outer. */ + judges: readonly [WindowPairUs, WindowPairUs, WindowPairUs, WindowPairUs]; + /** Empty-POOR (空POOR) window, or undefined when the context has none (long-note ends). */ + ms?: WindowPairUs; +} + +export interface RulesetWindowTables { + note: JudgeWindowSetUs; + scratch: JudgeWindowSetUs; + longNoteEnd: JudgeWindowSetUs; + longScratchEnd: JudgeWindowSetUs; +} + +export type PlaylogRulesetId = 'lr2' | 'beatoraja' | 'iidx'; + +export type BeatorajaJudgeAlgorithm = 'combo' | 'duration' | 'lowest' | 'score'; + +/** How a ruleset plays the chart's long notes. */ +export type LongNoteStyle = + /** LR2: every long note is an LN — one deferred judgment, early release = BAD. */ + | 'ln' + /** beatoraja: per-note lnMode decides LN (1) vs CN (2) vs HCN (3). */ + | 'per-note' + /** IIDX: every long note is a CN (HCN when the chart says mode 3) — head and tail judged separately. */ + | 'charge'; + +export interface GaugeGutsStep { + /** Gauge value below (or at, when `inclusive`) which the damage multiplier applies. */ + threshold: number; + multiplier: number; + inclusive?: boolean; +} + +export interface GaugeSpec { + /** Ruleset-scoped gauge label reported in the result. */ + id: string; + min: number; + max: number; + initial: number; + /** Clear border; survival gauges use 0 (clear = survive). */ + border: number; + /** Survival gauges fail the moment they reach 0. */ + survival: boolean; + /** LR2 survival gauges: values below this collapse to 0 (death border). */ + death?: number; + /** Per-judge deltas in percent, `[PG, GR, GD, BD, missPOOR, emptyPOOR]`, AFTER TOTAL modifiers. */ + values: readonly [number, number, number, number, number, number]; + /** Low-gauge damage reduction steps (first match wins). */ + guts: readonly GaugeGutsStep[]; +} + +export interface RulesetConfig { + /** Versioned ruleset id written into the result (`'lr2/1'` etc.). */ + id: string; + rulesetId: PlaylogRulesetId; + windows: RulesetWindowTables; + /** + * LR2 dynamic `#EXRANKxx` support: returns the window tables active at `timeUs`. Rulesets without dynamic + * rank support return the static tables. + */ + windowsAt: (timeUs: number) => RulesetWindowTables; + selection: BeatorajaJudgeAlgorithm; + /** LR2 multi-BAD: one press BADs every other in-BAD-window (but out-of-GOOD-window) note on the lane. */ + multiBad: boolean; + /** LR2: a long-note head is never judged as a LATE bad — the press is ignored instead. */ + ignoreLateBadOnLnHead: boolean; + longNoteStyle: LongNoteStyle; + /** IIDX: a BAD / POOR on a charge-note head skips the tail judgment entirely. */ + headBadSkipsTail: boolean; + comboBreaksOnEmptyPoor: boolean; + /** HCN hold-state gauge tick interval (µs). */ + hcnTickUs: number; + /** HCN tick deltas as `[heldJudgeIndex, heldRate, releasedJudgeIndex, releasedRate]` (beatoraja model). */ + hcnTick: { heldJudge: number; heldRate: number; releasedJudge: number; releasedRate: number }; + gauge: GaugeSpec; + /** EX-SCORE denominator note count (longs count 1 for LN styles, 2 for charge styles). */ + noteCount: number; + /** Effective TOTAL after the ruleset's default formula. */ + effectiveTotal: number; + /** LR2 money score (`(4PG + 2GR + GD) × 50000 / notes`, floored). */ + moneyScore: boolean; +} + +export interface ResolveRulesetOptions { + /** Overrides the gauge picked from `playlog.play.gauge`. Ruleset-scoped id (see gauge tables). */ + gauge?: string; + /** beatoraja note-selection algorithm (default `'combo'`, beatoraja's own default). */ + judgeAlgorithm?: BeatorajaJudgeAlgorithm; +} + +const scale = (windows: { pgreat: number; great: number; good: number; bad: number }): JudgeWindowSetUs => ({ + judges: [ + [-windows.pgreat * 1000, windows.pgreat * 1000], + [-windows.great * 1000, windows.great * 1000], + [-windows.good * 1000, windows.good * 1000], + [-windows.bad * 1000, windows.bad * 1000], + ], + ms: [0, 1_000_000], +}); + +/** LR2 long-note end tolerance is the (rank-scaled) GOOD window on both ends (OpenLR2 `ProcLongnote`). */ +const lr2LongNoteEnd = (windows: { good: number; bad: number }): JudgeWindowSetUs => ({ + judges: [ + [-windows.good * 1000, windows.good * 1000], + [-windows.good * 1000, windows.good * 1000], + [-windows.good * 1000, windows.good * 1000], + [-windows.bad * 1000, windows.bad * 1000], + ], +}); + +function resolveLr2WindowTables(percent: number, overrideBadMs: number | undefined): RulesetWindowTables { + const windows = resolveBmsJudgeWindowsMsForPercent(percent, overrideBadMs); + const note = scale(windows); + const longEnd = lr2LongNoteEnd(windows); + return { note, scratch: note, longNoteEnd: longEnd, longScratchEnd: longEnd }; +} + +/** beatoraja judgerank percent per `BMSPlayerRule.validate` (NORMAL window rule). */ +function resolveBeatorajaJudgeRank(chart: PlaylogChart, rankTable: readonly number[]): number { + const exRank = chart.judgeRank.sourceExRank; + if (typeof exRank === 'number' && exRank > 0) { + if (chart.sourceFormat === 'bmson') { + return exRank; + } + // BMS #DEFEXRANK: value × (NORMAL-rule rank-2 percent) / 100. + return (exRank * rankTable[2]!) / 100; + } + const rank = chart.judgeRank.sourceRank; + if (typeof rank === 'number' && Number.isInteger(rank) && rank >= 0 && rank < rankTable.length) { + return rankTable[rank]!; + } + return rankTable[2]!; +} + +interface BeatorajaModeWindows { + note: readonly number[]; + scratch?: readonly number[]; + longNoteEnd: readonly number[]; + longScratchEnd?: readonly number[]; + rankTable: readonly number[]; + /** Which judge windows scale with judgerank (`[PG, GR, GD, BD]`; MS is always fixed). */ + scaled: readonly [boolean, boolean, boolean, boolean]; +} + +// beatoraja JudgeProperty flat tables: {PG late, PG early, GR..., GD..., BD..., MS late, MS early} in µs. +const BEATORAJA_MODES: Record<'FIVEKEYS' | 'SEVENKEYS' | 'PMS' | 'KEYBOARD', BeatorajaModeWindows> = { + FIVEKEYS: { + note: [-20000, 20000, -50000, 50000, -100000, 100000, -150000, 150000, -150000, 500000], + scratch: [-30000, 30000, -60000, 60000, -110000, 110000, -160000, 160000, -160000, 500000], + longNoteEnd: [-120000, 120000, -150000, 150000, -200000, 200000, -250000, 250000], + longScratchEnd: [-130000, 130000, -160000, 160000, -110000, 110000, -260000, 260000], + rankTable: [25, 50, 75, 100, 125], + scaled: [true, true, true, true], + }, + SEVENKEYS: { + note: [-20000, 20000, -60000, 60000, -150000, 150000, -280000, 220000, -150000, 500000], + scratch: [-30000, 30000, -70000, 70000, -160000, 160000, -290000, 230000, -160000, 500000], + longNoteEnd: [-120000, 120000, -160000, 160000, -200000, 200000, -280000, 220000], + longScratchEnd: [-130000, 130000, -170000, 170000, -210000, 210000, -290000, 230000], + rankTable: [25, 50, 75, 100, 125], + scaled: [true, true, true, true], + }, + PMS: { + note: [-20000, 20000, -50000, 50000, -117000, 117000, -183000, 183000, -175000, 500000], + longNoteEnd: [-120000, 120000, -150000, 150000, -217000, 217000, -283000, 283000], + rankTable: [33, 50, 70, 100, 133], + scaled: [false, true, true, false], + }, + KEYBOARD: { + note: [-30000, 30000, -90000, 90000, -200000, 200000, -320000, 240000, -200000, 650000], + longNoteEnd: [-160000, 25000, -200000, 75000, -260000, 140000, -320000, 240000], + rankTable: [25, 50, 75, 100, 125], + scaled: [true, true, true, true], + }, +}; + +function resolveBeatorajaMode(laneMode: string): BeatorajaModeWindows { + if (laneMode.startsWith('5') || laneMode.startsWith('10')) return BEATORAJA_MODES.FIVEKEYS; + if (laneMode.startsWith('9')) return BEATORAJA_MODES.PMS; + if (laneMode.startsWith('24') || laneMode.startsWith('48')) return BEATORAJA_MODES.KEYBOARD; + return BEATORAJA_MODES.SEVENKEYS; +} + +/** + * Applies beatoraja's `JudgeWindowRule.create` to one flat window table: scale the non-fixed judges by + * `judgerank / 100`, clamp every leg to the BAD leg, then enforce inner-to-outer monotonicity. + */ +function scaleBeatorajaWindows( + flat: readonly number[], + judgeRank: number, + scaled: readonly [boolean, boolean, boolean, boolean], +): JudgeWindowSetUs { + const judgeCount = flat.length >= 10 ? 5 : 4; + const legs: number[] = []; + for (let judge = 0; judge < 4; judge += 1) { + for (let side = 0; side < 2; side += 1) { + let value = flat[judge * 2 + side]!; + if (scaled[judge]) { + value = (value * judgeRank) / 100; + } + const badLeg = flat[3 * 2 + side]!; + if (Math.abs(value) > Math.abs(badLeg)) { + value = badLeg; + } + if (judge > 0 && Math.abs(value) < Math.abs(legs[(judge - 1) * 2 + side]!)) { + value = legs[(judge - 1) * 2 + side]!; + } + legs.push(Math.round(value)); + } + } + const set: JudgeWindowSetUs = { + judges: [ + [legs[0]!, legs[1]!], + [legs[2]!, legs[3]!], + [legs[4]!, legs[5]!], + [legs[6]!, legs[7]!], + ], + ...(judgeCount === 5 ? { ms: [flat[8]!, flat[9]!] as WindowPairUs } : {}), + }; + return set; +} + +function resolveBeatorajaWindowTables(chart: PlaylogChart): RulesetWindowTables { + const mode = resolveBeatorajaMode(chart.laneMode); + const judgeRank = resolveBeatorajaJudgeRank(chart, mode.rankTable); + const note = scaleBeatorajaWindows(mode.note, judgeRank, mode.scaled); + const scratch = mode.scratch ? scaleBeatorajaWindows(mode.scratch, judgeRank, mode.scaled) : note; + const longNoteEnd = scaleBeatorajaWindows(mode.longNoteEnd, judgeRank, mode.scaled); + const longScratchEnd = mode.longScratchEnd + ? scaleBeatorajaWindows(mode.longScratchEnd, judgeRank, mode.scaled) + : longNoteEnd; + return { note, scratch, longNoteEnd, longScratchEnd }; +} + +// IIDX community-consensus windows (iidx.org): PG ±1F / GR ±2F / GD ±7F / BD ±15F at 60 fps. +const IIDX_NOTE: JudgeWindowSetUs = { + judges: [ + [-16667, 16667], + [-33333, 33333], + [-116667, 116667], + [-250000, 250000], + ], + // The IIDX empty-POOR window has never been measured precisely; beatoraja's MS window (late 150 ms / + // early 500 ms) is used as the stand-in. + ms: [-150000, 500000], +}; +// IIDX CN release windows are "significantly wider" but unmeasured — beatoraja's SEVENKEYS long-note end +// windows are used as the stand-in. +const IIDX_LN_END: JudgeWindowSetUs = { + judges: [ + [-120000, 120000], + [-160000, 160000], + [-200000, 200000], + [-280000, 220000], + ], +}; + +const IIDX_WINDOWS: RulesetWindowTables = { + note: IIDX_NOTE, + scratch: IIDX_NOTE, + longNoteEnd: IIDX_LN_END, + longScratchEnd: IIDX_LN_END, +}; + +/** LR2 default TOTAL (OpenLR2 `LR2_bmsload.cpp`): piecewise-linear in the note count, ×0.8. */ +export function resolveLr2DefaultTotal(noteCount: number): number { + const n = Math.max(0, noteCount); + let base: number; + if (n < 400) { + base = n / 5 + 200; + } else if (n < 600) { + base = (n - 400) / 2.5 + 280; + } else { + base = (n - 600) / 5 + 360; + } + return base * 0.8; +} + +/** beatoraja default TOTAL (`BMSPlayerRule.calculateDefaultTotal`, keyboard modes excluded). */ +export function resolveBeatorajaDefaultTotal(noteCount: number): number { + const n = Math.max(1, noteCount); + return Math.max(260.0, (7.605 * n) / (0.01 * n + 6.5)); +} + +/** + * IIDX gauge recovery unit ("a value", percent per PGREAT/GREAT) — iidx.org measurement: `260 / n` up to 338 + * notes, `760.5 / (n + 650)` beyond, rounded to the nearest 0.02 %. + */ +export function resolveIidxGaugeUnit(noteCount: number): number { + const n = Math.max(1, noteCount); + const raw = n <= 338 ? 260 / n : 760.5 / (n + 650); + return Math.round(raw / 0.02 + 1e-9) * 0.02; +} + +/** + * lr2oraja `MODIFY_DAMAGE` — LR2 HARD/EX-HARD damage multiplier from TOTAL (fix1) and note count (fix2). + */ +export function resolveLr2HardDamageMultiplier(total: number, noteCount: number): number { + const fix1 = 10.0 / Math.min(10.0, Math.max(1.0, Math.floor(total / 16.0) - 5.0)); + const n = noteCount; + let fix2: number; + if (n <= 20) fix2 = 10.0; + else if (n < 30) fix2 = 8.0 + 0.2 * (30 - n); + else if (n < 60) fix2 = 5.0 + (0.2 * (60 - n)) / 3.0; + else if (n < 125) fix2 = 4.0 + (125 - n) / 65.0; + else if (n < 250) fix2 = 3.0 + 0.008 * (250 - n); + else if (n < 500) fix2 = 2.0 + 0.004 * (500 - n); + else if (n < 1000) fix2 = 1.0 + 0.002 * (1000 - n); + else fix2 = 1.0; + return Math.max(fix1, fix2); +} + +/** beatoraja `LIMIT_INCREMENT` — HARD/EX-HARD recovery scale from TOTAL and note count. */ +export function resolveBeatorajaHardRecoverMultiplier(total: number, noteCount: number): number { + const pg = Math.max(Math.min(0.15, (2 * total - 320) / Math.max(1, noteCount)), 0); + return pg / 0.15; +} + +type GaugeValues = readonly [number, number, number, number, number, number]; + +function totalModifier(values: GaugeValues, total: number, noteCount: number): GaugeValues { + return values.map((value) => + value > 0 ? (value * total) / Math.max(1, noteCount) : value, + ) as unknown as GaugeValues; +} + +function scalePositive(values: GaugeValues, multiplier: number): GaugeValues { + return values.map((value) => (value > 0 ? value * multiplier : value)) as unknown as GaugeValues; +} + +function scaleNegative(values: GaugeValues, multiplier: number): GaugeValues { + return values.map((value) => (value < 0 ? value * multiplier : value)) as unknown as GaugeValues; +} + +function resolveLr2Gauge(gaugeId: string, total: number, noteCount: number): GaugeSpec { + switch (gaugeId) { + case 'EASY': + return { + id: 'EASY', + min: 2, + max: 100, + initial: 20, + border: 80, + survival: false, + values: totalModifier([1.2, 1.2, 0.6, -3.2, -4.8, -1.6], total, noteCount), + guts: [], + }; + case 'HARD': { + const damage = resolveLr2HardDamageMultiplier(total, noteCount); + return { + id: 'HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + death: 2, + values: scaleNegative([0.1, 0.1, 0.05, -6.0, -10.0, -2.0], damage), + guts: [{ threshold: 32, multiplier: 0.6 }], + }; + } + case 'EX-HARD': { + const damage = resolveLr2HardDamageMultiplier(total, noteCount); + return { + id: 'EX-HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + death: 2, + values: scaleNegative([0.1, 0.1, 0.05, -12.0, -20.0, -2.0], damage), + guts: [], + }; + } + case 'DEATH': + // lr2oraja HAZARD_LR2 (beatoraja-derived values — LR2's own G-ATTACK/P-ATTACK family has no exact analog). + return { + id: 'DEATH', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + death: 2, + values: [0.15, 0.06, 0, -100, -100, -10], + guts: [], + }; + default: + return { + id: 'GROOVE', + min: 2, + max: 100, + initial: 20, + border: 80, + survival: false, + values: totalModifier([1.0, 1.0, 0.5, -4.0, -6.0, -2.0], total, noteCount), + guts: [], + }; + } +} + +const BEATORAJA_HARD_GUTS: readonly GaugeGutsStep[] = [ + { threshold: 10, multiplier: 0.4 }, + { threshold: 20, multiplier: 0.5 }, + { threshold: 30, multiplier: 0.6 }, + { threshold: 40, multiplier: 0.7 }, + { threshold: 50, multiplier: 0.8 }, +]; + +function resolveBeatorajaGauge(gaugeId: string, total: number, noteCount: number): GaugeSpec { + switch (gaugeId) { + case 'ASSIST-EASY': + return { + id: 'ASSIST-EASY', + min: 2, + max: 100, + initial: 20, + border: 60, + survival: false, + values: totalModifier([1.0, 1.0, 0.5, -1.5, -3.0, -0.5], total, noteCount), + guts: [], + }; + case 'EASY': + return { + id: 'EASY', + min: 2, + max: 100, + initial: 20, + border: 80, + survival: false, + values: totalModifier([1.0, 1.0, 0.5, -1.5, -4.5, -1.0], total, noteCount), + guts: [], + }; + case 'HARD': { + const recover = resolveBeatorajaHardRecoverMultiplier(total, noteCount); + return { + id: 'HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + values: scalePositive([0.15, 0.12, 0.03, -5.0, -10.0, -5.0], recover), + guts: BEATORAJA_HARD_GUTS, + }; + } + case 'EX-HARD': { + const recover = resolveBeatorajaHardRecoverMultiplier(total, noteCount); + return { + id: 'EX-HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + values: scalePositive([0.15, 0.06, 0, -8.0, -16.0, -8.0], recover), + guts: [], + }; + } + case 'HAZARD': + return { + id: 'HAZARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + values: [0.15, 0.06, 0, -100, -100, -10], + guts: [], + }; + default: + return { + id: 'NORMAL', + min: 2, + max: 100, + initial: 20, + border: 80, + survival: false, + values: totalModifier([1.0, 1.0, 0.5, -3.0, -6.0, -2.0], total, noteCount), + guts: [], + }; + } +} + +function resolveIidxGauge(gaugeId: string, noteCount: number): GaugeSpec { + const a = resolveIidxGaugeUnit(noteCount); + switch (gaugeId) { + case 'ASSISTED-EASY': + return { + id: 'ASSISTED-EASY', + min: 0, + max: 100, + initial: 22, + border: 60, + survival: false, + values: [a, a, a / 2, -1.6, -4.8, -1.6], + guts: [], + }; + case 'EASY': + return { + id: 'EASY', + min: 0, + max: 100, + initial: 22, + border: 80, + survival: false, + values: [a, a, a / 2, -1.6, -4.8, -1.6], + guts: [], + }; + case 'HARD': + return { + id: 'HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + // Low Life Adjustment: at or below 30 %, BAD / POOR damage is halved (iidx.org). + values: [0.16, 0.16, 0, -5, -9, -5], + guts: [{ threshold: 30, multiplier: 0.5, inclusive: true }], + }; + case 'EX-HARD': + return { + id: 'EX-HARD', + min: 0, + max: 100, + initial: 100, + border: 0, + survival: true, + values: [0.16, 0.16, 0, -10, -18, -10], + guts: [], + }; + default: + return { + id: 'NORMAL', + min: 0, + max: 100, + initial: 22, + border: 80, + survival: false, + values: [a, a, a / 2, -2, -6, -2], + guts: [], + }; + } +} + +/** Maps the play-log's LR2-family gauge pick onto each ruleset's own gauge id. */ +function resolveDefaultGaugeId(rulesetId: PlaylogRulesetId, playGauge: string): string { + switch (playGauge) { + case 'EASY': + return 'EASY'; + case 'HARD': + return 'HARD'; + case 'DEATH': + return rulesetId === 'lr2' ? 'DEATH' : rulesetId === 'beatoraja' ? 'HAZARD' : 'EX-HARD'; + default: + return rulesetId === 'lr2' ? 'GROOVE' : 'NORMAL'; + } +} + +function countRulesetNotes(chart: PlaylogChart, style: LongNoteStyle): number { + let count = 0; + for (const note of chart.notes) { + if (note.type === 'normal') { + count += 1; + } else if (note.type === 'long') { + const mode = note.lnMode ?? chart.lnMode; + const chargeHeadAndTail = style === 'charge' || (style === 'per-note' && (mode === 2 || mode === 3)); + count += chargeHeadAndTail ? 2 : 1; + } + } + return count; +} + +function resolveEffectiveTotal(rulesetId: PlaylogRulesetId, chart: PlaylogChart, baseNoteCount: number): number { + const raw = chart.total; + if (rulesetId === 'beatoraja') { + const fallback = resolveBeatorajaDefaultTotal(baseNoteCount); + if (chart.sourceFormat === 'bmson') { + // bmson `info.total` is a percentage of the default TOTAL in beatoraja. + return typeof raw === 'number' && raw > 0 ? (raw / 100) * fallback : fallback; + } + return typeof raw === 'number' && raw > 0 ? raw : fallback; + } + if (typeof raw === 'number' && raw > 0) { + return raw; + } + return resolveLr2DefaultTotal(baseNoteCount); +} + +/** Number of scorable base notes (longs count 1) — the TOTAL formulas all use this denominator. */ +function countBaseNotes(chart: PlaylogChart): number { + let count = 0; + for (const note of chart.notes) { + if (note.type === 'normal' || note.type === 'long') { + count += 1; + } + } + return count; +} + +export function resolveRulesetConfig( + playlog: BeMusicPlaylog, + rulesetId: PlaylogRulesetId, + options: ResolveRulesetOptions = {}, +): RulesetConfig { + const chart = playlog.chart; + const baseNotes = countBaseNotes(chart); + const gaugeId = options.gauge ?? resolveDefaultGaugeId(rulesetId, playlog.play.gauge); + const overrideBadMs = playlog.play.judgeWindowOverrideMs; + + if (rulesetId === 'lr2') { + const noteCount = countRulesetNotes(chart, 'ln'); + const effectiveTotal = resolveEffectiveTotal('lr2', chart, baseNotes); + const timeline = chart.judgeRank.timeline ?? []; + const staticTables = resolveLr2WindowTables(chart.judgeRank.percent, overrideBadMs); + const timelineTables = timeline.map((change) => ({ + timeUs: change.timeUs, + tables: resolveLr2WindowTables(bmsExRankValueToJudgeRankPercent(change.exRankValue), overrideBadMs), + })); + return { + id: 'lr2/1', + rulesetId: 'lr2', + windows: staticTables, + windowsAt: (timeUs) => { + let active = staticTables; + for (const change of timelineTables) { + if (change.timeUs <= timeUs) { + active = change.tables; + } else { + break; + } + } + return active; + }, + selection: 'lowest', + multiBad: true, + ignoreLateBadOnLnHead: true, + longNoteStyle: 'ln', + headBadSkipsTail: false, + comboBreaksOnEmptyPoor: false, + hcnTickUs: 200_000, + hcnTick: { heldJudge: 1, heldRate: 0.5, releasedJudge: 3, releasedRate: 0.5 }, + gauge: resolveLr2Gauge(gaugeId, effectiveTotal, baseNotes), + noteCount, + effectiveTotal, + moneyScore: true, + }; + } + + if (rulesetId === 'beatoraja') { + const noteCount = countRulesetNotes(chart, 'per-note'); + const effectiveTotal = resolveEffectiveTotal('beatoraja', chart, baseNotes); + const tables = resolveBeatorajaWindowTables(chart); + const mode = resolveBeatorajaMode(chart.laneMode); + // FIVEKEYS / PMS break combo on an empty POOR (JudgeProperty combo[MS] = false). + const comboBreaksOnEmptyPoor = mode === BEATORAJA_MODES.FIVEKEYS || mode === BEATORAJA_MODES.PMS; + return { + id: 'beatoraja/1', + rulesetId: 'beatoraja', + windows: tables, + windowsAt: () => tables, + selection: options.judgeAlgorithm ?? 'combo', + multiBad: false, + ignoreLateBadOnLnHead: false, + longNoteStyle: 'per-note', + headBadSkipsTail: false, + comboBreaksOnEmptyPoor, + hcnTickUs: 200_000, + hcnTick: { heldJudge: 1, heldRate: 0.5, releasedJudge: 3, releasedRate: 0.5 }, + gauge: resolveBeatorajaGauge(gaugeId, effectiveTotal, baseNotes), + noteCount, + effectiveTotal, + moneyScore: false, + }; + } + + const noteCount = countRulesetNotes(chart, 'charge'); + const effectiveTotal = resolveEffectiveTotal('lr2', chart, baseNotes); + return { + id: 'iidx/1', + rulesetId: 'iidx', + windows: IIDX_WINDOWS, + windowsAt: () => IIDX_WINDOWS, + selection: 'lowest', + multiBad: false, + ignoreLateBadOnLnHead: false, + longNoteStyle: 'charge', + headBadSkipsTail: true, + comboBreaksOnEmptyPoor: false, + hcnTickUs: 200_000, + hcnTick: { heldJudge: 0, heldRate: 1, releasedJudge: 5, releasedRate: 1 }, + gauge: resolveIidxGauge(gaugeId, noteCount), + noteCount, + effectiveTotal, + moneyScore: false, + }; +} diff --git a/packages/player/src/playlog/simulate.test.ts b/packages/player/src/playlog/simulate.test.ts new file mode 100644 index 00000000..de427fe3 --- /dev/null +++ b/packages/player/src/playlog/simulate.test.ts @@ -0,0 +1,340 @@ +import { describe, expect, test } from 'vitest'; +import { + BE_MUSIC_PLAYLOG_FORMAT, + BE_MUSIC_PLAYLOG_VERSION, + type BeMusicPlaylog, + type PlaylogInputEvent, + type PlaylogNote, +} from './format.ts'; +import { + resolveBeatorajaDefaultTotal, + resolveBeatorajaHardRecoverMultiplier, + resolveIidxGaugeUnit, + resolveLr2DefaultTotal, + resolveLr2HardDamageMultiplier, + simulatePlaylog, + simulatePlaylogRulesets, + type PlaylogRulesetId, +} from './simulate.ts'; + +function note(timeUs: number, overrides: Partial<PlaylogNote> = {}): PlaylogNote { + return { id: 0, channel: '11', type: 'normal', timeUs, ...overrides }; +} + +function down(timeUs: number, channels: string[] = ['11']): Omit<PlaylogInputEvent, 'seq'> { + return { timeUs, action: 'down', channels }; +} + +function up(timeUs: number, channels: string[] = ['11']): Omit<PlaylogInputEvent, 'seq'> { + return { timeUs, action: 'up', channels }; +} + +interface MakePlaylogOverrides { + notes?: PlaylogNote[]; + inputs?: Array<Omit<PlaylogInputEvent, 'seq'>>; + chart?: Partial<BeMusicPlaylog['chart']>; + play?: Partial<BeMusicPlaylog['play']>; +} + +function makePlaylog(overrides: MakePlaylogOverrides = {}): BeMusicPlaylog { + const notes = (overrides.notes ?? []).map((entry, index) => ({ ...entry, id: index })); + return { + format: BE_MUSIC_PLAYLOG_FORMAT, + version: BE_MUSIC_PLAYLOG_VERSION, + clock: { unit: 'us', origin: 'chart-zero' }, + chart: { + sourceFormat: 'bms', + laneMode: '7keys', + lnMode: 1, + // LR2 #RANK 2 (NORMAL): internal percent 75; beatoraja reads sourceRank 2 → judgerank 75 %. + judgeRank: { percent: 75, sourceRank: 2 }, + noteCount: notes.filter((entry) => entry.type === 'normal' || entry.type === 'long').length, + notes, + ...overrides.chart, + }, + inputs: (overrides.inputs ?? []).map((input, seq) => ({ ...input, seq })), + play: { mode: 'manual', autoScratch: false, gauge: 'GROOVE', ...overrides.play }, + }; +} + +/** One note at t = 1s, one press at `dmUs` before it (dm = noteTimeUs − inputTimeUs; positive = early). */ +function simulateSingleNote(ruleset: PlaylogRulesetId, dmUs: number) { + return simulatePlaylog(makePlaylog({ notes: [note(1_000_000)], inputs: [down(1_000_000 - dmUs)] }), { ruleset }); +} + +describe('simulatePlaylog', () => { + test('a single note hit dead-on scores PGREAT on every ruleset', () => { + const playlog = makePlaylog({ notes: [note(1_000_000)], inputs: [down(1_000_000)] }); + const results = simulatePlaylogRulesets(playlog); + for (const ruleset of ['lr2', 'beatoraja', 'iidx'] as const) { + const result = results[ruleset]!; + expect(result.judge, ruleset).toEqual({ pgreat: 1, great: 0, good: 0, bad: 0, poor: 0, emptyPoor: 0 }); + expect(result.exScore, ruleset).toBe(2); + expect(result.maxCombo, ruleset).toBe(1); + expect(result.noteCount, ruleset).toBe(1); + // A one-note chart's gauge gain saturates every normal gauge. + expect(result.gauge.final, ruleset).toBe(100); + expect(result.gauge.cleared, ruleset).toBe(true); + } + expect(results.lr2!.ruleset).toBe('lr2/1'); + expect(results.beatoraja!.ruleset).toBe('beatoraja/1'); + expect(results.iidx!.ruleset).toBe('iidx/1'); + expect(results.lr2!.score).toBe(200000); // (4 PG × 50000) / 1 note + expect(results.lr2!.djLevel).toBe('AAA'); + expect(results.beatoraja!.score).toBeUndefined(); // money score is LR2-only + }); + + test('LR2 RANK 2 windows: PGREAT ±18ms / GREAT ±40ms / GOOD ±100ms / BAD ±200ms, inclusive', () => { + expect(simulateSingleNote('lr2', 18_000).judge.pgreat).toBe(1); + + const fastGreat = simulateSingleNote('lr2', 19_000); + expect(fastGreat.judge.great).toBe(1); + expect(fastGreat.fast).toBe(1); // positive dm = early press = FAST + expect(fastGreat.slow).toBe(0); + + expect(simulateSingleNote('lr2', 40_000).judge.great).toBe(1); + expect(simulateSingleNote('lr2', 41_000).judge.good).toBe(1); + expect(simulateSingleNote('lr2', 100_000).judge.good).toBe(1); + + const bad = simulateSingleNote('lr2', 101_000); + expect(bad.judge.bad).toBe(1); + expect(bad.judge.poor).toBe(0); // the BAD consumed the note + + // Late presses mirror the early windows and count as SLOW. + const slowGreat = simulateSingleNote('lr2', -19_000); + expect(slowGreat.judge.great).toBe(1); + expect(slowGreat.slow).toBe(1); + expect(slowGreat.fast).toBe(0); + }); + + test('LR2: outside BAD but inside the early-only MS window is an empty POOR that leaves the note', () => { + const result = simulateSingleNote('lr2', 201_000); + expect(result.judge.emptyPoor).toBe(1); + expect(result.judge.bad).toBe(0); + expect(result.judge.poor).toBe(1); // the untouched note eventually misses + expect(result.maxCombo).toBe(0); + }); + + test('IIDX windows: ±16.67ms PGREAT / ±33.33ms GREAT / ±116.67ms GOOD / ±250ms BAD', () => { + expect(simulateSingleNote('iidx', 16_667).judge.pgreat).toBe(1); + expect(simulateSingleNote('iidx', 16_668).judge.great).toBe(1); + expect(simulateSingleNote('iidx', 33_333).judge.great).toBe(1); + expect(simulateSingleNote('iidx', 33_334).judge.good).toBe(1); + expect(simulateSingleNote('iidx', 116_667).judge.good).toBe(1); + expect(simulateSingleNote('iidx', 116_668).judge.bad).toBe(1); + expect(simulateSingleNote('iidx', 250_000).judge.bad).toBe(1); + + const beyondBad = simulateSingleNote('iidx', 250_001); + expect(beyondBad.judge.emptyPoor).toBe(1); + expect(beyondBad.judge.poor).toBe(1); + }); + + test('beatoraja #RANK 2 scales the SEVENKEYS PGREAT window to ±15ms (20ms × 0.75)', () => { + expect(simulateSingleNote('beatoraja', 15_000).judge.pgreat).toBe(1); + expect(simulateSingleNote('beatoraja', 15_001).judge.great).toBe(1); + expect(simulateSingleNote('beatoraja', -15_000).judge.pgreat).toBe(1); + }); + + test('unplayed notes miss as POOR and drain the gauge', () => { + const playlog = makePlaylog({ notes: [note(1_000_000), note(2_000_000)] }); + const results = simulatePlaylogRulesets(playlog); + for (const ruleset of ['lr2', 'beatoraja', 'iidx'] as const) { + const result = results[ruleset]!; + expect(result.judge.poor, ruleset).toBe(2); + expect(result.exScore, ruleset).toBe(0); + expect(result.maxCombo, ruleset).toBe(0); + expect(result.gauge.cleared, ruleset).toBe(false); + } + expect(results.lr2!.gauge.final).toBeCloseTo(8, 6); // 20 − 6 − 6 + }); + + test('a press 900ms early: LR2 empty POOR; beatoraja is outside its MS window and does nothing', () => { + const playlog = makePlaylog({ notes: [note(2_000_000)], inputs: [down(1_100_000)] }); + + const lr2 = simulatePlaylog(playlog, { ruleset: 'lr2' }); + expect(lr2.judge.emptyPoor).toBe(1); + expect(lr2.judge.poor).toBe(1); // the note itself still misses + + const beatoraja = simulatePlaylog(playlog, { ruleset: 'beatoraja' }); + expect(beatoraja.judge.emptyPoor).toBe(0); // MS window is only 500ms early + expect(beatoraja.judge.poor).toBe(1); + }); + + test('LR2 multi-BAD: one press BADs every note inside BAD but outside GOOD', () => { + const playlog = makePlaylog({ + notes: [note(1_000_000), note(1_050_000), note(1_100_000)], + inputs: [down(850_000)], // dm = +150ms / +200ms / +250ms + }); + + const lr2 = simulatePlaylog(playlog, { ruleset: 'lr2' }); + expect(lr2.judge.bad).toBe(2); // notes 1 & 2; note 3 sits beyond the ±200ms BAD window + expect(lr2.judge.poor).toBe(1); // note 3 eventually misses + expect(lr2.judge.emptyPoor).toBe(0); + expect(lr2.maxCombo).toBe(0); + + // beatoraja has no multi-BAD: the same press consumes exactly one note. + const beatoraja = simulatePlaylog(playlog, { ruleset: 'beatoraja' }); + expect(beatoraja.judge.bad).toBe(1); + expect(beatoraja.judge.poor).toBe(2); + }); + + test('beatoraja note selection: combo picks the in-GOOD-reach later note, lowest the earliest', () => { + // First note is 120ms late (outside GOOD ±112.5ms), second is 40ms early (GREAT). + const playlog = makePlaylog({ + notes: [note(1_000_000), note(1_160_000)], + inputs: [down(1_120_000)], + }); + + const combo = simulatePlaylog(playlog, { ruleset: 'beatoraja' }); // beatoraja default algorithm + expect(combo.judge).toEqual({ pgreat: 0, great: 1, good: 0, bad: 0, poor: 1, emptyPoor: 0 }); + + const lowest = simulatePlaylog(playlog, { ruleset: 'beatoraja', judgeAlgorithm: 'lowest' }); + expect(lowest.judge).toEqual({ pgreat: 0, great: 0, good: 0, bad: 1, poor: 1, emptyPoor: 0 }); + }); + + test('LN (mode 1): head press held to the tail judges exactly once', () => { + for (const ruleset of ['lr2', 'beatoraja'] as const) { + const heldToEnd = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000, { type: 'long', endTimeUs: 2_000_000, lnMode: 1 })], + inputs: [down(1_000_000)], // never released — the hold survives to the tail + }), + { ruleset }, + ); + expect(heldToEnd.judge, ruleset).toEqual({ pgreat: 1, great: 0, good: 0, bad: 0, poor: 0, emptyPoor: 0 }); + expect(heldToEnd.exScore, ruleset).toBe(2); + expect(heldToEnd.noteCount, ruleset).toBe(1); + expect(heldToEnd.maxCombo, ruleset).toBe(1); + + const releasedAfterEnd = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000, { type: 'long', endTimeUs: 2_000_000, lnMode: 1 })], + inputs: [down(1_000_000), up(2_100_000)], + }), + { ruleset }, + ); + expect(releasedAfterEnd.judge.pgreat, ruleset).toBe(1); + expect(releasedAfterEnd.exScore, ruleset).toBe(2); + } + }); + + test('LN (mode 1): an early release is a single BAD', () => { + const result = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000, { type: 'long', endTimeUs: 2_000_000, lnMode: 1 })], + inputs: [down(1_000_000), up(1_200_000)], // released 800ms before the tail + }), + { ruleset: 'lr2' }, + ); + expect(result.judge).toEqual({ pgreat: 0, great: 0, good: 0, bad: 1, poor: 0, emptyPoor: 0 }); + expect(result.exScore).toBe(0); + }); + + test('CN (mode 2): beatoraja judges head and tail; noteCount is style-dependent', () => { + const playlog = makePlaylog({ + notes: [note(1_000_000, { type: 'long', endTimeUs: 2_000_000, lnMode: 2 })], + inputs: [down(1_000_000), up(2_000_000)], + }); + + const beatoraja = simulatePlaylog(playlog, { ruleset: 'beatoraja' }); + expect(beatoraja.judge.pgreat).toBe(2); + expect(beatoraja.exScore).toBe(4); + expect(beatoraja.noteCount).toBe(2); // charge style counts head + tail + expect(beatoraja.maxCombo).toBe(2); + + // LR2 plays the same chart as an LN: one deferred judgment. + const lr2 = simulatePlaylog(playlog, { ruleset: 'lr2' }); + expect(lr2.judge.pgreat).toBe(1); + expect(lr2.exScore).toBe(2); + expect(lr2.noteCount).toBe(1); + }); + + test('IIDX: a BAD on a charge-note head skips the tail judgment', () => { + const result = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000, { type: 'long', endTimeUs: 2_000_000 })], + inputs: [down(800_000)], // +200ms: inside BAD, outside GOOD + }), + { ruleset: 'iidx' }, + ); + expect(result.judge.bad).toBe(1); + expect(result.judge.poor).toBe(0); // no tail judgment at all + expect(result.judge.pgreat).toBe(0); + expect(result.noteCount).toBe(2); // the denominator still counts head + tail + }); + + test('HARD fails mid-play at 0 and stays failed; GROOVE bottoms out at its 2 % floor', () => { + const hard = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000), note(3_000_000)], + inputs: [down(850_000), down(2_850_000)], // two +150ms BADs at ×10 damage (tiny note count) + play: { gauge: 'HARD' }, + }), + { ruleset: 'lr2' }, + ); + expect(hard.judge.bad).toBe(2); + expect(hard.gauge.type).toBe('HARD'); + expect(hard.gauge.final).toBe(0); + expect(hard.gauge.failedMidPlay).toBe(true); + expect(hard.gauge.cleared).toBe(false); + + const groove = simulatePlaylog( + makePlaylog({ + notes: [note(1_000_000), note(2_000_000), note(3_000_000), note(4_000_000), note(5_000_000)], + }), + { ruleset: 'lr2' }, + ); + expect(groove.judge.poor).toBe(5); + expect(groove.gauge.final).toBe(2); // GROOVE's soft floor — it never dies + expect(groove.gauge.failedMidPlay).toBeUndefined(); + expect(groove.gauge.cleared).toBe(false); + }); + + test('auto play scores every note PGREAT and clears', () => { + const result = simulatePlaylog(makePlaylog({ notes: [note(1_000_000), note(2_000_000)], play: { mode: 'auto' } }), { + ruleset: 'lr2', + }); + expect(result.judge).toEqual({ pgreat: 2, great: 0, good: 0, bad: 0, poor: 0, emptyPoor: 0 }); + expect(result.maxCombo).toBe(2); + expect(result.gauge.final).toBe(100); + expect(result.gauge.cleared).toBe(true); + }); + + test('autoScratch auto-plays the scratch channel without inputs', () => { + const notes = [note(1_000_000, { channel: '16' })]; + + const auto = simulatePlaylog(makePlaylog({ notes, play: { autoScratch: true } }), { ruleset: 'lr2' }); + expect(auto.judge.pgreat).toBe(1); + expect(auto.judge.poor).toBe(0); + + const manual = simulatePlaylog(makePlaylog({ notes }), { ruleset: 'lr2' }); + expect(manual.judge.pgreat).toBe(0); + expect(manual.judge.poor).toBe(1); + }); +}); + +describe('ruleset helpers', () => { + test('resolveLr2DefaultTotal follows the OpenLR2 piecewise formula (×0.8)', () => { + expect(resolveLr2DefaultTotal(0)).toBeCloseTo(160, 9); + expect(resolveLr2DefaultTotal(500)).toBeCloseTo(256, 9); // ((500 − 400) / 2.5 + 280) × 0.8 + expect(resolveLr2DefaultTotal(1000)).toBeCloseTo(352, 9); // ((1000 − 600) / 5 + 360) × 0.8 + }); + + test('resolveBeatorajaDefaultTotal floors at 260', () => { + expect(resolveBeatorajaDefaultTotal(10)).toBe(260); + expect(resolveBeatorajaDefaultTotal(2000)).toBeCloseTo((7.605 * 2000) / (0.01 * 2000 + 6.5), 9); // ≈ 573.96 + }); + + test('resolveIidxGaugeUnit rounds to the nearest 0.02 %', () => { + expect(resolveIidxGaugeUnit(100)).toBeCloseTo(2.6, 9); // 260 / 100 + expect(resolveIidxGaugeUnit(338)).toBeCloseTo(0.76, 9); // 260 / 338 ≈ 0.769 → 0.76 + expect(resolveIidxGaugeUnit(1000)).toBeCloseTo(0.46, 9); // 760.5 / 1650 ≈ 0.461 → 0.46 + }); + + test('LR2 hard damage / beatoraja hard recovery multipliers', () => { + expect(resolveLr2HardDamageMultiplier(240, 1000)).toBe(1); // fix1 = 1, fix2 = 1 + expect(resolveLr2HardDamageMultiplier(160, 2)).toBe(10); // tiny charts hit the fix2 = 10 cap + expect(resolveBeatorajaHardRecoverMultiplier(300, 2000)).toBeCloseTo(0.14 / 0.15, 9); + expect(resolveBeatorajaHardRecoverMultiplier(160, 100)).toBe(0); // 2 × TOTAL − 320 = 0 + }); +}); diff --git a/packages/player/src/playlog/simulate.ts b/packages/player/src/playlog/simulate.ts new file mode 100644 index 00000000..a72e4b5c --- /dev/null +++ b/packages/player/src/playlog/simulate.ts @@ -0,0 +1,721 @@ +import { resolveIidxRankLabel } from '../core/scoring.ts'; +import type { BeMusicPlaylog, PlaylogInputEvent, PlaylogRulesetResult } from './format.ts'; +import { + resolveRulesetConfig, + type GaugeSpec, + type JudgeWindowSetUs, + type PlaylogRulesetId, + type ResolveRulesetOptions, + type RulesetConfig, + type RulesetWindowTables, +} from './rulesets.ts'; + +export type { PlaylogRulesetId, BeatorajaJudgeAlgorithm, ResolveRulesetOptions } from './rulesets.ts'; +export { + resolveRulesetConfig, + resolveLr2DefaultTotal, + resolveBeatorajaDefaultTotal, + resolveIidxGaugeUnit, + resolveLr2HardDamageMultiplier, + resolveBeatorajaHardRecoverMultiplier, +} from './rulesets.ts'; + +export interface SimulatePlaylogOptions extends ResolveRulesetOptions { + ruleset: PlaylogRulesetId; +} + +export const PLAYLOG_SIMULATOR_RULESETS: readonly PlaylogRulesetId[] = ['lr2', 'beatoraja', 'iidx']; + +/** + * Re-derives one ruleset's judgments / EX-SCORE / combo / gauge from a playlog's canonical data (resolved chart + + * raw input stream + play settings). Pure function — never mutates the playlog. + * + * The LR2 simulation follows lr2oraja / OpenLR2 semantics, the beatoraja simulation the current beatoraja master, + * and the IIDX simulation the community-measured behavior of recent arcade versions (with documented stand-ins + * where no measurement exists — see `rulesets.ts`). + */ +export function simulatePlaylog(playlog: BeMusicPlaylog, options: SimulatePlaylogOptions): PlaylogRulesetResult { + const config = resolveRulesetConfig(playlog, options.ruleset, options); + return new PlaylogSimulation(playlog, config).run(); +} + +/** Runs {@link simulatePlaylog} for each requested ruleset (default: LR2, beatoraja, IIDX). */ +export function simulatePlaylogRulesets( + playlog: BeMusicPlaylog, + rulesets: readonly PlaylogRulesetId[] = PLAYLOG_SIMULATOR_RULESETS, + options: ResolveRulesetOptions = {}, +): Record<string, PlaylogRulesetResult> { + const results: Record<string, PlaylogRulesetResult> = {}; + for (const ruleset of rulesets) { + results[ruleset] = simulatePlaylog(playlog, { ...options, ruleset }); + } + return results; +} + +// Judge indices follow beatoraja: 0 PG / 1 GR / 2 GD / 3 BD / 4 missed POOR / 5 empty POOR. +const JUDGE_PGREAT = 0; +const JUDGE_GREAT = 1; +const JUDGE_GOOD = 2; +const JUDGE_BAD = 3; +const JUDGE_MISS_POOR = 4; +const JUDGE_EMPTY_POOR = 5; +/** Selection-time marker: no window matched. */ +const JUDGE_NONE = 6; + +type SimLongStyle = 1 | 2 | 3; + +interface SimNote { + timeUs: number; + endTimeUs?: number; + scratch: boolean; + isLong: boolean; + /** Resolved long-note style for this ruleset (1 LN / 2 CN / 3 HCN). */ + longStyle?: SimLongStyle; + judged: boolean; + holding: boolean; + /** Miss-sweep deadline (input time past which the head can no longer be judged). */ + missDeadlineUs: number; +} + +interface SimMine { + timeUs: number; + damage: number; + applied: boolean; +} + +interface ActiveHold { + note: SimNote; + /** Deferred LN head judge (style 1). */ + headJudge?: number; + headDmUs?: number; + /** IIDX charge heads that BAD/POOR'd skip the tail — such notes never become holds. */ + /** Held-past-end deadline for charge tails. */ + tailMissDeadlineUs: number; + /** Signed HCN tick accumulator (µs). */ + hcnCounterUs: number; +} + +interface SimLane { + channel: string; + scratch: boolean; + notes: SimNote[]; + mines: SimMine[]; + sweepCursor: number; + mineCursor: number; + held: boolean; + hold?: ActiveHold; + /** Auto-played lane (AUTO mode or AUTO SCRATCH): every note resolves PGREAT on schedule. */ + auto: boolean; + autoCursor: number; +} + +class SimGauge { + value: number; + failedMidPlay = false; + private dead = false; + + constructor(private readonly spec: GaugeSpec) { + this.value = spec.initial; + } + + update(judgeIndex: number, rate = 1): void { + if (this.dead) return; + let delta = this.spec.values[judgeIndex]! * rate; + if (delta < 0) { + for (const step of this.spec.guts) { + if (step.inclusive === true ? this.value <= step.threshold : this.value < step.threshold) { + delta *= step.multiplier; + break; + } + } + } + this.set(this.value + delta); + } + + addRaw(delta: number): void { + if (this.dead) return; + this.set(this.value + delta); + } + + cleared(): boolean { + if (this.spec.survival) { + return !this.failedMidPlay && this.value > 0; + } + return this.value >= this.spec.border; + } + + private set(next: number): void { + let value = Math.min(this.spec.max, Math.max(this.spec.min, next)); + if (this.spec.death !== undefined && value < this.spec.death) { + value = 0; + } + if (this.spec.survival && value <= 0) { + value = 0; + this.dead = true; + this.failedMidPlay = true; + } + this.value = value; + } +} + +interface SelectionCandidate { + lane: SimLane; + note: SimNote; + dmUs: number; + /** 0..3 scoreable, 4 pending-in-MS-window, 5 judged-in-MS-window. */ + judge: number; +} + +class PlaylogSimulation { + private readonly lanes = new Map<string, SimLane>(); + private readonly counts = [0, 0, 0, 0, 0, 0]; + private readonly gauge: SimGauge; + private combo = 0; + private maxCombo = 0; + private fast = 0; + private slow = 0; + private exScore = 0; + private lastAdvanceUs = Number.NEGATIVE_INFINITY; + + constructor( + private readonly playlog: BeMusicPlaylog, + private readonly config: RulesetConfig, + ) { + this.gauge = new SimGauge(config.gauge); + this.buildLanes(); + } + + run(): PlaylogRulesetResult { + const inputs = [...this.playlog.inputs].sort((left, right) => left.timeUs - right.timeUs || left.seq - right.seq); + for (const input of inputs) { + this.advanceTime(input.timeUs); + if (input.action === 'down') { + this.handleDown(input); + } else { + this.handleUp(input); + } + } + this.advanceTime(Number.POSITIVE_INFINITY); + + const result: PlaylogRulesetResult = { + ruleset: this.config.id, + judge: { + pgreat: this.counts[JUDGE_PGREAT]!, + great: this.counts[JUDGE_GREAT]!, + good: this.counts[JUDGE_GOOD]!, + bad: this.counts[JUDGE_BAD]!, + poor: this.counts[JUDGE_MISS_POOR]!, + emptyPoor: this.counts[JUDGE_EMPTY_POOR]!, + }, + fast: this.fast, + slow: this.slow, + exScore: this.exScore, + noteCount: this.config.noteCount, + maxCombo: this.maxCombo, + djLevel: resolveIidxRankLabel(this.exScore, this.config.noteCount), + gauge: { + type: this.config.gauge.id, + final: Math.round(this.gauge.value * 100) / 100, + cleared: this.gauge.cleared(), + }, + }; + if (this.gauge.failedMidPlay) { + result.gauge.failedMidPlay = true; + } + if (this.config.moneyScore) { + const notes = Math.max(1, this.config.noteCount); + result.score = Math.floor( + ((4 * this.counts[JUDGE_PGREAT]! + 2 * this.counts[JUDGE_GREAT]! + this.counts[JUDGE_GOOD]!) * 50000) / notes, + ); + } + return result; + } + + private buildLanes(): void { + const autoPlay = this.playlog.play.mode === 'auto'; + const autoScratch = this.playlog.play.autoScratch; + for (const note of this.playlog.chart.notes) { + if (note.type === 'invisible' || note.type === 'freezone') { + continue; + } + const lane = this.laneFor(note.channel, autoPlay, autoScratch); + if (note.type === 'mine') { + lane.mines.push({ timeUs: note.timeUs, damage: note.damage ?? 0, applied: false }); + continue; + } + const isLong = + note.type === 'long' && typeof note.endTimeUs === 'number' && note.endTimeUs > note.timeUs ? true : false; + const longStyle = isLong ? this.resolveLongStyle(note.lnMode ?? this.playlog.chart.lnMode) : undefined; + const windows = this.config.windowsAt(note.timeUs); + const noteWindows = lane.scratch ? windows.scratch : windows.note; + const simNote: SimNote = { + timeUs: note.timeUs, + scratch: lane.scratch, + isLong, + judged: false, + holding: false, + missDeadlineUs: note.timeUs - noteWindows.judges[JUDGE_BAD]![0], + }; + if (isLong) { + simNote.endTimeUs = note.endTimeUs; + simNote.longStyle = longStyle; + } + lane.notes.push(simNote); + } + for (const lane of this.lanes.values()) { + lane.notes.sort((left, right) => left.timeUs - right.timeUs); + lane.mines.sort((left, right) => left.timeUs - right.timeUs); + } + } + + private laneFor(channel: string, autoPlay: boolean, autoScratch: boolean): SimLane { + let lane = this.lanes.get(channel); + if (!lane) { + const scratch = channel === '16' || channel === '26'; + lane = { + channel, + scratch, + notes: [], + mines: [], + sweepCursor: 0, + mineCursor: 0, + held: false, + auto: autoPlay || (autoScratch && scratch), + autoCursor: 0, + }; + this.lanes.set(channel, lane); + } + return lane; + } + + private resolveLongStyle(chartMode: SimLongStyle): SimLongStyle { + switch (this.config.longNoteStyle) { + case 'ln': + return 1; + case 'charge': + return chartMode === 3 ? 3 : 2; + default: + return chartMode; + } + } + + // ---- time advancement -------------------------------------------------------------------------------------- + + private advanceTime(untilUs: number): void { + interface TimedEvent { + timeUs: number; + kind: 'miss' | 'mine' | 'hold-deadline' | 'auto'; + lane: SimLane; + note?: SimNote; + mine?: SimMine; + } + const events: TimedEvent[] = []; + for (const lane of this.lanes.values()) { + if (lane.auto) { + for (let index = lane.autoCursor; index < lane.notes.length; index += 1) { + const note = lane.notes[index]!; + if (note.timeUs >= untilUs) break; + // LN-style longs score once at the tail; everything else at the head. (Charge-style auto applies the + // head and tail PGREATs together at the head — a simplification that only shifts gauge gain earlier.) + const autoAt = note.isLong && note.longStyle === 1 ? note.endTimeUs! : note.timeUs; + if (autoAt >= untilUs) continue; + events.push({ timeUs: autoAt, kind: 'auto', lane, note }); + } + } else { + for (let index = lane.sweepCursor; index < lane.notes.length; index += 1) { + const note = lane.notes[index]!; + if (note.missDeadlineUs >= untilUs) break; + if (!note.judged && !note.holding) { + events.push({ timeUs: note.missDeadlineUs, kind: 'miss', lane, note }); + } + } + if (lane.hold && lane.hold.tailMissDeadlineUs < untilUs) { + events.push({ timeUs: lane.hold.tailMissDeadlineUs, kind: 'hold-deadline', lane, note: lane.hold.note }); + } + } + for (let index = lane.mineCursor; index < lane.mines.length; index += 1) { + const mine = lane.mines[index]!; + if (mine.timeUs >= untilUs) break; + events.push({ timeUs: mine.timeUs, kind: 'mine', lane, mine }); + } + } + events.sort((left, right) => left.timeUs - right.timeUs); + + for (const event of events) { + if (event.kind === 'auto') { + const note = event.note!; + if (note.judged) continue; + note.judged = true; + this.applyJudge(JUDGE_PGREAT, undefined); + if (note.isLong && note.longStyle !== 1) { + // Charge styles judge head and tail; auto play scores both as PGREAT. + this.applyJudge(JUDGE_PGREAT, undefined); + } + } else if (event.kind === 'miss') { + const note = event.note!; + if (note.judged || note.holding) continue; + note.judged = true; + this.applyJudge(JUDGE_MISS_POOR, undefined); + if (note.isLong && note.longStyle !== 1 && !this.config.headBadSkipsTail) { + // beatoraja: a missed CN/HCN head also POORs the tail. IIDX (headBadSkipsTail) skips it. + this.applyJudge(JUDGE_MISS_POOR, undefined); + } + } else if (event.kind === 'mine') { + const mine = event.mine!; + if (mine.applied) continue; + mine.applied = true; + if (event.lane.held && mine.damage > 0) { + this.gauge.addRaw(-mine.damage); + } + } else { + // Charge tail held past its late window — the tail resolves as a missed POOR (beatoraja / IIDX). + const lane = event.lane; + const hold = lane.hold; + if (!hold || hold.note !== event.note) continue; + this.integrateHcn(lane, event.timeUs); + lane.hold = undefined; + hold.note.holding = false; + hold.note.judged = true; + this.applyJudge(JUDGE_MISS_POOR, undefined); + } + } + + // LN (style 1) holds confirm their deferred head judgment once the tail time is reached while still held. + for (const lane of this.lanes.values()) { + const hold = lane.hold; + if (hold && hold.note.longStyle === 1 && hold.note.endTimeUs! < untilUs) { + this.integrateHcn(lane, hold.note.endTimeUs!); + lane.hold = undefined; + hold.note.holding = false; + hold.note.judged = true; + this.applyJudge(hold.headJudge ?? JUDGE_BAD, hold.headDmUs); + } + this.integrateHcn(lane, untilUs); + } + + // Advance sweep cursors past settled notes. + for (const lane of this.lanes.values()) { + while (lane.sweepCursor < lane.notes.length) { + const note = lane.notes[lane.sweepCursor]!; + if (note.judged || (note.missDeadlineUs < untilUs && !note.holding)) { + lane.sweepCursor += 1; + } else { + break; + } + } + while (lane.autoCursor < lane.notes.length && lane.notes[lane.autoCursor]!.judged) { + lane.autoCursor += 1; + } + while (lane.mineCursor < lane.mines.length && lane.mines[lane.mineCursor]!.applied) { + lane.mineCursor += 1; + } + } + this.lastAdvanceUs = untilUs; + } + + /** Integrates the beatoraja-model HCN hold ticks for a lane up to `untilUs`. */ + private integrateHcn(lane: SimLane, untilUs: number): void { + const hold = lane.hold; + if (!hold || hold.note.longStyle !== 3) return; + const from = Math.max(hold.note.timeUs, this.lastAdvanceUs === Number.NEGATIVE_INFINITY ? 0 : this.lastAdvanceUs); + const to = Math.min(untilUs, hold.note.endTimeUs!); + if (!(to > from)) return; + let dt = to - from; + const tick = this.config.hcnTickUs; + if (lane.held) { + hold.hcnCounterUs += dt; + while (hold.hcnCounterUs > tick) { + this.gauge.update(this.config.hcnTick.heldJudge, this.config.hcnTick.heldRate); + hold.hcnCounterUs -= tick; + } + } else { + hold.hcnCounterUs -= dt; + while (hold.hcnCounterUs < -tick) { + this.gauge.update(this.config.hcnTick.releasedJudge, this.config.hcnTick.releasedRate); + hold.hcnCounterUs += tick; + } + } + } + + // ---- input handling ---------------------------------------------------------------------------------------- + + private handleDown(input: PlaylogInputEvent): void { + const timeUs = input.timeUs; + const windows = this.config.windowsAt(timeUs); + const lanes = this.resolveInputLanes(input); + for (const lane of lanes) { + lane.held = true; + } + if (lanes.length === 0) { + return; + } + + const selected = this.selectCandidate(lanes, timeUs, windows); + if (!selected) { + return; + } + if (selected.judge >= JUDGE_MISS_POOR) { + // Empty POOR — the note is never consumed. + this.applyJudge(JUDGE_EMPTY_POOR, undefined); + return; + } + + const { note, lane, dmUs } = selected; + if (note.isLong) { + this.startLongNote(lane, note, selected.judge, dmUs); + } else { + note.judged = true; + this.applyJudge(selected.judge, dmUs); + } + if (this.config.multiBad) { + this.applyMultiBad(lanes, timeUs, windows, note, selected.judge); + } + } + + private handleUp(input: PlaylogInputEvent): void { + const timeUs = input.timeUs; + for (const lane of this.resolveInputLanes(input)) { + lane.held = false; + const hold = lane.hold; + if (!hold) continue; + this.integrateHcn(lane, timeUs); + const note = hold.note; + const endWindows = note.scratch + ? this.config.windowsAt(timeUs).longScratchEnd + : this.config.windowsAt(timeUs).longNoteEnd; + const dmUs = note.endTimeUs! - timeUs; + const endJudge = classifyJudge(dmUs, endWindows); + lane.hold = undefined; + note.holding = false; + note.judged = true; + if (note.longStyle === 1) { + // LN: worse of head and tail; an early release outside the GOOD reach is a BAD. + let judge = Math.max(endJudge === JUDGE_NONE ? JUDGE_MISS_POOR : endJudge, hold.headJudge ?? JUDGE_BAD); + if (judge >= JUDGE_BAD && dmUs > 0) { + judge = JUDGE_BAD; + } + const worseDm = hold.headDmUs !== undefined && Math.abs(hold.headDmUs) > Math.abs(dmUs) ? hold.headDmUs : dmUs; + this.applyJudge(Math.min(judge, JUDGE_MISS_POOR), worseDm); + } else { + // CN / HCN tail: judged by the release timing; early releases beyond the windows are POOR. + const judge = endJudge === JUDGE_NONE ? JUDGE_MISS_POOR : endJudge; + this.applyJudge(judge, dmUs); + } + } + } + + private resolveInputLanes(input: PlaylogInputEvent): SimLane[] { + const lanes: SimLane[] = []; + for (const channel of input.channels) { + const lane = this.lanes.get(channel); + if (lane && !lane.auto) { + lanes.push(lane); + } + } + return lanes; + } + + private startLongNote(lane: SimLane, note: SimNote, judge: number, dmUs: number): void { + const style = note.longStyle ?? 1; + if (style === 1) { + note.holding = true; + lane.hold = { + note, + headJudge: judge, + headDmUs: dmUs, + tailMissDeadlineUs: Number.POSITIVE_INFINITY, + hcnCounterUs: 0, + }; + return; + } + // Charge styles score the head immediately. + this.applyJudge(judge, dmUs); + if (this.config.headBadSkipsTail && judge >= JUDGE_BAD) { + // IIDX: a BAD head cancels the tail judgment entirely. + note.judged = true; + return; + } + note.holding = true; + const endWindows = note.scratch ? this.config.windows.longScratchEnd : this.config.windows.longNoteEnd; + lane.hold = { + note, + tailMissDeadlineUs: note.endTimeUs! - endWindows.judges[JUDGE_BAD]![0], + hcnCounterUs: 0, + }; + } + + private selectCandidate( + lanes: readonly SimLane[], + timeUs: number, + windows: RulesetWindowTables, + ): SelectionCandidate | undefined { + const candidates: SelectionCandidate[] = []; + for (const lane of lanes) { + const set = lane.scratch ? windows.scratch : windows.note; + const scanLate = Math.min(set.judges[JUDGE_BAD]![0], set.ms?.[0] ?? 0); + const scanEarly = Math.max(set.judges[JUDGE_BAD]![1], set.ms?.[1] ?? 0); + for (const note of lane.notes) { + const dmUs = note.timeUs - timeUs; + if (dmUs < scanLate) continue; + if (dmUs > scanEarly) break; + if (note.holding) continue; + let judge: number; + if (note.judged) { + judge = set.ms && dmUs >= set.ms[0] && dmUs <= set.ms[1] ? JUDGE_EMPTY_POOR : JUDGE_NONE; + } else { + judge = classifyJudge(dmUs, set); + if (judge === JUDGE_NONE && set.ms && dmUs >= set.ms[0] && dmUs <= set.ms[1]) { + judge = JUDGE_MISS_POOR; + } + if ( + this.config.ignoreLateBadOnLnHead && + judge === JUDGE_BAD && + dmUs < 0 && + note.isLong && + (note.longStyle ?? 1) === 1 + ) { + // LR2: long-note heads have no late BAD — the press falls through. + judge = JUDGE_NONE; + } + } + if (judge === JUDGE_NONE) continue; + candidates.push({ lane, note, dmUs, judge }); + } + } + if (candidates.length === 0) { + return undefined; + } + candidates.sort((left, right) => left.note.timeUs - right.note.timeUs); + + let best: SelectionCandidate | undefined; + for (const candidate of candidates) { + if (!best) { + best = candidate; + continue; + } + const bestScoreable = best.judge < JUDGE_MISS_POOR; + const candScoreable = candidate.judge < JUDGE_MISS_POOR; + if (!bestScoreable) { + if (candScoreable) { + best = candidate; + } else if (Math.abs(candidate.dmUs) < Math.abs(best.dmUs)) { + best = candidate; + } + continue; + } + if (!candScoreable) { + continue; + } + if (this.shouldSwitchCandidate(best, candidate, timeUs, windows)) { + best = candidate; + } + } + return best; + } + + private shouldSwitchCandidate( + best: SelectionCandidate, + candidate: SelectionCandidate, + timeUs: number, + windows: RulesetWindowTables, + ): boolean { + const algorithm = this.config.selection; + if (algorithm === 'lowest') { + return false; + } + if (algorithm === 'duration') { + return Math.abs(candidate.dmUs) < Math.abs(best.dmUs); + } + // combo (GOOD reach) / score (GREAT reach) — beatoraja JudgeAlgorithm semantics. + const judgeIndex = algorithm === 'combo' ? JUDGE_GOOD : JUDGE_GREAT; + const bestSet = windowSetFor(best, windows); + const candidateSet = windowSetFor(candidate, windows); + return ( + best.note.timeUs < timeUs + bestSet.judges[judgeIndex]![0] && + candidate.note.timeUs <= timeUs + candidateSet.judges[judgeIndex]![1] + ); + } + + /** + * lr2oraja `MultiBadCollector`: after the press consumed `tnote`, every other unjudged note inside the BAD + * window but outside the GOOD window also resolves as a BAD — with the collector's own pruning rules. + */ + private applyMultiBad( + lanes: readonly SimLane[], + timeUs: number, + windows: RulesetWindowTables, + tnote: SimNote, + tnoteJudge: number, + ): void { + const extras: Array<{ note: SimNote }> = []; + for (const lane of lanes) { + const set = lane.scratch ? windows.scratch : windows.note; + const badWindow = set.judges[JUDGE_BAD]!; + const goodWindow = set.judges[JUDGE_GOOD]!; + for (const note of lane.notes) { + const dmUs = note.timeUs - timeUs; + if (dmUs < badWindow[0]) continue; + if (dmUs > badWindow[1]) break; + if (note.judged || note.holding || note === tnote) continue; + if (dmUs >= goodWindow[0] && dmUs <= goodWindow[1]) continue; + extras.push({ note }); + } + } + if (extras.length === 0) return; + extras.sort((left, right) => left.note.timeUs - right.note.timeUs); + const tnoteIsBad = tnoteJudge === JUDGE_BAD; + const filtered = extras.filter(({ note }) => { + if ((!tnoteIsBad || tnote.isLong) && note.timeUs > tnote.timeUs) return false; + if (note.isLong && note.timeUs < tnote.timeUs) return false; + return true; + }); + for (const { note } of filtered) { + note.judged = true; + this.applyJudge(JUDGE_BAD, undefined); + if (note.isLong && (note.longStyle ?? 1) !== 1 && !this.config.headBadSkipsTail) { + this.applyJudge(JUDGE_MISS_POOR, undefined); + } + } + } + + private applyJudge(judgeIndex: number, dmUs: number | undefined): void { + this.counts[judgeIndex]! += 1; + if (judgeIndex === JUDGE_PGREAT) { + this.exScore += 2; + } else if (judgeIndex === JUDGE_GREAT) { + this.exScore += 1; + } + if (judgeIndex <= JUDGE_GOOD) { + this.combo += 1; + if (this.combo > this.maxCombo) { + this.maxCombo = this.combo; + } + } else if (judgeIndex === JUDGE_BAD || judgeIndex === JUDGE_MISS_POOR) { + this.combo = 0; + } else if (judgeIndex === JUDGE_EMPTY_POOR && this.config.comboBreaksOnEmptyPoor) { + this.combo = 0; + } + if (dmUs !== undefined && (judgeIndex === JUDGE_GREAT || judgeIndex === JUDGE_GOOD)) { + if (dmUs > 0) { + this.fast += 1; + } else { + this.slow += 1; + } + } + this.gauge.update(judgeIndex); + } +} + +function classifyJudge(dmUs: number, set: JudgeWindowSetUs): number { + for (let index = 0; index < set.judges.length; index += 1) { + const window = set.judges[index]!; + if (dmUs >= window[0] && dmUs <= window[1]) { + return index; + } + } + return JUDGE_NONE; +} + +function windowSetFor(candidate: SelectionCandidate, windows: RulesetWindowTables): JudgeWindowSetUs { + return candidate.lane.scratch ? windows.scratch : windows.note; +} diff --git a/packages/player/tsdown.config.ts b/packages/player/tsdown.config.ts index 631a044e..c565279f 100644 --- a/packages/player/tsdown.config.ts +++ b/packages/player/tsdown.config.ts @@ -12,6 +12,7 @@ export default createPackageTsdownConfig({ 'image-resize-algorithm': 'src/image-resize-algorithm.ts', judging: 'src/judging.ts', 'playable-notes': 'src/playable-notes.ts', + playlog: 'src/playlog/index.ts', 'core/bga-timeline': 'src/core/bga-timeline.ts', 'core/engine': 'src/core/engine.ts', 'core/groove-gauge': 'src/core/groove-gauge.ts', diff --git a/packages/stringifier/package.json b/packages/stringifier/package.json index 01645d4a..93bb9698 100644 --- a/packages/stringifier/package.json +++ b/packages/stringifier/package.json @@ -24,7 +24,7 @@ "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsx --tsconfig ../../tsconfig.typecheck.json src/cli.ts", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/packages/utils/package.json b/packages/utils/package.json index 184700ee..9fd4a2af 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -55,7 +55,7 @@ "build:bundle": "tsdown", "build": "tsdown", "clean": "rimraf dist tsconfig.tsbuildinfo", - "typecheck": "tsgo -p tsconfig.typecheck.json --pretty", + "typecheck": "tsc -p tsconfig.typecheck.json --pretty", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "format": "oxfmt . --write", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e4f97cc..099c857a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,41 +9,41 @@ importers: .: devDependencies: '@changesets/cli': - specifier: ^2.31.0 - version: 2.31.0(@types/node@25.9.1) + specifier: ^2.31.1 + version: 2.31.1(@types/node@26.2.0) '@types/node': - specifier: ^25.9.1 - version: 25.9.1 - '@typescript/native-preview': - specifier: 7.0.0-dev.20260611.2 - version: 7.0.0-dev.20260611.2 + specifier: ^26.2.0 + version: 26.2.0 '@vitest/coverage-v8': - specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4)) + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) oxfmt: - specifier: ^0.54.0 - version: 0.54.0 + specifier: ^0.57.0 + version: 0.57.0 oxlint: - specifier: ^1.69.0 - version: 1.69.0 + specifier: ^1.77.0 + version: 1.78.0 rimraf: specifier: ^6.0.1 version: 6.1.3 tinybench: - specifier: ^6.0.2 - version: 6.0.2 + specifier: ^6.1.3 + version: 6.1.3 tsdown: - specifier: ^0.22.2 - version: 0.22.2(@typescript/native-preview@7.0.0-dev.20260611.2)(tsx@4.22.4)(unrun@0.2.37) + specifier: ^0.22.14 + version: 0.22.14(tsx@4.23.12)(typescript@7.0.2)(unrun@0.2.37) tsx: - specifier: ^4.22.4 - version: 4.22.4 + specifier: ^4.23.12 + version: 4.23.12 + typescript: + specifier: ^7.0.2 + version: 7.0.2 vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4) + specifier: ^8.1.3 + version: 8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12) vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4) + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)) packages/audio-renderer: dependencies: @@ -156,8 +156,8 @@ importers: specifier: ^3.2.1 version: 3.2.1 node-web-audio-api: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.2.0 + version: 2.2.0 packages/player-tui: dependencies: @@ -265,20 +265,20 @@ importers: version: 0.21.0 devDependencies: '@cloudflare/workers-types': - specifier: ^4.20260611.1 - version: 4.20260611.1 + specifier: ^5.20260813.1 + version: 5.20260817.1 ts-ebml: specifier: ^3.0.2 version: 3.0.2 vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4) + specifier: ^8.1.3 + version: 8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12) vite-plugin-node-polyfills: specifier: ^0.28.0 - version: 0.28.0(rollup@4.59.0)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4)) + version: 0.28.0(rollup@4.59.0)(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)) wrangler: - specifier: ^4.100.0 - version: 4.100.0(@cloudflare/workers-types@4.20260611.1) + specifier: ^4.107.0 + version: 4.107.0(@cloudflare/workers-types@5.20260817.1) packages/stringifier: dependencies: @@ -306,36 +306,19 @@ importers: packages: - '@babel/generator@8.0.0-rc.6': - resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@8.0.0-rc.6': - resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.0-rc.6': - resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@8.0.0-rc.6': - resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} - engines: {node: ^22.18.0 || >=24.11.0} - hasBin: true - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -344,10 +327,6 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@babel/types@8.0.0-rc.6': - resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -361,8 +340,8 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.31.0': - resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} hasBin: true '@changesets/config@3.1.4': @@ -420,38 +399,38 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260611.1': - resolution: {integrity: sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==} + '@cloudflare/workerd-darwin-64@1.20260701.1': + resolution: {integrity: sha512-Zd9Y1bah6DwwBN2RW8vJohffQrIUazb8UXnqSNecOxM+jJLhUuvv5IOG8dbHcV83TyZAubea6gsQXo2yH1lDdw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260611.1': - resolution: {integrity: sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==} + '@cloudflare/workerd-darwin-arm64@1.20260701.1': + resolution: {integrity: sha512-yBLsjS1qCWqFyCY37qRUrYfzHHvMGvjh8zRKJ6MvUivYDhkZTzqduppK38FoqYvayLJ5KbcxH7zo5rkxGqbsaA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260611.1': - resolution: {integrity: sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==} + '@cloudflare/workerd-linux-64@1.20260701.1': + resolution: {integrity: sha512-vMfqSIMfoo4xmZXEuUVqLpSFS921YKjiR9q7kDXPi6Vld1PK74UHg9LZuBavT2KSyemHUCTpj9y/4JSYOEyQbQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260611.1': - resolution: {integrity: sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==} + '@cloudflare/workerd-linux-arm64@1.20260701.1': + resolution: {integrity: sha512-HRfwbKU2pK44V2NhoM0+iH0JJSj7nQ9Wv13ifIiGYCmTtDL8/zKtEhX7kQ3D4Vy/Cpjhttl0FkfqXj1aqLDPPg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260611.1': - resolution: {integrity: sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==} + '@cloudflare/workerd-windows-64@1.20260701.1': + resolution: {integrity: sha512-ngxCiIN9s/fM2o1IBMD0o1/mcXrv2NJVdyznh51UH8sQuvrTrXvV2nM0Uj/qU2wMwF6prgNBcdcd7AZeZGiBQA==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260611.1': - resolution: {integrity: sha512-DLiz8Ol1OIWLigJ+dGvuQ5Nm66D/CHNPasl8YnPiz6fGo10ggYSIVuEDMlFk6oho+piAHstNmZMl088w8xqW6g==} + '@cloudflare/workers-types@5.20260817.1': + resolution: {integrity: sha512-5Dv+cyjusBTPLMRedUCiLJu3zqeeupgyn1QcHmpHJV9k/TjQAxx79AO8RVtpjVaO2CB+Oh3ywAp1UuNpbyDjGQ==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -460,479 +439,176 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.0': - resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1122,9 +798,6 @@ packages: '@types/node': optional: true - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1144,14 +817,8 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1171,252 +838,252 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@oxc-project/types@0.134.0': - resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - '@oxfmt/binding-android-arm-eabi@0.54.0': - resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} + '@oxfmt/binding-android-arm-eabi@0.57.0': + resolution: {integrity: sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.54.0': - resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} + '@oxfmt/binding-android-arm64@0.57.0': + resolution: {integrity: sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.54.0': - resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} + '@oxfmt/binding-darwin-arm64@0.57.0': + resolution: {integrity: sha512-T+0stuCBqmUVY+aMIvrgXhzGhHO3sD5tNiiEcYqgSdPsnukskQqn2u5qOVD0sv1l7RLdFS5Z/f5Wi9Ktyjr3Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.54.0': - resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} + '@oxfmt/binding-darwin-x64@0.57.0': + resolution: {integrity: sha512-O+3JbqWs/mCI2oi4xfhRO2IVPFJNDDEBV8Odo+ZpmsUOeKJfjXoNH7nDmBEQcDgK7NfjDIyE7kRgYSZcTLDO0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.54.0': - resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} + '@oxfmt/binding-freebsd-x64@0.57.0': + resolution: {integrity: sha512-pxwhxVC+JkLX9twOQ/8C/vbuOQcMZyKIDmiRDZfO7yITuVcIdZCiLRqqf4QOxb2+8FWrRXzQpm+1DBKcMpHSSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': - resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': + resolution: {integrity: sha512-pxBU4zH2imB/MDBfth2rOMeVxXUMjRQLCazagwLARIFH3hVlxZJBlM4nSnHXaIHJK4/qezoFCIORN6AY8Mra4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.54.0': - resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': + resolution: {integrity: sha512-JAprOzt8tycYou36ZgEw14DlRHTiN8qdtKANdV3VZIRIvTI/lh/cX13c9pJ/EnDk2GT3FASH7KvCgQ2AufAifQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.54.0': - resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} + '@oxfmt/binding-linux-arm64-gnu@0.57.0': + resolution: {integrity: sha512-ajtjaxSaj9xl4BW7REt+Cef/ttzbAq00Bq4z7JUDZEfgFXdwSjH8K9bF+IcIJzZB9lKqMfQ4eHuSFOvvlvtqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.54.0': - resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} + '@oxfmt/binding-linux-arm64-musl@0.57.0': + resolution: {integrity: sha512-p4Y/+RYk9Bk5WO+zHSUXAClRmZ2fbJCejMuCAsU2HhyME4jqf6Ftt/mJYEwIah1wGCBDYOB7wEGV1x5bCEZ6hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.54.0': - resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': + resolution: {integrity: sha512-By6tRALAZsno0F4zedmtG+wdMvJiJmJoXM4d3+A9zHE4HRXLqXITwRH8mgrlcXc5yJM2g2W3riRPwTYdgemZLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.54.0': - resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': + resolution: {integrity: sha512-skYeG+RgvyzspqVEBsEprL90OYYZfoVNqB3HcCNR6QDJyXKOzfDRT3zncnHmUaFluIlBHuY23mU1b5WGgR98hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.54.0': - resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} + '@oxfmt/binding-linux-riscv64-musl@0.57.0': + resolution: {integrity: sha512-FFgACrZOXAXUh5KQh2mt1CDOVOZmn+QzHP71wM9QobNwyQvoFfyAeefVUltW83g3sm7LTiH3yfFqLLVUpA5ZFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.54.0': - resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} + '@oxfmt/binding-linux-s390x-gnu@0.57.0': + resolution: {integrity: sha512-Nm/BAOfQeFiiKd502mZn/GAVKJwtd0RdCg17G3Wz/WSOIQmDi3+7/SZH4BHn1Ye5KvTVH3ua8WvfwLLycNIuvA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.54.0': - resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} + '@oxfmt/binding-linux-x64-gnu@0.57.0': + resolution: {integrity: sha512-BiSy5Ku3mQqyxS6YIqAJgd403wEUWvI7kerfzPxc2l/txZVmZM0pSj7oDM+4bGBExowxOi7o73jEam1W0EDTZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.54.0': - resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} + '@oxfmt/binding-linux-x64-musl@0.57.0': + resolution: {integrity: sha512-BCRkJiotz5s9afLYD2LuMvzAoDYx9H17E/YbDyu4xK7l4zHDPeny9ErSXL//i/nJyaOwRk08x4b8cgJC00+JDg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.54.0': - resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} + '@oxfmt/binding-openharmony-arm64@0.57.0': + resolution: {integrity: sha512-4Oaxe1qrGgXfpCJ1C/ERJ2iCtV2rN1R79ga9fsfyVHfSQRu/hVW780u2KDqZWFZ/iGTHODJji0JemxqFZ63eIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.54.0': - resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} + '@oxfmt/binding-win32-arm64-msvc@0.57.0': + resolution: {integrity: sha512-MYLAsDnhdNsSGheLYhWgbk0vfIrlS84iQYun/y21fX6u0jj8iBtYtbpZMdiqYeuf8U12eVPUjVY2xE2NrCfJ0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.54.0': - resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} + '@oxfmt/binding-win32-ia32-msvc@0.57.0': + resolution: {integrity: sha512-PBwdzZALJY/jcCx2E6is0yu+cuVXeySTDmwuseD+9j0mHqlRNxwlKgsyRTBed/woPeqfVfuXfWjoq4Cx2Zt3Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.54.0': - resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} + '@oxfmt/binding-win32-x64-msvc@0.57.0': + resolution: {integrity: sha512-bQJdH9i4RRfw55jm7+8/xS7GzHLLTbHx4huhrrDxQJaJtbSDbsyOnODvP1ftT7EG0KFKAYO2S+q6AcioXODx8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.69.0': - resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==} + '@oxlint/binding-android-arm-eabi@1.78.0': + resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.69.0': - resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==} + '@oxlint/binding-android-arm64@1.78.0': + resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.69.0': - resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==} + '@oxlint/binding-darwin-arm64@1.78.0': + resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.69.0': - resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==} + '@oxlint/binding-darwin-x64@1.78.0': + resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.69.0': - resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==} + '@oxlint/binding-freebsd-x64@1.78.0': + resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.69.0': - resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==} + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.69.0': - resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==} + '@oxlint/binding-linux-arm-musleabihf@1.78.0': + resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.69.0': - resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==} + '@oxlint/binding-linux-arm64-gnu@1.78.0': + resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.69.0': - resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==} + '@oxlint/binding-linux-arm64-musl@1.78.0': + resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.69.0': - resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==} + '@oxlint/binding-linux-ppc64-gnu@1.78.0': + resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.69.0': - resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==} + '@oxlint/binding-linux-riscv64-gnu@1.78.0': + resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.69.0': - resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==} + '@oxlint/binding-linux-riscv64-musl@1.78.0': + resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.69.0': - resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==} + '@oxlint/binding-linux-s390x-gnu@1.78.0': + resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.69.0': - resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==} + '@oxlint/binding-linux-x64-gnu@1.78.0': + resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.69.0': - resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==} + '@oxlint/binding-linux-x64-musl@1.78.0': + resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.69.0': - resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==} + '@oxlint/binding-openharmony-arm64@1.78.0': + resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.69.0': - resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==} + '@oxlint/binding-win32-arm64-msvc@1.78.0': + resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.69.0': - resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==} + '@oxlint/binding-win32-ia32-msvc@1.78.0': + resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.69.0': - resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==} + '@oxlint/binding-win32-x64-msvc@1.78.0': + resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1445,14 +1112,14 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.1.0': - resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1463,14 +1130,14 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.0': - resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1481,14 +1148,14 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.0': - resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1499,14 +1166,14 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.0': - resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1517,14 +1184,14 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': - resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1536,15 +1203,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.0': - resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1557,15 +1224,15 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.0': - resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1578,15 +1245,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.0': - resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -1599,15 +1266,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.0': - resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -1620,15 +1287,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.0': - resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1641,15 +1308,15 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.0': - resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1661,14 +1328,14 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.0': - resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1678,13 +1345,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-wasm32-wasi@1.1.0': - resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] @@ -1694,14 +1356,14 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.0': - resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1712,14 +1374,14 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.0': - resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1890,14 +1552,14 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.16': - resolution: {integrity: sha512-yNm/fYEcnpRjYduLMaddTK9XKYil6xB88+qFg79ZdZhHu1PadfoQmFW7pVTx7FZqMBNcUuThiAhxhENgtAO2/w==} + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1917,105 +1579,175 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/jsesc@2.5.1': - resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} '@types/wicg-file-system-access@2020.9.8': resolution: {integrity: sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-X6NqNmoTnO1vtcsE4qP571BByPi+i16Ynrp6fffcQ38pbRuy4mwECxA9nKb273gscG0wvcIcGM81B+vy1F4nDw==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-VeguHC/F7EbxNPCrcwCRH2wif6PZKcdUg2DtMyjxohtcVK3vOoVCYqhJBgJVWQ2gbXk+bXcIz8L2/uNYDksN0w==} + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-19TXmRxxPUNNDAP/4xBwDNud1NvuNgQJhRm87t7/CSh4FGhWeeEm7AuyEvrCB+vzeeI/ExT+J58U1HOo+kYKzg==} + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-szcSb7sSn3OQZ5UWtToG2RTQeDlokd05pXM+rGctBIctDj6E4j9bvAfp6xLZCzqyDuZJhi3cXCJBdSlBD0NjHw==} + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-h5Etd2Kc6lvvc+X4MU/EFeYDUZAS4hOqB18piWuy2INHfRvJMOo8jZOwUJRix3NQu75tPbltFCkwFOGqB0fd2Q==} + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-JUUFHNi3asye8iOlVmFEra34LuQ8bw8qvYWp+cW7EXg3mrMxtac/u5chhi8BmICEav2Ff3UFd8ZFL0n+F5EvBA==} + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} engines: {node: '>=16.20.0'} cpu: [arm64] - os: [win32] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-1ZPtOctecgkUHBIEoTefE34t4CewqxwdzIkRx22fDEC9bHhN8xO4JSfQyL+OSWDxiZaJKuqR9BnfWSl2eAFtmg==} + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} engines: {node: '>=16.20.0'} cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260611.2': - resolution: {integrity: sha512-nP/OrQRFTRKHeQiXzMVhQlxSrOPeuDgeGGd9KMlmOFTc/bbQ8Zd6Ep5izsdTkFljd26QUfpm98crp5E5bYAvJg==} + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} - hasBin: true + cpu: [x64] + os: [win32] '@uwx/libav.js-fat@6.0.0-nightly.29.f420ff.ffmpeg.6.1.1': resolution: {integrity: sha512-XMUWb4CEKAKZ8ONnBT/U6jwJlH+cvbTPj9ufdQ4H+nIaRDen8tcS2PyPAh2Smrxf6MmkvUqj4kH4/groYlNjOQ==} - '@vitest/coverage-v8@4.0.18': - resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} peerDependencies: - '@vitest/browser': 4.0.18 - vitest: 4.0.18 + '@vitest/browser': 4.1.10 + vitest: 4.1.10 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@wasm-audio-decoders/common@9.0.7': resolution: {integrity: sha512-WRaUuWSKV7pkttBygml/a6dIEpatq2nnZGFIoPTc5yPLkxL6Wk4YaslPM98OPQvWacvNZ+Py9xROGDtrFBDzag==} @@ -2032,6 +1764,142 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version + + '@yuku-codegen/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} @@ -2068,12 +1936,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-kit@3.0.0-beta.1: - resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} - engines: {node: '>=20.19.0'} - - ast-v8-to-istanbul@0.3.12: - resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} @@ -2094,9 +1958,6 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - birpc@4.0.0: - resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -2196,6 +2057,9 @@ packages: constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -2313,25 +2177,15 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -2643,11 +2497,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2779,8 +2628,8 @@ packages: resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} hasBin: true - miniflare@4.20260611.0: - resolution: {integrity: sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg==} + miniflare@4.20260701.0: + resolution: {integrity: sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==} engines: {node: '>=22.0.0'} hasBin: true @@ -2812,8 +2661,8 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2830,8 +2679,8 @@ packages: resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==} engines: {node: '>=10'} - node-web-audio-api@2.0.0: - resolution: {integrity: sha512-I8r10N26N4ssu7KbL70ywWZIVu5F6eSas/DO1wHWwLjiBSo7TKSOXgCVd0mfEvrewDY5vkmqGCUL3ix/iMr4yA==} + node-web-audio-api@2.2.0: + resolution: {integrity: sha512-qM3dW3Z5HIz+O+++Uiia8x0OSNHcfJGUlexpCTdAkw8JlM17Ru1nV9a94wlxNRXqxT0Lr/ewwEnBXUwGMt24AA==} engines: {node: '>= 22'} object-inspect@1.13.4: @@ -2850,8 +2699,13 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} ogg-opus-decoder@1.7.3: resolution: {integrity: sha512-w47tiZpkLgdkpa+34VzYD8mHUj8I9kfWVZa82mBbNwDvB1byfLXSSzW/HxA4fI3e9kVlICSpXGFwMLV1LPdjwg==} @@ -2869,8 +2723,8 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - oxfmt@0.54.0: - resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} + oxfmt@0.57.0: + resolution: {integrity: sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2882,12 +2736,12 @@ packages: vite-plus: optional: true - oxlint@1.69.0: - resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==} + oxlint@1.78.0: + resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.22.1' + oxlint-tsgolint: '>=7.0.2001' vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: @@ -2978,14 +2832,14 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -3011,8 +2865,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} prettier@2.8.8: @@ -3106,20 +2960,20 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} - rolldown-plugin-dts@0.25.2: - resolution: {integrity: sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==} - engines: {node: ^22.18.0 || >=24.0.0} + rolldown-plugin-dts@0.27.14: + resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} + engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: - '@ts-macro/tsc': ^0.3.6 - '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 rolldown: ^1.0.0 - typescript: ^5.0.0 || ^6.0.0 - vue-tsc: ~3.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 peerDependenciesMeta: - '@ts-macro/tsc': - optional: true '@typescript/native-preview': optional: true + '@volar/typescript': + optional: true typescript: optional: true vue-tsc: @@ -3130,13 +2984,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.1.0: - resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3165,18 +3019,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} - engines: {node: '>=10'} - hasBin: true - - semver@7.8.4: - resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -3257,8 +3101,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -3311,22 +3155,14 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinybench@6.0.2: - resolution: {integrity: sha512-FlHoQpcFvCzeXK5kVPvV7IVgW/hs/B36QWTz876iSdeJguBDfdTSRQmYmaHX+fQNt4hp+gEFB2XXw+8hT4/y8A==} + tinybench@6.1.3: + resolution: {integrity: sha512-8k25iNSZHnzkjp3nrpEmx6YkrTNs41PzOHYc28DR5Z2l/CId/Nzw+Ume/41jCeDDmCUMPuK5GkQ/VxJaJ2P6Qg==} engines: {node: '>=20.0.0'} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -3335,8 +3171,8 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tmp@0.2.5: @@ -3359,18 +3195,18 @@ packages: resolution: {integrity: sha512-Br6DA/YbdpsiSQjc0KaF3ASQtkk3MCiA4q5kIA7ptv6adZa/MdYa2TXAXF2bAzRZIMWfyFEC9Gicr3nb51MgDA==} hasBin: true - tsdown@0.22.2: - resolution: {integrity: sha512-VX9gsyKXsTnBZjnIM4jsHl9aRv+GfgkE/k1hQslilaBfZMlaw3JuGR+6yhiU0QxWBtOCDnTjwOSoXzgB7Rr50g==} - engines: {node: ^22.18.0 || >=24.0.0} + tsdown@0.22.14: + resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==} + engines: {node: ^22.18.0 || >=24.11.0} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.22.2 - '@tsdown/exe': 0.22.2 + '@tsdown/css': 0.22.14 + '@tsdown/exe': 0.22.14 '@vitejs/devtools': '*' publint: ^0.3.8 tsx: '*' - typescript: ^5.0.0 || ^6.0.0 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 unplugin-unused: ^0.5.0 unrun: '*' peerDependenciesMeta: @@ -3396,8 +3232,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.22.4: - resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} engines: {node: '>=18.0.0'} hasBin: true @@ -3408,14 +3244,19 @@ packages: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@7.24.8: - resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -3445,58 +3286,22 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + vite-plugin-node-polyfills@0.28.0: resolution: {integrity: sha512-NXct/ci2ef4fRyCfTb8fk2HmR80Rv7icLd+cRH41TnUugDzdKMFKqFPpZYCFUInZMMem9bkLv5pkq02+7Xu7+w==} peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite@7.3.5: - resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -3533,20 +3338,23 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3560,6 +3368,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -3592,23 +3404,23 @@ packages: engines: {node: '>=8'} hasBin: true - workerd@1.20260611.1: - resolution: {integrity: sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w==} + workerd@1.20260701.1: + resolution: {integrity: sha512-uF813NG09JwNRRUfJ0zBomyTslSPM810dMj9LVvkQ7RAkLrQLzAlPU8Xh/3dIqZDo2bfd7tChbf2PtqLRARRJQ==} engines: {node: '>=16'} hasBin: true - wrangler@4.100.0: - resolution: {integrity: sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg==} + wrangler@4.107.0: + resolution: {integrity: sha512-fw69ThymNitZ0oIEBU2yNeq3kK59UKz/jyA3udwRrQIAIsxX57q5qLOpPTN7qc5t8n9pnUeofe0uxtMuhQZW8w==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260611.1 + '@cloudflare/workers-types': ^4.20260701.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3633,33 +3445,25 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} -snapshots: + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} - '@babel/generator@8.0.0-rc.6': - dependencies: - '@babel/parser': 8.0.0-rc.6 - '@babel/types': 8.0.0-rc.6 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 - jsesc: 3.1.0 + yuku-codegen@0.8.7: + resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==} - '@babel/helper-string-parser@7.27.1': {} + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + +snapshots: - '@babel/helper-string-parser@8.0.0-rc.6': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.0-rc.6': {} - '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 - '@babel/parser@8.0.0-rc.6': - dependencies: - '@babel/types': 8.0.0-rc.6 - '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': @@ -3667,11 +3471,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@8.0.0-rc.6': - dependencies: - '@babel/helper-string-parser': 8.0.0-rc.6 - '@babel/helper-validator-identifier': 8.0.0-rc.6 - '@bcoe/v8-coverage@1.0.2': {} '@changesets/apply-release-plan@7.1.1': @@ -3688,7 +3487,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.5 '@changesets/assemble-release-plan@6.0.10': dependencies: @@ -3697,13 +3496,13 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.7.4 + semver: 7.8.5 '@changesets/changelog-git@0.2.1': dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@25.9.1)': + '@changesets/cli@2.31.1(@types/node@26.2.0)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -3719,7 +3518,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.9.1) + '@inquirer/external-editor': 1.0.3(@types/node@26.2.0) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -3728,7 +3527,7 @@ snapshots: package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.5 spawndamnit: 3.0.1 term-size: 2.2.1 transitivePeerDependencies: @@ -3754,7 +3553,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.4 + semver: 7.8.5 '@changesets/get-release-plan@4.0.16': dependencies: @@ -3819,28 +3618,28 @@ snapshots: '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260701.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260611.1 + workerd: 1.20260701.1 - '@cloudflare/workerd-darwin-64@1.20260611.1': + '@cloudflare/workerd-darwin-64@1.20260701.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260611.1': + '@cloudflare/workerd-darwin-arm64@1.20260701.1': optional: true - '@cloudflare/workerd-linux-64@1.20260611.1': + '@cloudflare/workerd-linux-64@1.20260701.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260611.1': + '@cloudflare/workerd-linux-arm64@1.20260701.1': optional: true - '@cloudflare/workerd-windows-64@1.20260611.1': + '@cloudflare/workerd-windows-64@1.20260701.1': optional: true - '@cloudflare/workers-types@4.20260611.1': {} + '@cloudflare/workers-types@5.20260817.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -3852,253 +3651,113 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/core@1.11.1': dependencies: + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.0': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/aix-ppc64@0.28.0': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.28.0': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-arm@0.28.0': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/android-x64@0.28.0': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.28.0': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.28.0': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.28.0': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.28.0': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.28.0': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-arm@0.28.0': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.28.0': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.28.0': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.28.0': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.28.0': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.28.0': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.28.0': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.7': + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/linux-x64@0.28.0': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eshaz/web-worker@1.2.2': {} @@ -4199,7 +3858,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.11.0 + '@emnapi/runtime': 1.11.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -4211,17 +3870,12 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.9.1)': + '@inquirer/external-editor@1.0.3(@types/node@26.2.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.9.1 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 26.2.0 '@jridgewell/resolve-uri@3.1.2': {} @@ -4253,18 +3907,18 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true '@nodelib/fs.scandir@2.1.5': @@ -4282,122 +3936,122 @@ snapshots: '@oxc-project/types@0.127.0': optional: true - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.138.0': {} - '@oxc-project/types@0.134.0': {} + '@oxc-project/types@0.144.0': {} - '@oxfmt/binding-android-arm-eabi@0.54.0': + '@oxfmt/binding-android-arm-eabi@0.57.0': optional: true - '@oxfmt/binding-android-arm64@0.54.0': + '@oxfmt/binding-android-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-arm64@0.54.0': + '@oxfmt/binding-darwin-arm64@0.57.0': optional: true - '@oxfmt/binding-darwin-x64@0.54.0': + '@oxfmt/binding-darwin-x64@0.57.0': optional: true - '@oxfmt/binding-freebsd-x64@0.54.0': + '@oxfmt/binding-freebsd-x64@0.57.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + '@oxfmt/binding-linux-arm-musleabihf@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.54.0': + '@oxfmt/binding-linux-arm64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.54.0': + '@oxfmt/binding-linux-arm64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + '@oxfmt/binding-linux-ppc64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + '@oxfmt/binding-linux-riscv64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.54.0': + '@oxfmt/binding-linux-riscv64-musl@0.57.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.54.0': + '@oxfmt/binding-linux-s390x-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.54.0': + '@oxfmt/binding-linux-x64-gnu@0.57.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.54.0': + '@oxfmt/binding-linux-x64-musl@0.57.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.54.0': + '@oxfmt/binding-openharmony-arm64@0.57.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.54.0': + '@oxfmt/binding-win32-arm64-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.54.0': + '@oxfmt/binding-win32-ia32-msvc@0.57.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.54.0': + '@oxfmt/binding-win32-x64-msvc@0.57.0': optional: true - '@oxlint/binding-android-arm-eabi@1.69.0': + '@oxlint/binding-android-arm-eabi@1.78.0': optional: true - '@oxlint/binding-android-arm64@1.69.0': + '@oxlint/binding-android-arm64@1.78.0': optional: true - '@oxlint/binding-darwin-arm64@1.69.0': + '@oxlint/binding-darwin-arm64@1.78.0': optional: true - '@oxlint/binding-darwin-x64@1.69.0': + '@oxlint/binding-darwin-x64@1.78.0': optional: true - '@oxlint/binding-freebsd-x64@1.69.0': + '@oxlint/binding-freebsd-x64@1.78.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.69.0': + '@oxlint/binding-linux-arm-musleabihf@1.78.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.69.0': + '@oxlint/binding-linux-arm64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.69.0': + '@oxlint/binding-linux-arm64-musl@1.78.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.69.0': + '@oxlint/binding-linux-ppc64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.69.0': + '@oxlint/binding-linux-riscv64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.69.0': + '@oxlint/binding-linux-riscv64-musl@1.78.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.69.0': + '@oxlint/binding-linux-s390x-gnu@1.78.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.69.0': + '@oxlint/binding-linux-x64-gnu@1.78.0': optional: true - '@oxlint/binding-linux-x64-musl@1.69.0': + '@oxlint/binding-linux-x64-musl@1.78.0': optional: true - '@oxlint/binding-openharmony-arm64@1.69.0': + '@oxlint/binding-openharmony-arm64@1.78.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.69.0': + '@oxlint/binding-win32-arm64-msvc@1.78.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.69.0': + '@oxlint/binding-win32-ia32-msvc@1.78.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.69.0': + '@oxlint/binding-win32-x64-msvc@1.78.0': optional: true '@pinojs/redact@0.4.0': {} @@ -4423,148 +4077,141 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.4': optional: true - '@rolldown/binding-android-arm64@1.1.0': + '@rolldown/binding-android-arm64@1.2.4': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.4': optional: true - '@rolldown/binding-darwin-arm64@1.1.0': + '@rolldown/binding-darwin-arm64@1.2.4': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.4': optional: true - '@rolldown/binding-darwin-x64@1.1.0': + '@rolldown/binding-darwin-x64@1.2.4': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.4': optional: true - '@rolldown/binding-freebsd-x64@1.1.0': + '@rolldown/binding-freebsd-x64@1.2.4': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.0': + '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.0': + '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.0': + '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.0': + '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.0': + '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.4': optional: true - '@rolldown/binding-linux-x64-musl@1.1.0': + '@rolldown/binding-linux-x64-musl@1.2.4': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.4': optional: true - '@rolldown/binding-openharmony-arm64@1.1.0': + '@rolldown/binding-openharmony-arm64@1.2.4': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.3': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.1.0': + '@rolldown/binding-wasm32-wasi@1.1.4': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.0': + '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.0': + '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true '@rolldown/pluginutils@1.0.0-rc.17': @@ -4584,7 +4231,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.4 + picomatch: 4.0.5 optionalDependencies: rollup: 4.59.0 @@ -4665,11 +4312,11 @@ snapshots: '@sindresorhus/is@7.2.0': {} - '@speed-highlight/core@1.2.16': {} + '@speed-highlight/core@1.2.17': {} '@standard-schema/spec@1.1.0': {} - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -4689,101 +4336,130 @@ snapshots: '@types/estree@1.0.9': {} - '@types/jsesc@2.5.1': {} - '@types/node@12.20.55': {} - '@types/node@25.9.1': + '@types/node@26.2.0': dependencies: - undici-types: 7.24.6 + undici-types: 8.3.0 '@types/wicg-file-system-access@2020.9.8': {} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260611.2': + '@typescript/typescript-aix-ppc64@7.0.2': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260611.2': + '@typescript/typescript-darwin-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260611.2': + '@typescript/typescript-darwin-x64@7.0.2': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260611.2': + '@typescript/typescript-freebsd-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260611.2': + '@typescript/typescript-freebsd-x64@7.0.2': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260611.2': + '@typescript/typescript-linux-arm64@7.0.2': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260611.2': + '@typescript/typescript-linux-arm@7.0.2': optional: true - '@typescript/native-preview@7.0.0-dev.20260611.2': - optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260611.2 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260611.2 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260611.2 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260611.2 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260611.2 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260611.2 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260611.2 + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true '@uwx/libav.js-fat@6.0.0-nightly.29.f420ff.ffmpeg.6.1.1': {} - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4))': + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.18 - ast-v8-to-istanbul: 0.3.12 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.2 - obug: 2.1.1 - std-env: 3.10.0 - tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4) + obug: 2.1.3 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)) - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.5(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4))': + '@vitest/mocker@4.1.10(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.5(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4) + vite: 8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.10': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@wasm-audio-decoders/common@9.0.7': dependencies: @@ -4803,6 +4479,80 @@ snapshots: '@xmldom/xmldom@0.8.13': {} + '@yuku-codegen/binding-android-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-x64@0.8.7': + optional: true + + '@yuku-parser/binding-android-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-win32-x64@0.8.7': + optional: true + + '@yuku-toolchain/types@0.8.7': {} + alien-signals@3.2.1: {} ansi-colors@4.1.3: {} @@ -4835,13 +4585,7 @@ snapshots: assertion-error@2.0.1: {} - ast-kit@3.0.0-beta.1: - dependencies: - '@babel/parser': 8.0.0-rc.6 - estree-walker: 3.0.3 - pathe: 2.0.3 - - ast-v8-to-istanbul@0.3.12: + ast-v8-to-istanbul@1.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -4861,8 +4605,6 @@ snapshots: dependencies: is-windows: 1.0.2 - birpc@4.0.0: {} - blake3-wasm@2.1.5: {} bn.js@4.12.3: {} @@ -4977,6 +4719,8 @@ snapshots: constants-browserify@1.0.0: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} @@ -5109,98 +4853,40 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.3.0: {} es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - esbuild@0.27.7: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - esbuild@0.28.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 esprima@4.0.1: {} @@ -5244,9 +4930,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 fengari-interop@0.1.4(fengari@0.1.5): dependencies: @@ -5512,8 +5198,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.1.0: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -5595,7 +5279,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.5 math-intrinsics@1.1.0: {} @@ -5619,13 +5303,13 @@ snapshots: bn.js: 4.12.3 brorand: 1.1.0 - miniflare@4.20260611.0: + miniflare@4.20260701.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.24.8 - workerd: 1.20260611.1 - ws: 8.20.1 + undici: 7.28.0 + workerd: 1.20260701.1 + ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: - bufferutil @@ -5654,7 +5338,7 @@ snapshots: ms@2.0.0: {} - nanoid@3.3.12: {} + nanoid@3.3.15: {} node-domexception@1.0.0: {} @@ -5694,7 +5378,7 @@ snapshots: util: 0.12.5 vm-browserify: 1.1.2 - node-web-audio-api@2.0.0: + node-web-audio-api@2.2.0: dependencies: caller: 1.1.0 node-fetch: 3.3.2 @@ -5718,7 +5402,9 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - obug@2.1.1: {} + obug@2.1.3: {} + + obug@2.1.4: {} ogg-opus-decoder@1.7.3: dependencies: @@ -5737,51 +5423,51 @@ snapshots: outdent@0.5.0: {} - oxfmt@0.54.0: + oxfmt@0.57.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.54.0 - '@oxfmt/binding-android-arm64': 0.54.0 - '@oxfmt/binding-darwin-arm64': 0.54.0 - '@oxfmt/binding-darwin-x64': 0.54.0 - '@oxfmt/binding-freebsd-x64': 0.54.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 - '@oxfmt/binding-linux-arm64-gnu': 0.54.0 - '@oxfmt/binding-linux-arm64-musl': 0.54.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 - '@oxfmt/binding-linux-riscv64-musl': 0.54.0 - '@oxfmt/binding-linux-s390x-gnu': 0.54.0 - '@oxfmt/binding-linux-x64-gnu': 0.54.0 - '@oxfmt/binding-linux-x64-musl': 0.54.0 - '@oxfmt/binding-openharmony-arm64': 0.54.0 - '@oxfmt/binding-win32-arm64-msvc': 0.54.0 - '@oxfmt/binding-win32-ia32-msvc': 0.54.0 - '@oxfmt/binding-win32-x64-msvc': 0.54.0 - - oxlint@1.69.0: + '@oxfmt/binding-android-arm-eabi': 0.57.0 + '@oxfmt/binding-android-arm64': 0.57.0 + '@oxfmt/binding-darwin-arm64': 0.57.0 + '@oxfmt/binding-darwin-x64': 0.57.0 + '@oxfmt/binding-freebsd-x64': 0.57.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.57.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.57.0 + '@oxfmt/binding-linux-arm64-gnu': 0.57.0 + '@oxfmt/binding-linux-arm64-musl': 0.57.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-musl': 0.57.0 + '@oxfmt/binding-linux-s390x-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-musl': 0.57.0 + '@oxfmt/binding-openharmony-arm64': 0.57.0 + '@oxfmt/binding-win32-arm64-msvc': 0.57.0 + '@oxfmt/binding-win32-ia32-msvc': 0.57.0 + '@oxfmt/binding-win32-x64-msvc': 0.57.0 + + oxlint@1.78.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.69.0 - '@oxlint/binding-android-arm64': 1.69.0 - '@oxlint/binding-darwin-arm64': 1.69.0 - '@oxlint/binding-darwin-x64': 1.69.0 - '@oxlint/binding-freebsd-x64': 1.69.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.69.0 - '@oxlint/binding-linux-arm-musleabihf': 1.69.0 - '@oxlint/binding-linux-arm64-gnu': 1.69.0 - '@oxlint/binding-linux-arm64-musl': 1.69.0 - '@oxlint/binding-linux-ppc64-gnu': 1.69.0 - '@oxlint/binding-linux-riscv64-gnu': 1.69.0 - '@oxlint/binding-linux-riscv64-musl': 1.69.0 - '@oxlint/binding-linux-s390x-gnu': 1.69.0 - '@oxlint/binding-linux-x64-gnu': 1.69.0 - '@oxlint/binding-linux-x64-musl': 1.69.0 - '@oxlint/binding-openharmony-arm64': 1.69.0 - '@oxlint/binding-win32-arm64-msvc': 1.69.0 - '@oxlint/binding-win32-ia32-msvc': 1.69.0 - '@oxlint/binding-win32-x64-msvc': 1.69.0 + '@oxlint/binding-android-arm-eabi': 1.78.0 + '@oxlint/binding-android-arm64': 1.78.0 + '@oxlint/binding-darwin-arm64': 1.78.0 + '@oxlint/binding-darwin-x64': 1.78.0 + '@oxlint/binding-freebsd-x64': 1.78.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 + '@oxlint/binding-linux-arm-musleabihf': 1.78.0 + '@oxlint/binding-linux-arm64-gnu': 1.78.0 + '@oxlint/binding-linux-arm64-musl': 1.78.0 + '@oxlint/binding-linux-ppc64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-musl': 1.78.0 + '@oxlint/binding-linux-s390x-gnu': 1.78.0 + '@oxlint/binding-linux-x64-gnu': 1.78.0 + '@oxlint/binding-linux-x64-musl': 1.78.0 + '@oxlint/binding-openharmony-arm64': 1.78.0 + '@oxlint/binding-win32-arm64-msvc': 1.78.0 + '@oxlint/binding-win32-ia32-msvc': 1.78.0 + '@oxlint/binding-win32-x64-msvc': 1.78.0 p-filter@2.1.0: dependencies: @@ -5857,10 +5543,10 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.3: {} - picomatch@4.0.4: {} + picomatch@4.0.5: {} + pify@4.0.1: {} pino-abstract-transport@3.0.0: @@ -5902,9 +5588,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.15: + postcss@8.5.16: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -6000,19 +5686,17 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 - rolldown-plugin-dts@0.25.2(@typescript/native-preview@7.0.0-dev.20260611.2)(rolldown@1.1.0): + rolldown-plugin-dts@0.27.14(rolldown@1.2.4)(typescript@7.0.2): dependencies: - '@babel/generator': 8.0.0-rc.6 - '@babel/helper-validator-identifier': 8.0.0-rc.6 - '@babel/parser': 8.0.0-rc.6 - ast-kit: 3.0.0-beta.1 - birpc: 4.0.0 dts-resolver: 3.0.0 get-tsconfig: 5.0.0-beta.5 - obug: 2.1.1 - rolldown: 1.1.0 + obug: 2.1.4 + rolldown: 1.2.4 + yuku-ast: 0.8.7 + yuku-codegen: 0.8.7 + yuku-parser: 0.8.7 optionalDependencies: - '@typescript/native-preview': 7.0.0-dev.20260611.2 + typescript: 7.0.2 transitivePeerDependencies: - oxc-resolver @@ -6038,47 +5722,46 @@ snapshots: '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 optional: true - rolldown@1.0.3: + rolldown@1.1.4: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.138.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - rolldown@1.1.0: - dependencies: - '@oxc-project/types': 0.134.0 + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 + + rolldown@1.2.4: + dependencies: + '@oxc-project/types': 0.144.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.0 - '@rolldown/binding-darwin-arm64': 1.1.0 - '@rolldown/binding-darwin-x64': 1.1.0 - '@rolldown/binding-freebsd-x64': 1.1.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 - '@rolldown/binding-linux-arm64-gnu': 1.1.0 - '@rolldown/binding-linux-arm64-musl': 1.1.0 - '@rolldown/binding-linux-ppc64-gnu': 1.1.0 - '@rolldown/binding-linux-s390x-gnu': 1.1.0 - '@rolldown/binding-linux-x64-gnu': 1.1.0 - '@rolldown/binding-linux-x64-musl': 1.1.0 - '@rolldown/binding-openharmony-arm64': 1.1.0 - '@rolldown/binding-wasm32-wasi': 1.1.0 - '@rolldown/binding-win32-arm64-msvc': 1.1.0 - '@rolldown/binding-win32-x64-msvc': 1.1.0 + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 rollup@4.59.0: dependencies: @@ -6110,6 +5793,7 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.59.0 '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + optional: true run-parallel@1.2.0: dependencies: @@ -6129,11 +5813,7 @@ snapshots: safer-buffer@2.1.2: {} - semver@7.7.4: {} - - semver@7.8.1: {} - - semver@7.8.4: {} + semver@7.8.5: {} set-function-length@1.2.2: dependencies: @@ -6156,7 +5836,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.4 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -6244,7 +5924,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.1.0: {} stream-browserify@3.0.0: dependencies: @@ -6294,25 +5974,18 @@ snapshots: tinybench@2.9.0: {} - tinybench@6.0.2: {} - - tinyexec@1.0.2: {} + tinybench@6.1.3: {} tinyexec@1.2.4: {} - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinypool@2.1.0: {} - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tmp@0.2.5: {} @@ -6339,7 +6012,7 @@ snapshots: transitivePeerDependencies: - supports-color - tsdown@0.22.2(@typescript/native-preview@7.0.0-dev.20260611.2)(tsx@4.22.4)(unrun@0.2.37): + tsdown@0.22.14(tsx@4.23.12)(typescript@7.0.2)(unrun@0.2.37): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -6347,30 +6020,31 @@ snapshots: empathic: 2.0.1 hookable: 6.1.1 import-without-cache: 0.4.0 - obug: 2.1.1 - picomatch: 4.0.4 - rolldown: 1.1.0 - rolldown-plugin-dts: 0.25.2(@typescript/native-preview@7.0.0-dev.20260611.2)(rolldown@1.1.0) - semver: 7.8.1 + obug: 2.1.4 + picomatch: 4.0.5 + rolldown: 1.2.4 + rolldown-plugin-dts: 0.27.14(rolldown@1.2.4)(typescript@7.0.2) tinyexec: 1.2.4 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 + verkit: 0.3.2 optionalDependencies: - tsx: 4.22.4 + tsx: 4.23.12 + typescript: 7.0.2 unrun: 0.2.37 transitivePeerDependencies: - - '@ts-macro/tsc' - '@typescript/native-preview' + - '@volar/typescript' - oxc-resolver - vue-tsc tslib@2.8.1: optional: true - tsx@4.22.4: + tsx@4.23.12: dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -6382,14 +6056,37 @@ snapshots: es-errors: 1.3.0 is-typed-array: 1.1.15 + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + unconfig-core@7.5.0: dependencies: '@quansync/fs': 1.0.0 quansync: 1.0.0 - undici-types@7.24.6: {} + undici-types@8.3.0: {} - undici@7.24.8: {} + undici@7.28.0: {} unenv@2.0.0-rc.24: dependencies: @@ -6417,77 +6114,56 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.20 - vite-plugin-node-polyfills@0.28.0(rollup@4.59.0)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4)): + verkit@0.3.2: {} + + vite-plugin-node-polyfills@0.28.0(rollup@4.59.0)(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.59.0) node-stdlib-browser: 1.3.1 - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4) + vite: 8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12) transitivePeerDependencies: - rollup - vite@7.3.5(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4): - dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.59.0 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 25.9.1 - fsevents: 2.3.3 - lightningcss: 1.32.0 - tsx: 4.22.4 - - vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(tsx@4.22.4): + vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.4 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.1 - esbuild: 0.28.0 + '@types/node': 26.2.0 + esbuild: 0.28.1 fsevents: 2.3.3 - tsx: 4.22.4 - - vitest@4.0.18(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4): - dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.5(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + tsx: 4.23.12 + + vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.5(@types/node@25.9.1)(lightningcss@1.32.0)(tsx@4.22.4) + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.3(@types/node@26.2.0)(esbuild@0.28.1)(tsx@4.23.12) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 26.2.0 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml vm-browserify@1.1.2: {} @@ -6514,32 +6190,32 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - workerd@1.20260611.1: + workerd@1.20260701.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260611.1 - '@cloudflare/workerd-darwin-arm64': 1.20260611.1 - '@cloudflare/workerd-linux-64': 1.20260611.1 - '@cloudflare/workerd-linux-arm64': 1.20260611.1 - '@cloudflare/workerd-windows-64': 1.20260611.1 + '@cloudflare/workerd-darwin-64': 1.20260701.1 + '@cloudflare/workerd-darwin-arm64': 1.20260701.1 + '@cloudflare/workerd-linux-64': 1.20260701.1 + '@cloudflare/workerd-linux-arm64': 1.20260701.1 + '@cloudflare/workerd-windows-64': 1.20260701.1 - wrangler@4.100.0(@cloudflare/workers-types@4.20260611.1): + wrangler@4.107.0(@cloudflare/workers-types@5.20260817.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1) + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260701.1) blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260611.0 + esbuild: 0.28.1 + miniflare: 4.20260701.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260611.1 + workerd: 1.20260701.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260611.1 + '@cloudflare/workers-types': 5.20260817.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate - ws@8.20.1: {} + ws@8.21.0: {} xtend@4.0.2: {} @@ -6554,6 +6230,45 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.16 + '@speed-highlight/core': 1.2.17 cookie: 1.1.1 youch-core: 0.3.3 + + yuku-ast@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + + yuku-codegen@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-x64': 0.8.7 + '@yuku-codegen/binding-freebsd-x64': 0.8.7 + '@yuku-codegen/binding-linux-arm-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm-musl': 0.8.7 + '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm64-musl': 0.8.7 + '@yuku-codegen/binding-linux-x64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-x64-musl': 0.8.7 + '@yuku-codegen/binding-win32-arm64': 0.8.7 + '@yuku-codegen/binding-win32-x64': 0.8.7 + + yuku-parser@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 diff --git a/scripts/bench/packages/player.ts b/scripts/bench/packages/player.ts index 765589eb..8cfa2898 100644 --- a/scripts/bench/packages/player.ts +++ b/scripts/bench/packages/player.ts @@ -1,5 +1,6 @@ import * as playerApi from '@be-music/player'; import * as judgingApi from '@be-music/player/judging'; +import * as playlogApi from '@be-music/player/playlog'; import { registerPlayerExportsCases } from '../../../packages/player/scripts/exports-cases.ts'; import type { BenchmarkPackageDefinition } from '../exports.types.ts'; @@ -7,6 +8,10 @@ export const playerBenchmarkPackage: BenchmarkPackageDefinition = { module: { ...(playerApi as Record<string, unknown>), 'judging.findClosestCandidateInWindow': judgingApi.findClosestCandidateInWindow, + 'playlog.serializePlaylog': playlogApi.serializePlaylog, + 'playlog.parsePlaylog': playlogApi.parsePlaylog, + 'playlog.simulatePlaylog': playlogApi.simulatePlaylog, + 'playlog.simulatePlaylogRulesets': playlogApi.simulatePlaylogRulesets, }, registerCases: registerPlayerExportsCases, }; diff --git a/scripts/build-sea.ts b/scripts/build-sea.ts index 8d8030c1..29e16a0d 100644 --- a/scripts/build-sea.ts +++ b/scripts/build-sea.ts @@ -146,7 +146,7 @@ function printUsage() { ' -h, --help Show this help', '', 'Requirements:', - ' Node.js 25.5+ with built-in `--build-sea` support', + ' Node.js 26+ with built-in `--build-sea` support', ].join('\n') + '\n', ); } @@ -282,7 +282,7 @@ async function buildSeaBundle(config: SeaTargetConfig, seaDir: string): Promise< mainFields: [...defaultServerMainFields], }, build: { - target: 'node25', + target: 'node26', outDir: seaDir, emptyOutDir: true, codeSplitting: false, @@ -497,7 +497,7 @@ async function runSeaBuild(nodeBinaryPath: string, cwd: string, configFilePath: if (output.includes('--build-sea') && output.toLowerCase().includes('unknown')) { throw new Error( `The selected Node executable does not support --build-sea: ${nodeBinaryPath}. ` + - 'Use Node.js 25.5+ with built-in SEA support.', + 'Use Node.js 26+ with built-in SEA support.', ); } @@ -573,7 +573,7 @@ async function main(): Promise<void> { if (!hasBuildSea) { throw new Error( `The selected Node executable does not support --build-sea: ${nodeBinaryPath}. ` + - 'Use Node.js 25.5+ with built-in SEA support.', + 'Use Node.js 26+ with built-in SEA support.', ); } diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 4fd87fcc..a28e5105 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -12,6 +12,7 @@ "@be-music/player/audio-sink": ["./packages/player/src/audio-sink.ts"], "@be-music/player/image-resize-algorithm": ["./packages/player/src/image-resize-algorithm.ts"], "@be-music/player/judging": ["./packages/player/src/judging.ts"], + "@be-music/player/playlog": ["./packages/player/src/playlog/index.ts"], "@be-music/player/state-signals": ["./packages/player/src/state-signals.ts"], "@be-music/player/utils": ["./packages/player/src/utils.ts"], "@be-music/player/core/*": ["./packages/player/src/core/*.ts"], diff --git a/tsdown.package.config.mts b/tsdown.package.config.mts index 27c94a2e..c67ab433 100644 --- a/tsdown.package.config.mts +++ b/tsdown.package.config.mts @@ -35,7 +35,7 @@ export function createPackageTsdownConfig(options: CreatePackageTsdownConfigOpti outDir: 'dist', platform: 'node', sourcemap: true, - target: 'node25', + target: 'node26', plugins: Object.hasOwn(entry, 'cli') ? [createCliShebangPlugin()] : undefined, }); } diff --git a/vitest.config.ts b/vitest.config.ts index 73879e30..365fef44 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -33,6 +33,7 @@ const workspaceAliases = [ replacement: resolve(rootDir, 'packages/player/src/image-resize-algorithm.ts'), }, { find: '@be-music/player/judging', replacement: resolve(rootDir, 'packages/player/src/judging.ts') }, + { find: '@be-music/player/playlog', replacement: resolve(rootDir, 'packages/player/src/playlog/index.ts') }, { find: '@be-music/player/state-signals', replacement: resolve(rootDir, 'packages/player/src/state-signals.ts') }, { find: '@be-music/player/utils', replacement: resolve(rootDir, 'packages/player/src/utils.ts') }, { find: '@be-music/player/core', replacement: resolve(rootDir, 'packages/player/src/core') },