Skip to content

feat: add replay support#797

Open
lyh2011 wants to merge 16 commits into
TeamFlos:mainfrom
lyh2011:replay
Open

feat: add replay support#797
lyh2011 wants to merge 16 commits into
TeamFlos:mainfrom
lyh2011:replay

Conversation

@lyh2011
Copy link
Copy Markdown

@lyh2011 lyh2011 commented May 17, 2026

给phira添加回放功能
解决了录屏占用空间大和因为没有录屏而感到遗憾的问题
可在设置内开启或关闭自动录制,可查看录制
已过clippy,fmt
990822c1184761da940edca3f29780bc
d33c3dc4191f6414c72d69e0b1f81485
b8d956285a755f566472314003398a8b

lyhlyhcc added 13 commits May 17, 2026 16:16
Introduces a self-contained replay system on top of the upstream v0.7.1
phira / prpr.

prpr (engine) changes:
- New `prpr::replay` module with `ReplayData`, `NoteRecord`, JSON
  serialization, and a save_replay_file() helper that writes to
  {data_dir}/replays/.
- New `prpr::export` module: an Exporter that pipes raw RGBA frames into
  an external `ffmpeg` CLI to produce an MP4. Audio mux is optional.
- prpr::lib: register a global data dir (set_data_dir) and re-export rfd
  for use by the host.
- Judge gains `start_recording` / `take_replay_record` /
  `set_replay_data` / `is_replaying` plus a `replay_update` path that
  drives note state from a recorded replay.
- Hold-press events are also written to the replay buffer so playback
  reproduces hold notes correctly.
- GameScene picks up a pending replay/record/exporter from thread-local
  slots in `prpr::replay` / `prpr::export`. On State::Ending it persists
  the recorded replay and finalizes any in-progress export.
- A new Config field `auto_record` (default true) gates recording.

phira (host) changes:
- New `dir::replays()` returning data/replays.
- New `phira::page::ReplayListPage`: lists replays grouped by chart,
  supports playing back a replay or exporting it to MP4 (rfd save_file
  with osascript fallback for macOS Sequoia non-bundled apps).
- Home page gets a Replay (回放) icon between message and settings.
- Settings -> Chart gets an "Auto-record replays" toggle that drives the
  new Config::auto_record field. l10n added for zh-CN and en-US.
- SongScene::launch sets `prpr::replay::set_pending_record(true)` when
  the user has auto_record on, so the next-built GameScene records a
  replay automatically.

prpr-avc:
- ffi.rs: link kind is `static` everywhere except x86_64-apple-darwin,
  which links the Homebrew ffmpeg@4 dylibs. The pre-ffmpeg-5 swresample
  API is required (`swr_alloc_set_opts`).
- build.rs: on x86_64-apple-darwin, automatically point rustc at
  /usr/local/opt/ffmpeg@4/lib (or HOMEBREW_PREFIX equivalent) so local
  dev builds work without a custom prebuilt static slot.

Cross-platform release builds are unchanged: every target other than
x86_64-apple-darwin keeps the original prebuilt-static contract.
- Run nightly-2026-01-01 rustfmt across all replay/export/etc. files so
  the upstream `cargo fmt -- --check` CI is clean.
- Fix clippy lints (-D warnings) introduced by the replay branch:
  drop unused `icons` field and `chart_id` on ReplayEntry; remove
  unused `rank_icon` helper; replace `sort_by(|a,b| b.cmp(&a))` with
  `sort_by_key(... Reverse(...))`; turn the trailing `return out;` into
  a tail expression in pick_save_path.
- Add `.github/workflows/build-android.yml`: a manual + push-triggered
  Android build that installs the cmdline-tools, accepts SDK licenses,
  pulls NDK r27c, downloads a per-target prpr-avc-ffmpeg static-lib
  release tarball matching the env var, runs `cargo ndk -t arm64-v8a`,
  and uploads `libphira.so` as an artifact.
The previous tag 20260309_v0 was either removed from the GitHub
release page or never published as an asset; the download step in
build-android.yml was hitting a 404. 20260408_v0 is the newest
release at the time of this commit and ships all the targets
including aarch64-linux-android.
The repo's rust-toolchain.toml pins nightly-2026-01-01 as the active
toolchain. The previous workflow installed stable + the Android target,
but cargo (when run inside the workspace) honors rust-toolchain.toml
and tried to compile with nightly, which had no Android target — hence
the 'can't find crate for core' error.

