use log::warn; use std::collections::{HashMap, BTreeMap, BTreeSet}; use std::fs::File; use std::io::{Read, Write, Cursor, Seek, SeekFrom}; use std::path::PathBuf; use std::convert::TryInto; use async_std::sync::Arc; use thiserror::Error; use serde::{Serialize, Deserialize}; use ages_prs::{LegacyPrsDecoder, LegacyPrsEncoder}; use byteorder::{LittleEndian, ReadBytesExt}; use libpso::util::array_to_utf16; use crate::ship::map::{MapArea, MapAreaError, MapObject, MapEnemy, enemy_data_from_stream, objects_from_stream}; use crate::ship::room::{Episode, RoomMode}; use crate::ship::map::area::{MapAreaLookup, MapAreaLookupBuilder}; #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct QuestCategory { index: usize, pub name: String, pub description: String, } #[derive(Debug, Serialize, Deserialize, Hash)] struct QuestListEntry { bin: String, dat: String, } #[derive(Debug, Serialize, Deserialize, Hash)] struct QuestListCategory { list_order: usize, description: String, quests: Vec, } #[derive(Debug, Serialize, Deserialize)] struct QuestListConfig { questlist: HashMap>, } #[derive(Error, Debug)] #[error("")] pub enum ParseDatError { IoError(#[from] std::io::Error), MapError(#[from] MapAreaError), UnknownDatHeader(u32), CouldNotDetermineEpisode, InvalidMapAreaId(u16), } const DAT_OBJECT_HEADER_ID: u32 = 1; const DAT_ENEMY_HEADER_ID: u32 = 2; const DAT_WAVE_HEADER_ID: u32 = 3; enum DatBlock { Object(Vec>), Enemy(Vec>), Wave, } fn read_dat_section_header(cursor: &mut T, episode: &Episode, map_areas: &MapAreaLookup) -> Result { let header = cursor.read_u32::()?; let _offset = cursor.read_u32::()?; let area = cursor.read_u16::()?; let _unknown1 = cursor.read_u16::()?; let length = cursor.read_u32::()?; let map_area = map_areas.get_area_map(area).map_err(|_| ParseDatError::InvalidMapAreaId(area))?; match header { DAT_OBJECT_HEADER_ID => { let mut obj_data = vec![0u8; length as usize]; cursor.read_exact(&mut obj_data)?; let mut obj_cursor = Cursor::new(obj_data); let objects = objects_from_stream(&mut obj_cursor, episode, &map_area); Ok(DatBlock::Object(objects)) }, DAT_ENEMY_HEADER_ID => { let mut enemy_data = vec![0u8; length as usize]; cursor.read_exact(&mut enemy_data)?; let mut enemy_cursor = Cursor::new(enemy_data); let enemies = enemy_data_from_stream(&mut enemy_cursor, &map_area, episode); Ok(DatBlock::Enemy(enemies)) }, DAT_WAVE_HEADER_ID => { cursor.seek(SeekFrom::Current(length as i64))?; Ok(DatBlock::Wave) }, _ => Err(ParseDatError::UnknownDatHeader(header)) } } fn quest_episode(bin: &[u8]) -> Option { for bytes in bin.windows(3) { // set_episode if bytes[0] == 0xF8 && bytes[1] == 0xBC { return Episode::from_quest(bytes[2]).ok() } } None } fn map_area_mappings(bin: &[u8]) -> MapAreaLookup { let mut map_areas = MapAreaLookupBuilder::default(); for bytes in bin.windows(4) { // BB_Map_Designate if bytes[0] == 0xF9 && bytes[1] == 0x51 { //return Some(Episode::from_quest(bytes[2]).ok()?) let floor_value = bytes[2] as u16; if let Some(map_area) = MapArea::from_bb_map_designate(bytes[3]) { map_areas = map_areas.add(floor_value, map_area); } } } map_areas.build() } #[allow(clippy::type_complexity)] fn parse_dat(dat: &[u8], episode: &Episode, map_areas: &MapAreaLookup) -> Result<(Vec>, Vec>), ParseDatError> { let mut cursor = Cursor::new(dat); let header_iter = std::iter::from_fn(move || { match read_dat_section_header(&mut cursor, episode, map_areas) { Ok(dat_block) => Some(dat_block), Err(err) => { warn!("unknown header in dat: {:?}", err); None } } }); Ok(header_iter.fold((Vec::new(), Vec::new()), |(mut enemies, mut objects), dat_block| { match dat_block { DatBlock::Object(mut objs) => { objects.append(&mut objs) }, DatBlock::Enemy(mut enemy) => { enemies.append(&mut enemy) }, _ => {} } (enemies, objects) })) } #[derive(Error, Debug)] pub enum QuestLoadError { #[error("io error {0}")] IoError(#[from] std::io::Error), #[error("parse dat error {0}")] ParseDatError(#[from] ParseDatError), #[error("could not read metadata")] CouldNotReadMetadata, #[error("could not load config file")] CouldNotLoadConfigFile, } #[derive(Debug, Clone)] pub struct Quest { pub name: String, pub description: String, pub full_description: String, pub language: u16, pub id: u16, pub bin_blob: Arc>, pub dat_blob: Arc>, pub enemies: Vec>, // TODO: Arc? pub objects: Vec>, // TODO: Arc? pub map_areas: MapAreaLookup, // TODO: Arc? } impl Quest { fn from_bin_dat(bin: Vec, dat: Vec) -> Result { let id = u16::from_le_bytes(bin[16..18].try_into().map_err(|_| QuestLoadError::CouldNotReadMetadata)?); let language = u16::from_le_bytes(bin[18..20].try_into().map_err(|_| QuestLoadError::CouldNotReadMetadata)?); let name = array_to_utf16(&bin[24..88]); let description = array_to_utf16(&bin[88..334]); let full_description = array_to_utf16(&bin[334..920]); let episode = quest_episode(&bin).ok_or(ParseDatError::CouldNotDetermineEpisode)?; let map_areas = map_area_mappings(&bin); let (enemies, objects) = parse_dat(&dat, &episode, &map_areas)?; let mut prs_bin = LegacyPrsEncoder::new(Vec::new()); prs_bin.write_all(&bin)?; let mut prs_dat = LegacyPrsEncoder::new(Vec::new()); prs_dat.write_all(&dat)?; Ok(Quest { name, description, full_description, id, language, bin_blob: Arc::new(prs_bin.into_inner().map_err(|_| QuestLoadError::CouldNotReadMetadata)?), dat_blob: Arc::new(prs_dat.into_inner().map_err(|_| QuestLoadError::CouldNotReadMetadata)?), enemies, objects, map_areas, }) } } // QuestCollection pub type QuestList = BTreeMap>; pub fn load_quest(bin_path: PathBuf, dat_path: PathBuf, quest_path: PathBuf) -> Option { let dat_file = File::open(quest_path.join(dat_path.clone())) .map_err(|err| { warn!("could not load quest file {:?}: {:?}", dat_path, err) }).ok()?; let bin_file = File::open(quest_path.join(bin_path.clone())) .map_err(|err| { warn!("could not load quest file {:?}: {:?}", bin_path, err) }).ok()?; let mut dat_prs = LegacyPrsDecoder::new(dat_file); let mut bin_prs = LegacyPrsDecoder::new(bin_file); let mut dat = Vec::new(); let mut bin = Vec::new(); dat_prs.read_to_end(&mut dat).ok()?; bin_prs.read_to_end(&mut bin).ok()?; let quest = Quest::from_bin_dat(bin, dat).map_err(|err| { warn!("could not parse quest file {:?}/{:?}: {:?}", bin_path, dat_path, err) }).ok()?; Some(quest) } pub fn load_quests_path(mut quest_path: PathBuf) -> Result { let mut f = File::open(quest_path.clone()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?; let mut s = String::new(); f.read_to_string(&mut s)?; quest_path.pop(); // remove quests.toml from the path let mut used_quest_ids = BTreeSet::new(); let ql: BTreeMap = toml::from_str(s.as_str()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?; Ok(ql.into_iter().map(|(category, category_details)| { ( QuestCategory { index: category_details.list_order, name: category, description: category_details.description, }, category_details.quests .into_iter() .filter_map(|quest| { load_quest(quest.bin.into(), quest.dat.into(), quest_path.to_path_buf()) .and_then(|quest | { if used_quest_ids.contains(&quest.id) { warn!("quest id already exists: {}", quest.id); return None; } used_quest_ids.insert(quest.id); Some(quest) }) }) .collect() ) }).collect()) } pub fn load_standard_quests(mode: RoomMode) -> Result { match mode { RoomMode::Single {episode, .. } => { load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "single", "quests.toml"])) }, RoomMode::Multi {episode, .. } => { load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "multi", "quests.toml"])) }, _ => { Ok(BTreeMap::new()) } } } pub fn load_government_quests(mode: RoomMode) -> Result { match mode { RoomMode::Single {episode, .. } => { load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "government", "quests.toml"])) }, RoomMode::Multi {episode, .. } => { load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "government", "quests.toml"])) }, _ => { Ok(BTreeMap::new()) } } } #[cfg(test)] mod tests { use super::*; // the quest phantasmal world 4 uses the tower map twice, to do this it had to remap // one of the other maps to be a second tower #[test] fn test_quest_with_remapped_floors() { let pw4 = load_quest("q236-ext-bb.bin".into(), "q236-ext-bb.dat".into(), "data/quests/bb/ep2/multi".into()).unwrap(); let enemies_not_in_tower = pw4.enemies.iter() .filter(|enemy| { enemy.is_some() }) .filter(|enemy| { enemy.unwrap().map_area != MapArea::Tower }); assert!(enemies_not_in_tower.count() == 0); } }