Skip to content

Commit 015c790

Browse files
committed
Fancy hex view
- Parses array headers and splits array data visually - Shows floats instead of hex where it seems plausible - If a row is all positive floats, a color square is displayed right to that row
1 parent 1dc5ab5 commit 015c790

File tree

4 files changed

+273
-29
lines changed

4 files changed

+273
-29
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "quicktag"
3-
version = "0.5.2"
3+
version = "0.6.0"
44
edition = "2021"
55

66
[dependencies]

src/gui/hextag.rs

Lines changed: 250 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,266 @@
1-
use destiny_pkg::TagHash;
2-
use eframe::egui::Ui;
3-
4-
#[derive(Copy, Clone)]
5-
enum DataViewMode {
6-
Float,
7-
Raw,
8-
U32,
9-
}
1+
use crate::package_manager::package_manager;
2+
use crate::references::REFERENCE_NAMES;
3+
use crate::swap_to_ne;
4+
use binrw::{binread, BinReaderExt, Endian};
5+
use destiny_pkg::{PackageVersion, TagHash};
6+
use eframe::egui;
7+
use eframe::egui::{vec2, Color32, Rgba, RichText, ScrollArea, Sense, Ui};
8+
use itertools::Itertools;
9+
use std::io::{Cursor, Seek, SeekFrom};
1010

1111
pub struct TagHexView {
1212
data: Vec<u8>,
13+
rows: Vec<DataRow>,
14+
array_ranges: Vec<ArrayRange>,
15+
1316
mode: DataViewMode,
17+
detect_floats: bool,
18+
split_arrays: bool,
1419
}
1520