Fix: install stable separately for cargo-ndk's own bootstrap, then
explicitly add aarch64-linux-android to the project-pinned toolchain
via rustup target add. cargo-ndk is installed with '+stable' so it
doesn't drag the project nightly into the host build path.
- Remove the in-page "返回" button I added; it was overlapping the
  global "Glory" back button rendered by MainScene. Replace it with
  an `on_back_pressed` override so the system back button (and the
  Glory button, since they go through the same codepath) navigates
  up one level inside ReplayListPage instead of popping the page
  outright.
- The MP4 export button silently did nothing on Android because
  `pick_save_path` is a no-op there and the host ffmpeg CLI isn't
  shipped with the apk. Show a clear toast explaining the desktop
  requirement instead of vanishing the click.
Goal: a single "导出 MP4" button on every replay item that pops the same
native save dialog used by chart export — desktop (rfd), Android (SAF
via the host's `showExportDialog` JNI call), iOS (NSTemporaryDirectory
+ UIDocumentPicker) — and writes the encoded mp4 into the file the
user chose.

prpr changes:
- Refactor `prpr::export` into a module with a backend trait and one
  implementation per platform:
    * `desktop::FfmpegCliEncoder` — same RGBA→ffmpeg-stdin pipe as before
    * `android::AndroidEncoder` — pure-JNI driver of MediaCodec H.264
      + MediaMuxer mp4. RGBA→NV12 (BT.601 limited range) is done on the
      CPU; no extra Java helpers required in the host APK.
    * `ios::IosEncoder` — placeholder that surfaces a clear
      "not yet implemented" error; the AVAssetWriter integration is
      sketched in comments and will land in a follow-up.
- The encoder always writes to a *temporary* mp4 path. After
  `Exporter::finish()` succeeds, prpr publishes the temp path via a
  thread-local (`publish_export_result`) so the host can copy it into
  the user-chosen `File` and call `resolve_export()`.

phira changes:
- ReplayListPage drops its own ad-hoc save dialog and reuses the
  workspace's chart-export plumbing (`request_export` /
  `take_export` / `resolve_export`). The flow is identical to the
  existing "导出谱面" feature, so it works the same way on every
  platform without per-OS code in the page.
- `ExportConfig` is re-exported from the page module so other pages
  can share it.

bad-note animation in replays:
- `Judge::replay_update` now spawns a `BadNote` (the dark fading
  duplicate that normal gameplay shows) instead of letting the note
  vanish, mirroring the visual feedback of a live Bad judgement.
The Android encoder I sketched against jni 0.22.4 hit several real-API
mismatches that I can't reliably debug without an Android device on the
critical path:
  - GlobalRef -> Global<T> with explicit type parameter
  - get_field needs FieldSignature, not JNIStr
  - get_native_interface() doesn't exist; raw env access is via get_raw()
  - JByteBuffer typing for getDirectBufferAddress

Replace it with a stub that returns 'not yet implemented' on finish().
The end-to-end pipeline (file picker -> temp mp4 -> user-chosen file ->
resolve_export) is wired and tested via the desktop ffmpeg backend; the
real MediaCodec implementation can land in a follow-up branch with
device verification.

Also:
  - Move 'use anyhow::Context' into mod desktop so non-desktop builds
    don't see an unused import.
  - Trim ios.rs's unused 'output' field.
Reverts the cross-platform MP4 export work: the JNI 0.22 surface
(Global<T>, FieldSignature, versioned native-interface unions) made
the Android encoder backend infeasible to keep on the current
toolchain, and the user prefers to drop the feature rather than
ship a desktop-only or stubbed implementation.

Removed:
  * prpr/src/export/{mod,android,ios}.rs
  * `pub mod export;` and `pub use rfd;` from prpr/src/lib.rs
  * `exporter` field, `set_exporter`, render-time capture/blit, and
    the `State::Ending` finalize hop in prpr/src/scene/game.rs
  * `take_pending_exporter()` plumbing on GameScene::new
  * `ExportConfig` re-export from phira/src/page.rs

