1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-25 05:47:45 +00:00
physis/src/dic.rs
Joshua Goins d58a216462 Shrink the dependency and feature complexity, auto-cleanup and more
We had a few odd dependencies that caused nothing but pain in dependent projects
like libphysis. One of these was libunshield (a C library) that our game_install
feature used, but to be honest this was the wrong library to put this code. It
was really only ever used by Astra, and should live there instead - there's no
reason to have it shared between applications (and it's small enough to be
copied if *you* need it.) Also that also killed the system-deps dependency which
had a significant impact on our build time.

Another dependency was replaced: libz-sys. This is replaced by the pure Rust
libz-rs (through libz-rs-sys) which should simplify deploying physis without
having to worry about manually linking libz or other nonsense. Some leftover
copied code from flate2 can also be removed.

I also removed the visual_data feature as Astra ended up using it anyway, and
the distinction doesn't make much sense now. It was previously to gate some
dependencies needed for visual data extraction, but the bitflags and half crates
are small. I can look into splitting the crate up into more features if needed
later.

A dependency that was erroneously included in the refactoring was quote, which
has been removed. Also ran cargo fmt, clippy too.
2025-03-11 16:29:24 -04:00

241 lines
6.4 KiB
Rust

// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
use std::collections::HashMap;
use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan;
use binrw::binrw;
use binrw::{BinRead, BinReaderExt};
// Based off of https://github.com/Lotlab/ffxiv-vulgar-words-reader/
// Credit goes to Jim Kirisame for documenting this format
// TODO: double check I'm reading everything correctly
#[binrw]
#[derive(Debug)]
#[brw(little)]
pub struct EntryItem {
flag: u32,
sibling: u32,
child: u32,
offset: u32,
}
#[binrw]
#[derive(Debug)]
#[brw(little)]
struct DictionaryHeader {
#[br(seek_before = SeekFrom::Start(0x8124))]
#[br(count = 256)]
chara_replace1: Vec<u16>,
#[br(count = 256)]
chara_replace2: Vec<u16>,
#[br(count = 256)]
chara_replace3: Vec<u16>,
#[br(count = 5)]
block_offsets: Vec<u32>,
#[br(count = 5)]
block_lengths: Vec<u32>,
#[br(pad_before = 4)]
#[br(count = 256)]
chara_block: Vec<u32>,
#[br(ignore)]
begin_node: Vec<u16>,
#[br(ignore)]
inner_node: Vec<u16>,
#[br(ignore)]
chara: Vec<u16>,
#[br(ignore)]
word: Vec<u16>,
#[br(ignore)]
entries: Vec<EntryItem>,
}
pub struct Dictionary {
header: DictionaryHeader,
pub words: Vec<String>,
}
impl Dictionary {
/// Parses an existing dictionary file.
pub fn from_existing(buffer: ByteSpan) -> Option<Dictionary> {
let mut cursor = Cursor::new(buffer);
let mut dict = DictionaryHeader::read(&mut cursor).unwrap();
let map_start = 0x8750u32;
let map_size = 0x200u32;
// fix up offsets
for offset in &mut dict.block_offsets {
*offset = *offset + map_start + map_size;
}
for i in 0..dict.block_lengths[0] / 2 {
let offset = dict.block_offsets[0] + i * 2;
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
dict.begin_node.push(cursor.read_le::<u16>().ok()?);
}
for i in 0..dict.block_lengths[1] / 2 {
let offset = dict.block_offsets[1] + i * 2;
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
dict.inner_node.push(cursor.read_le::<u16>().ok()?);
}
for i in 0..dict.block_lengths[2] / 2 {
let offset = dict.block_offsets[2] + i * 2;
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
dict.chara.push(cursor.read_le::<u16>().ok()?);
}
for i in 0..dict.block_lengths[3] / 2 {
let offset = dict.block_offsets[3] + i * 2;
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
dict.word.push(cursor.read_le::<u16>().ok()?);
}
for i in 0..dict.block_lengths[4] / 16 {
let offset = dict.block_offsets[4] + i * 16;
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
dict.entries.push(cursor.read_le::<EntryItem>().ok()?);
}
let mut dict = Dictionary {
header: dict,
words: Vec::new(),
};
// TODO: lol
dict.words = dict.list_words()?;
Some(dict)
}
fn list_words(&self) -> Option<Vec<String>> {
let mut result = Vec::new();
let lut = self.generate_index_rune_lookup_table();
for (id, v) in self.header.begin_node.iter().enumerate() {
if *v == 0 {
continue;
}
let chara = Dictionary::index_to_rune(&lut, id as u32);
self.dump_dict_node(&mut result, *v as i32, String::from(chara as u8 as char))
}
Some(result)
}
fn generate_index_rune_lookup_table(&self) -> HashMap<u16, u16> {
let mut map = HashMap::new();
for i in 0..self.header.chara_block.len() {
map.insert(self.header.chara_block[i] as u16, i as u16);
}
map
}
fn index_to_rune(lookup_table: &HashMap<u16, u16>, index: u32) -> i32 {
let higher = index >> 8;
let lower = index & 0xFF;
if higher == 0 {
return 0;
}
if let Some(new_val) = lookup_table.get(&(higher as u16)) {
(((*new_val as u32) << 8) + lower) as i32
} else {
0
}
}
fn dump_dict_node(&self, vec: &mut Vec<String>, entry_id: i32, prev: String) {
let node = &self.header.entries[entry_id as usize];
for i in 0..node.sibling {
let Some(current) = self.get_string(entry_id, i as i32) else {
return;
};
if node.child == 0 {
vec.push(prev.clone() + &current);
continue;
}
let value = self.header.inner_node[(node.child + i) as usize];
if value == 0 {
vec.push(prev.clone() + &current);
continue;
}
self.dump_dict_node(vec, value as i32, prev.clone() + &current);
}
}
fn get_string(&self, entry_id: i32, sibling_id: i32) -> Option<String> {
if let Some(characters) = self.get_string_characters(entry_id, sibling_id) {
return String::from_utf16(&characters).ok();
}
None
}
fn get_string_characters(&self, entry_id: i32, sibling_id: i32) -> Option<Vec<u16>> {
if entry_id as usize >= self.header.entries.len() {
return None;
}
let entry = self.header.entries.get(entry_id as usize)?;
if entry.flag == 0 {
let pos = (entry.offset / 2) as i32 + sibling_id;
if pos as usize > self.header.chara.len() {
return None;
}
if self.header.chara[pos as usize] == 0 {
return None;
}
return Some(vec![self.header.chara[pos as usize]]);
}
let begin = entry.offset / 2;
let mut end = begin + 1;
while (end as usize) < self.header.word.len() && self.header.word[end as usize] != 0 {
end += 1;
}
Some(self.header.word[begin as usize..end as usize].to_vec())
}
}
#[cfg(test)]
mod tests {
use std::fs::read;
use std::path::PathBuf;
use super::*;
#[test]
fn test_invalid() {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/tests");
d.push("random");
// Feeding it invalid data should not panic
Dictionary::from_existing(&read(d).unwrap());
}
}