The movie cutscene subtitle + voice pipeline, driven by the ADVERTISE_MOVIE manifest (the authoritative movie -> subtitle -> voice index). Also flushes several sessions of local WIP (async viewer loading, grouped-pool XBG7/hero-ship decode, drawlog tooling). See docs/HANDOFF-movie-voice-subtitles-2026-07-19.md. Subtitles (movie_subtitle.rs): - Full movie->track->text chain; join multi-line captions sharing one timing (fixes S13A dropped "Look at it father" line); overlap-safe active_cues(); Latin-1 accents preserved. Voice (slb.rs): XACT .slb -> XMA1 RIFF; take the FIRST sub-wave bounded by its declared data size (fixes S10-S16 alternate-take garble); list_voice_clips. Manifest (movie_manifest.rs): parse ADVERTISE_MOVIE (0x5B983A08) for the real movie->voice binding (not always VOICE_<movie>; e.g. hokyu -> VOICE_D_* in etc\). Resolve the token's sound.pak path via sounds.tbl. DIRECT bindings only — the demo-id shared-clip fallback for unbound hokyu movies was verified WRONG in-game and reverted (unbound hokyu stay unvoiced; correct join key is an OPEN problem). Viewer: manifest-driven voice (movie player toggle + solo button), standalone "Voice Lines" browser, stacked caption overlay. Tests: 46 formats-lib + 11 viewer-lib + movie_manifest/movie_subtitle/slb disc tests; full workspace green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.4 KiB
Rust
100 lines
3.4 KiB
Rust
//! # sylpheed-viewer
|
|
//!
|
|
//! Bevy-based asset viewer for Project Sylpheed: Arc of Deception.
|
|
//!
|
|
//! This crate serves as both:
|
|
//! - A **native binary** (via `main.rs`) — full desktop viewer
|
|
//! - A **WASM library** (this file) — browser viewer at `index.html`
|
|
//!
|
|
//! ## Architecture
|
|
//! - `sylpheed_formats` handles all binary parsing — no Bevy dependency
|
|
//! - This crate is the thin Bevy integration layer on top
|
|
//! - `asset_loader` bridges the two: wraps parsers as `AssetLoader` impls
|
|
//!
|
|
//! ## WASM notes
|
|
//! On the web, local file access is unavailable. Assets must be
|
|
//! pre-extracted and served over HTTP. XISO reading is native-only.
|
|
|
|
use bevy::prelude::*;
|
|
use bevy_egui::EguiPlugin;
|
|
|
|
pub mod asset_loader;
|
|
pub mod camera;
|
|
pub mod iso_loader;
|
|
pub mod ui;
|
|
|
|
// ── Application state ────────────────────────────────────────────────────────
|
|
|
|
/// Global viewer state, shared across all UI and rendering systems.
|
|
#[derive(Resource)]
|
|
pub struct ViewerState {
|
|
/// Whether the plain-text viewer wraps long lines.
|
|
pub text_wrap: bool,
|
|
}
|
|
|
|
impl Default for ViewerState {
|
|
fn default() -> Self {
|
|
Self { text_wrap: true }
|
|
}
|
|
}
|
|
|
|
// ── App builder ──────────────────────────────────────────────────────────────
|
|
|
|
/// Build and run the viewer application.
|
|
///
|
|
/// Called from `main.rs` on native and from `wasm_bindgen` init on the web.
|
|
pub fn run() {
|
|
let mut app = App::new();
|
|
|
|
app.add_plugins(
|
|
DefaultPlugins.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(),
|
|
resolution: (1280.0, 720.0).into(),
|
|
..default()
|
|
}),
|
|
..default()
|
|
}),
|
|
);
|
|
|
|
app.add_plugins(EguiPlugin);
|
|
app.add_plugins(asset_loader::SylpheedAssetPlugin);
|
|
app.add_plugins(iso_loader::IsoLoaderPlugin);
|
|
app.add_plugins(camera::OrbitCameraPlugin);
|
|
app.add_plugins(ui::ViewerUiPlugin);
|
|
|
|
app.init_resource::<ViewerState>();
|
|
|
|
// Minimal 3D scene: a directional light so meshes are visible
|
|
app.add_systems(Startup, setup_scene);
|
|
|
|
app.run();
|
|
}
|
|
|
|
fn setup_scene(mut commands: Commands) {
|
|
// Bright ambient so surfaces facing away from every light aren't pure black.
|
|
commands.insert_resource(AmbientLight {
|
|
color: Color::srgb(0.9, 0.93, 1.0),
|
|
brightness: 900.0,
|
|
});
|
|
// A multi-directional rig (key + fills + rim + underside) so an orbiting
|
|
// camera always has some light on the visible side — the single front light
|
|
// left the backside unreadable.
|
|
let lights = [
|
|
(Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right
|
|
(Vec3::new(-2.0, 1.0, 0.5), 4_500.0, false), // fill: left
|
|
(Vec3::new(0.5, 0.8, -2.0), 6_000.0, false), // rim: behind
|
|
(Vec3::new(0.0, -1.5, 0.5), 2_500.0, false), // underside fill
|
|
];
|
|
for (from, lux, shadows) in lights {
|
|
commands.spawn((
|
|
DirectionalLight {
|
|
illuminance: lux,
|
|
shadows_enabled: shadows,
|
|
..default()
|
|
},
|
|
Transform::from_translation(from).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
}
|
|
}
|