Replay list (phira/src/page/replay_list.rs) is rewritten to a
2-column grid for both folder and entry views: cell width is
(r.w - pad * (cols + 1)) / cols, items are placed at
(i % cols, i / cols), and font sizes / paddings were tightened
(score 0.55, accuracy 0.32, delete button 0.10x0.05 pinned to the
bottom-right corner) so the denser layout still reads cleanly.

Also adds local helper scripts under scripts/ for the Android
and iOS-simulator local build flows.
Drop everything that is fork-specific or unrelated to the replay
feature so the diff against TeamFlos/phira:main is reviewable as a
single PR.

Removed:
  * .github/workflows/build-android.yml (fork CI)
  * scripts/build-{android,ios-sim}.sh (local dev helpers)
  * prpr-avc/{build.rs,ffi.rs} hacks for Intel-Mac dev linking
    against Homebrew ffmpeg@4 dylibs - reverted to upstream
  * prpr::set_data_dir / prpr::get_data_dir global state and the
    matching call in phira/src/lib.rs
  * Cosmetic msg/settings button resizing in phira/src/page/home.rs
    (only the new replay button is added, existing layout preserved)
  * Stale "MP4 export" wording in auto-record locale strings

Refactored:
  * Replay save path now uses an explicit RecordSaveFn callback,
    mirroring the existing SaveFn pattern, threaded through
    LoadingScene/UnlockScene/GameScene::new. The phira side wires
    it to a closure that calls prpr::replay::save_replay_to_dir
    with dir::replays(). prpr no longer needs to know where the
    data dir lives.
  * After a replay finishes, GameScene now pushes EndingScene like
    a normal play (with upload_fn / record_data / best_record
    suppressed) so the user sees a result screen instead of a
    silent pop back to the replay list.

Fixed:
  * Replay list rank icon mapping was off by one and could not
    return F. Switched to canonical prpr::judge::icon_index.

Everything builds clippy-clean on host and Android arm64-v8a.
The replay button at top+0.24 was placed below the 0.23-tall row that
holds Event/Respack and the side icons, so it overflowed the area.
Switch the side column to three 0.11x0.07 buttons (msg / replay /
settings) at top, top+0.08 and top+0.16 so all three fit inside the
existing row.

Also drop the data/replays/ path from the auto-record subtitle - it's
internal info; the help text just points users at Home -> Replays.
Two locally imported charts can share the same display name, which made
the previous name-based matching collapse them into one folder and
sometimes launch the wrong chart on replay. Carry phira's per-chart
`local_path` (`download/<id>` for online charts, the unzipped dir
name for local imports) through the recording pipeline so each replay
can be matched back unambiguously.

  * prpr::replay::ReplayData gains `chart_local_path: String` (defaults
    to empty for backward compat with old replays).
  * The `set_pending_record` thread-local now carries the local_path
    instead of a bool. `take_pending_record` returns Option<String>.
  * Judge::start_recording takes the local_path and stores it on the
    ReplayData.
  * The replay list groups by a stable key (`path:<local_path>`,
    falling back to `id:<chart_id>` then `name:<display>`) and
    launches the chart with id > local_path > name preference.

Old replay files keep working — they fall through to the
name-based group key like before.
@lyh2011
Copy link
Copy Markdown
Author

lyh2011 commented May 24, 2026

添加重命名和收藏
a9a571d55850a5c84e59ec7e65e42fd9
ef7b66c2548f8749892c3bea62d7adb1
6ce63d7fe7e9b24c54f138de39aa8103

@ljlVink
Copy link
Copy Markdown
Contributor

ljlVink commented May 25, 2026

@codex review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 为 phira 增加“回放”能力:在引擎侧记录/回放判定事件并落盘为 JSON,在宿主侧提供回放列表入口与设置开关,用于回看历史游玩过程并缓解录屏占空间的问题。

