Skip to content

Commit e527d52

Browse files
committed
fix: preview D4 paylow/paymed .tex from the payload twin
D4 paylow/ and paymed/ files are mip-stream variants whose byte layout isn't reverse-engineered yet, so the brute-force BC decoder can't recover power-of-2 dimensions and the preview pane just shows "could not be decoded". When that happens, fetch the matching base/payload/<rest> file via SiblingFetcher and preview that instead, with a note in the text panel calling out what the user is actually looking at. Export Raw is unaffected (framework-level) so the original paylow bytes are still what gets written. PNG export uses the twin bytes too, since it should mirror what's on screen.
1 parent 65b58d5 commit e527d52

1 file changed

Lines changed: 125 additions & 29 deletions

File tree

rustydemon/src/preview/tex.rs

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,133 @@ impl PreviewPlugin for TexPreview {
2020
filename: &str,
2121
data: &[u8],
2222
ctx: &egui::Context,
23-
_fetch: &super::SiblingFetcher<'_>,
23+
fetch: &super::SiblingFetcher<'_>,
2424
) -> PreviewOutput {
25-
let mut out = PreviewOutput::new();
25+
// Try the file as-is first. paylow/paymed are mip-stream variants we
26+
// can't decode yet, so on failure we fall back to previewing the
27+
// matching payload/ twin. Export Raw still writes the original bytes.
28+
if let Some((rgba, w, h, fmt)) = crate::tex_preview::decode_tex(data, filename) {
29+
return build_output(ctx, rgba, w, h, fmt, filename.to_owned(), None, None);
30+
}
31+
32+
if let Some((kind, twin_path)) = payload_twin(filename) {
33+
if let Some(twin_bytes) = (fetch.by_name)(&twin_path) {
34+
if let Some((rgba, w, h, fmt)) =
35+
crate::tex_preview::decode_tex(&twin_bytes, &twin_path)
36+
{
37+
let note = format!(
38+
"{kind}/ mipmap stream isn't decoded yet — preview is from \
39+
the matching payload/ variant. Export Raw still writes \
40+
the original {kind} bytes."
41+
);
42+
return build_output(
43+
ctx,
44+
rgba,
45+
w,
46+
h,
47+
fmt,
48+
twin_path,
49+
Some(twin_bytes),
50+
Some(note),
51+
);
52+
}
53+
}
54+
}
2655

27-
let Some((rgba, w, h, fmt)) = crate::tex_preview::decode_tex(data, filename) else {
28-
out.text = Some(
29-
".tex header could not be decoded — unknown dimensions or unsupported BC format."
30-
.into(),
31-
);
32-
return out;
33-
};
34-
35-
let color_image = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &rgba);
36-
out.texture =
37-
Some(ctx.load_texture("tex_preview", color_image, egui::TextureOptions::default()));
38-
out.texture_pixels = Some((rgba, w, h));
39-
out.text = Some(format!(
40-
"D4 .tex texture\n{w}×{h} {fmt}\n\nDecoded from raw block-compressed data."
41-
));
42-
43-
let filename = filename.to_owned();
44-
out.extra_exports.push(ExportAction {
45-
label: "Export As PNG",
46-
default_extension: "png",
47-
filter_name: "PNG image",
48-
build: Arc::new(move |data, _path| {
49-
let (rgba, w, h, _fmt) = crate::tex_preview::decode_tex(data, &filename)
50-
.ok_or_else(|| "tex decode failed".to_string())?;
51-
crate::preview::encode_png(&rgba, w, h)
52-
}),
53-
});
56+
let mut out = PreviewOutput::new();
57+
out.text = Some(
58+
".tex header could not be decoded — unknown dimensions or unsupported BC format."
59+
.into(),
60+
);
5461
out
5562
}
5663
}
64+
65+
/// If `filename` lives under `base/paylow/` or `base/paymed/`, return
66+
/// `(tier_label, twin_path_under_base/payload/)` so the plugin can fetch
67+
/// the full-resolution variant for preview.
68+
fn payload_twin(filename: &str) -> Option<(&'static str, String)> {
69+
let lower = filename.to_ascii_lowercase();
70+
if let Some(rest) = lower.strip_prefix("base/paylow/") {
71+
Some(("paylow", format!("base/payload/{rest}")))
72+
} else if let Some(rest) = lower.strip_prefix("base/paymed/") {
73+
Some(("paymed", format!("base/payload/{rest}")))
74+
} else {
75+
None
76+
}
77+
}
78+
79+
#[allow(clippy::too_many_arguments)]
80+
fn build_output(
81+
ctx: &egui::Context,
82+
rgba: Vec<u8>,
83+
w: u32,
84+
h: u32,
85+
fmt: &'static str,
86+
decode_filename: String,
87+
decoded_bytes_for_export: Option<Vec<u8>>,
88+
note: Option<String>,
89+
) -> PreviewOutput {
90+
let mut out = PreviewOutput::new();
91+
let color_image = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &rgba);
92+
out.texture =
93+
Some(ctx.load_texture("tex_preview", color_image, egui::TextureOptions::default()));
94+
out.texture_pixels = Some((rgba, w, h));
95+
96+
let mut text =
97+
format!("D4 .tex texture\n{w}×{h} {fmt}\n\nDecoded from raw block-compressed data.");
98+
if let Some(n) = note {
99+
text.push_str("\n\n");
100+
text.push_str(&n);
101+
}
102+
out.text = Some(text);
103+
104+
out.extra_exports.push(ExportAction {
105+
label: "Export As PNG",
106+
default_extension: "png",
107+
filter_name: "PNG image",
108+
build: Arc::new(move |data, _path| {
109+
// PNG export mirrors what the user *sees*. When we previewed a
110+
// payload twin, encode that; otherwise fall back to the file
111+
// bytes the framework hands us.
112+
let bytes: &[u8] = decoded_bytes_for_export.as_deref().unwrap_or(data);
113+
let (rgba, w, h, _fmt) = crate::tex_preview::decode_tex(bytes, &decode_filename)
114+
.ok_or_else(|| "tex decode failed".to_string())?;
115+
crate::preview::encode_png(&rgba, w, h)
116+
}),
117+
});
118+
out
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::payload_twin;
124+
125+
#[test]
126+
fn paylow_maps_to_payload_twin() {
127+
let (tier, twin) =
128+
payload_twin("base/paylow/Texture/warlock_sigilOfSummons_Color.tex").unwrap();
129+
assert_eq!(tier, "paylow");
130+
assert_eq!(
131+
twin,
132+
"base/payload/texture/warlock_sigilofsummons_color.tex"
133+
);
134+
}
135+
136+
#[test]
137+
fn paymed_maps_to_payload_twin() {
138+
let (tier, twin) = payload_twin("base/paymed/Texture/some_file.tex").unwrap();
139+
assert_eq!(tier, "paymed");
140+
assert_eq!(twin, "base/payload/texture/some_file.tex");
141+
}
142+
143+
#[test]
144+
fn payload_returns_none() {
145+
assert!(payload_twin("base/payload/Texture/foo.tex").is_none());
146+
}
147+
148+
#[test]
149+
fn unrelated_path_returns_none() {
150+
assert!(payload_twin("World/Maps/Azeroth/foo.adt").is_none());
151+
}
152+
}

0 commit comments

Comments
 (0)