mirror of
https://github.com/redstrate/Physis.git
synced 2025-04-25 05:47:45 +00:00
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.
241 lines
6.4 KiB
Rust
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() + ¤t);
|
|
continue;
|
|
}
|
|
|
|
let value = self.header.inner_node[(node.child + i) as usize];
|
|
if value == 0 {
|
|
vec.push(prev.clone() + ¤t);
|
|
continue;
|
|
}
|
|
|
|
self.dump_dict_node(vec, value as i32, prev.clone() + ¤t);
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|