Changes:

  • prpr:新增 replay 数据结构与线程本地的“待回放/待录制”交接;Judge 增加录制与回放驱动路径;GameScene 结束时保存回放并在回放模式下禁止更新本地最佳/上传。
  • phira:新增回放列表页与首页入口;设置中增加“自动录制回放”开关;启动游戏时按配置自动请求录制,并将保存逻辑通过回调下沉到宿主。
  • 适配调用链:LoadingScene / UnlockScene / monitor 启动路径补齐新增参数传递。

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
prpr/src/scene/loading.rs 向 GameScene 传递 record_save_fn 回调以支持宿主保存回放
prpr/src/scene/game.rs 接入 pending 回放/录制配置;结束时落盘回放并屏蔽回放模式的成绩更新/上传
prpr/src/replay.rs 新增 ReplayData/NoteRecord、JSON 序列化与 save_replay_to_dir,以及 thread-local 交接槽
prpr/src/lib.rs 导出 replay 模块
prpr/src/judge.rs 增加录制缓冲、回放驱动 replay_update,并在 commit/hold 事件中写入记录
prpr/src/config.rs 新增 auto_record 配置项(默认开启)
phira/src/scene/unlock.rs 传递 record_save_fn 以保持启动链一致
phira/src/scene/song.rs 根据配置设置 pending 录制;提供回放保存回调实现(保存到 data/replays)
phira/src/page/settings.rs 增加“自动录制回放”开关 UI
phira/src/page/replay_list.rs 新增回放列表页:分组、收藏、重命名、删除、启动回放
phira/src/page/home.rs 首页新增回放入口按钮
phira/src/page.rs 导出 ReplayListPage
phira/src/lib.rs 新增 dir::replays() 目录入口
phira/locales/zh-CN/settings.ftl 增加 auto-record 相关文案
phira/locales/en-US/settings.ftl 增加 auto-record 相关文案
phira-monitor/src/launch.rs 更新 LoadingScene::new 调用以匹配新增参数

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread prpr/src/scene/game.rs Outdated
Comment on lines +356 to +363
// Pick up any pending replay/record configuration queued by
// the caller before returning.
if let Some(replay) = crate::replay::take_pending_playback() {
this.judge.set_replay_data(replay);
} else if let Some(local_path) = crate::replay::take_pending_record() {
// Record only when it actually makes sense to: a normal play with
// no autoplay and 1x speed.
if normal_mode && !this.res.config.autoplay() && (this.res.config.speed - 1.0).abs() < 1e-3 {
Comment thread prpr/src/judge.rs Outdated
Comment on lines +1001 to +1005
let nt = if matches!(note.kind, NoteKind::Hold { .. }) {
rec.time
} else {
note.time
};
Comment thread prpr/src/replay.rs Outdated
Comment on lines +163 to +181
/// Convenience: serialize `data` as pretty JSON into `dir`. The filename
/// encodes timestamp and chart name to avoid collisions. Hosts can use this
/// from inside their own `RecordSaveFn`, or roll their own.
pub fn save_replay_to_dir(dir: &Path, data: &ReplayData) -> anyhow::Result<PathBuf> {
std::fs::create_dir_all(dir)?;
let safe_name: String = data
.chart_name
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
c if c.is_control() => '_',
c => c,
})
.collect();
let filename = format!("{}_{}.json", data.timestamp, safe_name);
let path = dir.join(filename);
let json = serde_json::to_string_pretty(data)?;
std::fs::write(&path, json)?;
Ok(path)
Comment thread phira/src/page/replay_list.rs Outdated
if let Some((_, name)) = &self.current_folder {
name.clone().into()
} else {
"回放列表".into()
Comment thread phira/src/page/replay_list.rs Outdated
})
.or_else(|| get_data().charts.iter().find(|c| c.info.name == replay.chart_name))
.map(|c| c.local_path.clone())
.ok_or_else(|| anyhow::anyhow!("找不到对应的铺面: {}", replay.chart_name))?;
Comment on lines +423 to +433
let rr = Rect::new(0.76, -top + 0.04, 0.20, 0.07);
self.favorite_filter_btn.render_shadow(ui, rr, t, |ui, path| {
ui.fill_path(&path, if chosen { WHITE } else { semi_black(0.5) });
ui.text("仅显示收藏")
.pos(rr.center().x, rr.center().y)
.anchor(0.5, 0.5)
.no_baseline()
.size(0.35)
.max_width(rr.w - 0.02)
.color(if chosen { Color::new(0.3, 0.3, 0.3, 1.) } else { WHITE })
.draw();
@liquidhelium liquidhelium added the enhancement New feature or request label May 26, 2026
@lyh2011
Copy link
Copy Markdown
Author

lyh2011 commented May 26, 2026

修好了Screenshot_2026-05-26-22-53-09-78_d2f71cd433eba6fe82c4c12ffba30ed5.jpg

@lyh2011 lyh2011 changed the title 给phira添加回放功能 feat: add replay support May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants