viewer: make the two screen presenters agree, and expose the compose options
The UI Screens window enumerated with `is_build` while the PAK browser's inline preview composes anything `parse_build` accepts. So the browser drew screens this window flatly refused to list -- most visibly `palogo`, the publisher splash, which declares its sprites directly and has no `.rat` layout child. `compose_screen` now gates on `is_composable`, the documented superset (every `is_build` bundle passes it), so anything the list offers is drawable and the two presenters share one rule. A "Fragments" toggle widens the enumeration to it as well, off by default: the extra ~1786 bundles are mostly two-element fragments (a button beside its glow) that would bury the real screens. It re-scans, which renumbers the display ordinal -- harmless, because the pak ENTRY index is the locator and the stale catalog is cleared. The toggle sets a `rescan` flag rather than self-sending RequestScreenCatalog: a system that both reads and writes one event type is a B0002 panic at startup, which is how the Save browser broke. Also plumbs the last two ComposeOptions the CLI had and the UI did not -- `black backdrop` (what a framebuffer capture must be compared against) and `primitives` (decoded, but paint order unsolved; hover text says so). Verified: `screen render --all --build 13 GP_TITLE.pak` draws the SQUARE ENIX splash, 2/3 elements, the third being the .prm quad. Workspace builds, viewer reaches steady state, disc-gated suite 20/20 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -716,6 +716,21 @@ pub struct ScreenBrowser {
|
||||
pub loading: bool,
|
||||
pub loaded: bool,
|
||||
pub filter: String,
|
||||
/// Enumerate every bundle `compose` can draw ([`ui_layout::is_composable`])
|
||||
/// rather than only full screens ([`ui_layout::is_build`], the default).
|
||||
///
|
||||
/// This is what reconciles the two presenters: the pak browser's inline
|
||||
/// preview composes any RATC bundle it can, so without this toggle it drew
|
||||
/// things this window flatly refused to list — most visibly the developer
|
||||
/// logo splash, which declares its sprites directly and has no `.rat` child.
|
||||
/// Off by default because the extra ~1 786 bundles are mostly two-element
|
||||
/// fragments (a button beside its glow), which would bury the real screens.
|
||||
pub include_fragments: bool,
|
||||
/// Set by the window when `include_fragments` is toggled. A flag, not a
|
||||
/// self-sent [`RequestScreenCatalog`]: a system that both reads and writes
|
||||
/// one event type is a Bevy B0002 panic at startup, which is how the Save
|
||||
/// browser broke. `handle_screen_catalog_request` consumes it.
|
||||
pub rescan: bool,
|
||||
pub paks: Vec<ScreenPak>,
|
||||
/// Which pak is selected, and which of its builds.
|
||||
pub selected: Option<usize>,
|
||||
@@ -724,6 +739,13 @@ pub struct ScreenBrowser {
|
||||
pub show_focus: bool,
|
||||
/// Draw `loop*` sprite animations.
|
||||
pub show_animated: bool,
|
||||
/// Composite over black instead of the default dim slate — what the game
|
||||
/// composites over on a screen that carries its own full-screen background,
|
||||
/// and so what a framebuffer capture must be compared against.
|
||||
pub black_backdrop: bool,
|
||||
/// Draw the untextured `.prm` fade/dim/flash quads. Off by default because
|
||||
/// their paint order is unsolved, not because they are undecoded.
|
||||
pub show_primitives: bool,
|
||||
/// Per-element visibility for the current build (index-aligned).
|
||||
pub hidden: Vec<bool>,
|
||||
pub composed: Option<ComposedScreenResult>,
|
||||
@@ -737,7 +759,12 @@ pub struct ScreenBrowser {
|
||||
|
||||
/// Ask the loader to find the UI screen paks on the disc.
|
||||
#[derive(Event, Default)]
|
||||
pub struct RequestScreenCatalog;
|
||||
pub struct RequestScreenCatalog {
|
||||
/// Re-scan even though a catalog is already loaded — the enumeration rule
|
||||
/// changed. The View-menu button sends the default (`false`), so re-opening
|
||||
/// the window stays free.
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// Ask the loader to reassemble + composite one screen build.
|
||||
#[derive(Event)]
|
||||
@@ -750,6 +777,8 @@ pub struct RequestScreenCompose {
|
||||
pub entry: usize,
|
||||
pub focus: bool,
|
||||
pub animated: bool,
|
||||
pub black_backdrop: bool,
|
||||
pub primitives: bool,
|
||||
/// Element indices to leave out (the per-element toggles).
|
||||
pub hidden: Vec<bool>,
|
||||
}
|
||||
@@ -4120,25 +4149,41 @@ fn handle_screen_catalog_request(
|
||||
channels: Res<IsoChannels>,
|
||||
mut screens: ResMut<ScreenBrowser>,
|
||||
) {
|
||||
if events.read().next().is_none() {
|
||||
// Either the View-menu event, or the in-window toggle's flag.
|
||||
let force = match events.read().last() {
|
||||
Some(req) => req.force || screens.rescan,
|
||||
None if screens.rescan => true,
|
||||
None => return,
|
||||
};
|
||||
screens.rescan = false;
|
||||
if screens.loading || (screens.loaded && !force) {
|
||||
return;
|
||||
}
|
||||
if screens.loaded || screens.loading {
|
||||
return;
|
||||
if force {
|
||||
// The rule changed, so every locator in the old catalog is stale.
|
||||
screens.loaded = false;
|
||||
screens.paks.clear();
|
||||
screens.selected = None;
|
||||
screens.composed = None;
|
||||
}
|
||||
screens.loading = true;
|
||||
let fragments = screens.include_fragments;
|
||||
let source = iso_state.source_kind.clone();
|
||||
let files = browser.files.clone();
|
||||
let sender = channels.sender.clone();
|
||||
std::thread::spawn(move || {
|
||||
let paks = build_screen_catalog(&source, &files);
|
||||
let paks = build_screen_catalog(&source, &files, fragments);
|
||||
let _ = sender.send(IsoLoaderMsg::ScreenCatalogLoaded(paks));
|
||||
});
|
||||
}
|
||||
|
||||
/// Open every `GP_*.pak` and record which entries parse as a screen build.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_screen_catalog(source: &SourceKind, files: &[String]) -> Vec<ScreenPak> {
|
||||
fn build_screen_catalog(
|
||||
source: &SourceKind,
|
||||
files: &[String],
|
||||
fragments: bool,
|
||||
) -> Vec<ScreenPak> {
|
||||
use sylpheed_formats::ui_layout;
|
||||
let mut out = Vec::new();
|
||||
let mut candidates: Vec<&String> = files
|
||||
@@ -4170,7 +4215,12 @@ fn build_screen_catalog(source: &SourceKind, files: &[String]) -> Vec<ScreenPak>
|
||||
}
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
budget += bytes.len();
|
||||
if ui_layout::is_build(&bytes) {
|
||||
let keep = if fragments {
|
||||
ui_layout::is_composable(&bytes)
|
||||
} else {
|
||||
ui_layout::is_build(&bytes)
|
||||
};
|
||||
if keep {
|
||||
builds.push((i, bytes.len()));
|
||||
}
|
||||
}
|
||||
@@ -4203,18 +4253,17 @@ fn handle_screen_compose_request(
|
||||
screens.generation = screens.generation.wrapping_add(1);
|
||||
screens.composing = true;
|
||||
let generation = screens.generation;
|
||||
let (pak, build, entry, focus, animated, hidden) = (
|
||||
req.pak.clone(),
|
||||
req.build,
|
||||
req.entry,
|
||||
req.focus,
|
||||
req.animated,
|
||||
req.hidden.clone(),
|
||||
);
|
||||
let opts = ScreenComposeOpts {
|
||||
focus: req.focus,
|
||||
animated: req.animated,
|
||||
black_backdrop: req.black_backdrop,
|
||||
primitives: req.primitives,
|
||||
};
|
||||
let (pak, build, entry, hidden) = (req.pak.clone(), req.build, req.entry, req.hidden.clone());
|
||||
let source = iso_state.source_kind.clone();
|
||||
let sender = channels.sender.clone();
|
||||
std::thread::spawn(move || {
|
||||
let composed = compose_screen(&source, &pak, entry, build, focus, animated, &hidden);
|
||||
let composed = compose_screen(&source, &pak, entry, build, opts, &hidden);
|
||||
let _ = sender.send(IsoLoaderMsg::ScreenComposed {
|
||||
generation,
|
||||
composed: Box::new(composed),
|
||||
@@ -4222,14 +4271,23 @@ fn handle_screen_compose_request(
|
||||
});
|
||||
}
|
||||
|
||||
/// The compose toggles, bundled so `compose_screen` keeps a readable signature.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone, Copy)]
|
||||
struct ScreenComposeOpts {
|
||||
focus: bool,
|
||||
animated: bool,
|
||||
black_backdrop: bool,
|
||||
primitives: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn compose_screen(
|
||||
source: &SourceKind,
|
||||
pak: &str,
|
||||
entry: usize,
|
||||
build_idx: usize,
|
||||
focus: bool,
|
||||
animated: bool,
|
||||
opts: ScreenComposeOpts,
|
||||
hidden: &[bool],
|
||||
) -> ComposedScreenResult {
|
||||
use sylpheed_formats::ui_layout::{self, ComposeOptions};
|
||||
@@ -4251,21 +4309,21 @@ fn compose_screen(
|
||||
// version inflated EVERY entry in the pak and held every build in memory at
|
||||
// once, just to index into the result — repeated on every checkbox click,
|
||||
// with the ISO re-opened from scratch each time. That, not the compositing,
|
||||
// was the cost.
|
||||
let one: Option<Vec<u8>> = ar
|
||||
// was the cost. `build_idx` stays a display ordinal; `entry` is the locator.
|
||||
//
|
||||
// Gate on `is_composable`, not `is_build`: it is the documented superset
|
||||
// (every `is_build` bundle passes it) and it is the rule the pak browser's
|
||||
// inline preview already draws by, so the two presenters agree about what
|
||||
// this viewer can render.
|
||||
let Some(bundle) = ar
|
||||
.entries()
|
||||
.get(entry)
|
||||
.and_then(|e| ar.read(e).ok())
|
||||
.filter(|b| ui_layout::is_build(b));
|
||||
let bundles: Vec<Vec<u8>> = one.into_iter().collect();
|
||||
let Some(bundle) = bundles.first().filter(|_| {
|
||||
// `build_idx` stays the display ordinal; `entry` is the real locator.
|
||||
let _ = build_idx;
|
||||
true
|
||||
}) else {
|
||||
return fail(format!("build {build_idx} not in {pak}"));
|
||||
.filter(|b| ui_layout::is_composable(b))
|
||||
else {
|
||||
return fail(format!("build {build_idx} (entry {entry}) not in {pak}"));
|
||||
};
|
||||
let Some(build) = ui_layout::parse_build(bundle) else {
|
||||
let Some(build) = ui_layout::parse_build(&bundle) else {
|
||||
return fail("build did not parse".into());
|
||||
};
|
||||
// `visible` is the inverse of the UI's `hidden`; a short list means "all
|
||||
@@ -4277,11 +4335,16 @@ fn compose_screen(
|
||||
.collect();
|
||||
let screen = ui_layout::compose(
|
||||
&build,
|
||||
bundle,
|
||||
&bundle,
|
||||
ComposeOptions {
|
||||
include_focus: focus,
|
||||
include_animated: animated,
|
||||
..Default::default()
|
||||
include_focus: opts.focus,
|
||||
include_animated: opts.animated,
|
||||
include_primitives: opts.primitives,
|
||||
backdrop: if opts.black_backdrop {
|
||||
[0, 0, 0, 255]
|
||||
} else {
|
||||
ComposeOptions::default().backdrop
|
||||
},
|
||||
},
|
||||
Some(&visible),
|
||||
);
|
||||
|
||||
@@ -1876,6 +1876,21 @@ fn draw_screens_ui(
|
||||
if !screens.filter.is_empty() && ui.small_button("✖").clicked() {
|
||||
screens.filter.clear();
|
||||
}
|
||||
ui.separator();
|
||||
// Widens the enumeration to everything `compose` can draw — the
|
||||
// same rule the pak browser's inline preview uses. Costs a
|
||||
// re-scan, so it is a deliberate click rather than the default.
|
||||
if ui
|
||||
.checkbox(&mut screens.include_fragments, "Fragments")
|
||||
.on_hover_text(
|
||||
"Also list RATC bundles with no .rat layout child: the developer-logo \
|
||||
splash, and ~1 786 two-element fragments (a button beside its glow). \
|
||||
These are what the PAK browser preview draws but this list omits.",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
screens.rescan = true;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
@@ -1928,6 +1943,32 @@ fn draw_screens_ui(
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut screens.black_backdrop, "Black backdrop")
|
||||
.on_hover_text(
|
||||
"Composite over black instead of the default dim slate. The slate \
|
||||
stands in for the PRMD dim quad plus the live 3D scene behind an \
|
||||
in-mission screen; black is what the game composites over on a \
|
||||
screen carrying its own background — and what a framebuffer \
|
||||
capture must be compared against.",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut screens.show_primitives, "Primitives")
|
||||
.on_hover_text(
|
||||
"Draw the untextured .prm fade/dim/flash quads. Decoded, but their \
|
||||
paint order is unsolved: a primitive has no T8aD header and so no \
|
||||
layer key, and the derived order forces keyless elements last. \
|
||||
That is measurably wrong — expect an opaque quad to wipe some \
|
||||
screens (32 of GP_DIALOG's builds).",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if screens.composing {
|
||||
ui.spinner();
|
||||
ctx.request_repaint();
|
||||
@@ -2096,6 +2137,8 @@ fn draw_screens_ui(
|
||||
.unwrap_or(0),
|
||||
focus: screens.show_focus,
|
||||
animated: screens.show_animated,
|
||||
black_backdrop: screens.black_backdrop,
|
||||
primitives: screens.show_primitives,
|
||||
hidden: screens.hidden.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user