1621
impl TagHexView {
17-
pub fn new(data: Vec<u8>) -> Self {
22+
pub fn new(mut data: Vec<u8>) -> Self {
23+
// Pad data to an alignment of 16 bytes
24+
let remainder = data.len() % 16;
25+
if remainder != 0 {
26+
data.extend(vec![0; 16 - remainder]);
27+
}
28+
1829
Self {
30+
rows: data
31+
.chunks_exact(16)
32+
.map(|chunk| DataRow::from(<[u8; 16]>::try_from(chunk).unwrap()))
33+
.collect(),
34+
array_ranges: find_all_array_ranges(&data),
1935
data,
20-
mode: DataViewMode::Raw,
36+
mode: DataViewMode::Auto,
37+
detect_floats: true,
38+
split_arrays: true,
2139
}
2240
}
2341

2442
pub fn show(&mut self, ui: &mut Ui) -> Option<TagHash> {
43+
ScrollArea::vertical()
44+
.auto_shrink([false, false])
45+
.show(ui, |ui| {
46+
if self.split_arrays && !self.array_ranges.is_empty() {
47+
let first_array_offset = self.array_ranges[0].start as usize;
48+
self.show_row_block(ui, &self.rows[..first_array_offset / 16], 0);
49+
50+
for array in &self.array_ranges {
51+
ui.add_space(16.0);
52+
ui.horizontal(|ui| {
53+
let ref_label = REFERENCE_NAMES
54+
.read()
55+
.get(&array.class)
56+
.map(|s| format!("{s} ({:08X})", array.class))
57+
.unwrap_or_else(|| format!("{:08X}", array.class));
58+
59+
ui.heading(
60+
RichText::new(format!(
61+
"Array {ref_label} ({} elements)",
62+
array.length
63+
))
64+
.color(Color32::WHITE)
65+
.strong(),
66+
);
67+
});
68+
69+
self.show_row_block(
70+
ui,
71+
&self.rows[array.data_start as usize / 16..array.end as usize / 16],
72+
array.data_start as usize,
73+
);
74+
}
75+
} else {
76+
self.show_row_block(ui, &self.rows, 0);
77+
}
78+
});
79+
2580
None
2681
}
82+
83+
fn show_row_block(&self, ui: &mut Ui, rows: &[DataRow], base_offset: usize) {
84+
for (i, row) in rows.iter().enumerate() {
85+
ui.horizontal(|ui| {
86+
ui.strong(format!("{:08X}:", base_offset + i * 16));
87+
match row {
88+
DataRow::Raw(data) => {
89+
let string = data
90+
.chunks_exact(4)
91+
.map(|b| format!("{:02X} {:02X} {:02X} {:02X}", b[0], b[1], b[2], b[2]))
92+
.join(" ");
93+
ui.monospace(string);
94+
}
95+
DataRow::Float(data) => {
96+
let string = data.iter().map(|f| format!("{f:<11.2}")).join(" ");
97+
ui.monospace(string);
98+
99+
if data.iter().all(|&v| v >= 0.0) {
100+
let needs_normalization = data.iter().any(|&v| v > 1.0);
101+
let floats = if needs_normalization {
102+
let factor = data.clone().into_iter().reduce(f32::max).unwrap();
103+
[
104+
data[0] / factor,
105+
data[1] / factor,
106+
data[2] / factor,
107+
data[3] / factor,
108+
]
109+
} else {
110+
data.clone()
111+
};
112+
113+
let color =
114+
Rgba::from_rgb(floats[0].abs(), floats[1].abs(), floats[2].abs());
115+
116+
let (response, painter) =
117+
ui.allocate_painter(vec2(16.0, 16.0), Sense::hover());
118+
119+
painter.rect_filled(response.rect, 0.0, color);
120+
}
121+
}
122+
}
123+
});
124+
}
125+
}
126+
}
127+
128+
#[derive(Copy, Clone)]
129+
enum DataViewMode {
130+
Auto,
131+
Raw,
132+
Float,
133+
U32,
134+
}
135+
136+
enum DataRow {
137+
Raw([u8; 16]),
138+
Float([f32; 4]),
139+
// U32([u32; 4]),
140+
}
141+
142+
impl From<[u8; 16]> for DataRow {
143+
fn from(data: [u8; 16]) -> Self {
144+
let from_xe_bytes = if package_manager().version.endian() == Endian::Big {
145+
f32::from_be_bytes
146+
} else {
147+
f32::from_le_bytes
148+
};
149+
150+
let floats = [
151+
from_xe_bytes(data[0..4].try_into().unwrap()),
152+
from_xe_bytes(data[4..8].try_into().unwrap()),
153+
from_xe_bytes(data[8..12].try_into().unwrap()),
154+
from_xe_bytes(data[12..16].try_into().unwrap()),
155+
];
156+
157+
let mut all_valid_floats = floats
158+
.iter()
159+
.all(|&v| (v.is_normal() && v.abs() < 1e7 && v.abs() > 1e-10) || v == 0.0);
160+
if floats.iter().all(|&v| v == 0.0) {
161+
all_valid_floats = false;
162+
}
163+
164+
if all_valid_floats {
165+
DataRow::Float(floats)
166+
} else {
167+
DataRow::Raw(data)
168+
}
169+
}
170+
}
171+
172+
#[derive(Debug)]
173+
struct ArrayRange {
174+
/// Start of array header
175+
start: u64,
176+
/// Start of array data
177+
data_start: u64,
178+
end: u64,
179+
180+
class: u32,
181+
length: u64,
182+
}
183+
184+
fn find_all_array_ranges(data: &[u8]) -> Vec<ArrayRange> {
185+
let mut cur = Cursor::new(data);
186+
let endian = package_manager().version.endian();
187+
188+
let mut data_chunks_u32 = vec![0u32; data.len() / 4];
189+
190+
unsafe {
191+
std::ptr::copy_nonoverlapping(
192+
data.as_ptr(),
193+
data_chunks_u32.as_mut_ptr() as *mut u8,
194+
data_chunks_u32.len() * 4,
195+
);
196+
}
197+
198+
for value in data_chunks_u32.iter_mut() {
199+
*value = swap_to_ne!(*value, endian);
200+
}
201+
202+
let mut array_offsets = vec![];
203+
for (i, &value) in data_chunks_u32.iter().enumerate() {
204+
let offset = i as u64 * 4;
205+
206+
if matches!(value, 0x80809fb8 | 0x80800184 | 0x80800142) {
207+
array_offsets.push(offset + 4);
208+
}
209+
}
210+
211+
let arrays: Vec<(u64, TagArrayHeader)> = if matches!(
212+
package_manager().version,
213+
PackageVersion::DestinyInternalAlpha | PackageVersion::DestinyTheTakenKing
214+
) {
215+
array_offsets
216+
.into_iter()
217+
.filter_map(|o| {
218+
cur.seek(SeekFrom::Start(o)).ok()?;
219+
Some((
220+
o,
221+
TagArrayHeader {
222+
count: cur.read_be::<u32>().ok()? as _,
223+
tagtype: cur.read_be::<u32>().ok()?,
224+
},
225+
))
226+
})
227+
.collect_vec()
228+
} else {
229+
array_offsets
230+
.into_iter()
231+
.filter_map(|o| {
232+
cur.seek(SeekFrom::Start(o)).ok()?;
233+
Some((o, cur.read_le().ok()?))
234+
})
235+
.collect_vec()
236+
};
237+
238+
let mut array_ranges = vec![];
239+
240+
let file_end = data.len() as u64;
241+
for (offset, header) in arrays {
242+
let start = offset;
243+
let data_start = offset + 16;
244+
245+
array_ranges.push(ArrayRange {
246+
start,
247+
data_start,
248+
end: file_end,
249+
class: header.tagtype,
250+
length: header.count,
251+
})
252+
}
253+
254+
for i in 0..(array_ranges.len().max(1) - 1) {
255+
let next_start = array_ranges.get(i + 1).map(|r| r.start).unwrap_or(file_end);
256+
array_ranges[i].end = next_start;
257+
}
258+
259+
array_ranges
260+
}
261+
262+
#[binread]
263+
struct TagArrayHeader {
264+
pub count: u64,
265+
pub tagtype: u32,
27266
}

src/gui/tag.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ pub struct TagView {
8686
mode: TagViewMode,
8787
}
8888

89+
#[macro_export]
90+
macro_rules! swap_to_ne {
91+
($v:expr, $endian:ident) => {
92+
if $endian != Endian::NATIVE {
93+
$v.swap_bytes()
94+
} else {
95+
$v
96+
}
97+
};
98+
}
99+
89100
impl TagView {
90101
pub fn create(
91102
cache: Arc<TagCache>,
@@ -102,16 +113,6 @@ impl TagView {
102113
let mut string_hashes = vec![];
103114
let mut raw_string_hashes = vec![];
104115

105-
macro_rules! swap_to_ne {
106-
($v:expr, $endian:ident) => {
107-
if $endian != Endian::NATIVE {
108-
$v.swap_bytes()
109-
} else {
110-
$v
111-
}
112-
};
113-
}
114-
115116
let endian = package_manager().version.endian();
116117
let mut data_chunks_u32 = vec![0u32; tag_data.len() / 4];
117118
let mut data_chunks_u64 = vec![0u64; tag_data.len() / 8];
@@ -544,9 +545,13 @@ impl TagView {
544545

545546
for (i, row) in data_f32.chunks(4).enumerate() {
546547
// Check if all values are reasonable enough to be floats. Any very low/high values (with exponents) are likely not floats.
547-
let all_valid = row
548+
549+
let mut all_valid = row
548550
.iter()
549-
.all(|&v| v.is_normal() && v.abs() < 1e10 && (v.abs() > 1e-10 || v == 0.0));
551+
.all(|&v| (v.is_normal() && v.abs() < 1e7 && v.abs() > 1e-10) || v == 0.0);
552+
if row.iter().all(|&v| v == 0.0) {
553+
all_valid = false;
554+
}
550555

551556
if all_valid {
552557
ui.horizontal(|ui| {
@@ -666,15 +671,15 @@ impl View for TagView {
666671
ui.label(RichText::new("No incoming references found").italics());
667672
} else {
668673
let mut references_collapsed =
669-
FxHashMap::<TagHash, UEntryHeader>::default();
674+
FxHashMap::<TagHash, Option<UEntryHeader>>::default();
670675
for (tag, entry) in &self.scan.references {
671676
references_collapsed
672677
.entry(*tag)
673678
.or_insert_with(|| entry.clone());
674679
}
675680

676681
for (tag, entry) in &references_collapsed {
677-
let fancy_tag = format_tag_entry(*tag, Some(entry));
682+
let fancy_tag = format_tag_entry(*tag, entry.as_ref());
678683
let response = ui.add_enabled(
679684
*tag != self.tag,
680685
egui::SelectableLabel::new(false, fancy_tag),
@@ -1017,7 +1022,7 @@ struct ExtendedScanResult {
10171022
pub file_hashes: Vec<ScannedHashWithEntry<ExtendedTagHash>>,
10181023

10191024
/// References from other files
1020-
pub references: Vec<(TagHash, UEntryHeader)>,
1025+
pub references: Vec<(TagHash, Option<UEntryHeader>)>,
10211026
}
10221027

10231028
impl ExtendedScanResult {
@@ -1053,7 +1058,7 @@ impl ExtendedScanResult {
10531058
.references
10541059
.into_iter()
10551060
// TODO(cohae): Unwrap *should* be safe as long as the cache is valid but i want to be sure
1056-
.map(|t| (t, package_manager().get_entry(t).unwrap()))
1061+
.map(|t| (t, package_manager().get_entry(t)))
10571062
.collect(),
10581063
}
10591064
}

0 commit comments

Comments
 (0)