diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs index a6c3dc1..69c9cea 100644 --- a/src/ship/map/area.rs +++ b/src/ship/map/area.rs @@ -1,4 +1,5 @@ // TOOD: `pub(super) for most of these?` +use std::collections::HashMap; use thiserror::Error; use crate::ship::room::Episode; @@ -57,53 +58,55 @@ pub enum MapAreaError { } impl MapArea { - pub fn from_value(episode: &Episode, area: u16) -> Result { - match (episode, area) { - (Episode::One, 0) => Ok(MapArea::Pioneer2Ep1), - (Episode::One, 1) => Ok(MapArea::Forest1), - (Episode::One, 2) => Ok(MapArea::Forest2), - (Episode::One, 3) => Ok(MapArea::Caves1), - (Episode::One, 4) => Ok(MapArea::Caves2), - (Episode::One, 5) => Ok(MapArea::Caves3), - (Episode::One, 6) => Ok(MapArea::Mines1), - (Episode::One, 7) => Ok(MapArea::Mines2), - (Episode::One, 8) => Ok(MapArea::Ruins1), - (Episode::One, 9) => Ok(MapArea::Ruins2), - (Episode::One, 10) => Ok(MapArea::Ruins3), - (Episode::One, 11) => Ok(MapArea::Dragon), - (Episode::One, 12) => Ok(MapArea::DeRolLe), - (Episode::One, 13) => Ok(MapArea::VolOpt), - (Episode::One, 14) => Ok(MapArea::DarkFalz), - (Episode::Two, 0) => Ok(MapArea::Pioneer2Ep2), - (Episode::Two, 1) => Ok(MapArea::VrTempleAlpha), - (Episode::Two, 2) => Ok(MapArea::VrTempleBeta), - (Episode::Two, 3) => Ok(MapArea::VrSpaceshipAlpha), - (Episode::Two, 4) => Ok(MapArea::VrSpaceshipBeta), - (Episode::Two, 5) => Ok(MapArea::Cca), - (Episode::Two, 6) => Ok(MapArea::JungleAreaNorth), - (Episode::Two, 7) => Ok(MapArea::JungleAreaEast), - (Episode::Two, 8) => Ok(MapArea::Mountain), - (Episode::Two, 9) => Ok(MapArea::Seaside), - (Episode::Two, 10) => Ok(MapArea::SeabedUpper), - (Episode::Two, 11) => Ok(MapArea::SeabedLower), - (Episode::Two, 12) => Ok(MapArea::GalGryphon), - (Episode::Two, 13) => Ok(MapArea::OlgaFlow), - (Episode::Two, 14) => Ok(MapArea::BarbaRay), - (Episode::Two, 15) => Ok(MapArea::GolDragon), - (Episode::Two, 16) => Ok(MapArea::SeasideNight), - (Episode::Two, 17) => Ok(MapArea::Tower), - (Episode::Four, 0) => Ok(MapArea::Pioneer2Ep4), - (Episode::Four, 1) => Ok(MapArea::CraterEast), - (Episode::Four, 2) => Ok(MapArea::CraterWest), - (Episode::Four, 3) => Ok(MapArea::CraterSouth), - (Episode::Four, 4) => Ok(MapArea::CraterNorth), - (Episode::Four, 5) => Ok(MapArea::CraterInterior), - (Episode::Four, 6) => Ok(MapArea::SubDesert1), - (Episode::Four, 7) => Ok(MapArea::SubDesert2), - (Episode::Four, 8) => Ok(MapArea::SubDesert3), - (Episode::Four, 9) => Ok(MapArea::SaintMillion), - // (Episode::Four, 10) => Ok(MapArea::TestMapEp4), - _ => Err(MapAreaError::UnknownMapArea(area)) + pub fn from_bb_map_designate(map_number: u8) -> Option { + match map_number { + 0 => Some(MapArea::Pioneer2Ep1), + 1 => Some(MapArea::Forest1), + 2 => Some(MapArea::Forest2), + 3 => Some(MapArea::Caves1), + 4 => Some(MapArea::Caves2), + 5 => Some(MapArea::Caves3), + 6 => Some(MapArea::Mines1), + 7 => Some(MapArea::Mines2), + 8 => Some(MapArea::Ruins1), + 9 => Some(MapArea::Ruins2), + 10 => Some(MapArea::Ruins3), + 11 => Some(MapArea::Dragon), + 12 => Some(MapArea::DeRolLe), + 13 => Some(MapArea::VolOpt), + 14 => Some(MapArea::DarkFalz), + + 18 => Some(MapArea::Pioneer2Ep2), + 19 => Some(MapArea::VrTempleAlpha), + 20 => Some(MapArea::VrTempleBeta), + 21 => Some(MapArea::VrSpaceshipAlpha), + 22 => Some(MapArea::VrSpaceshipBeta), + 23 => Some(MapArea::Cca), + 24 => Some(MapArea::JungleAreaNorth), + 25 => Some(MapArea::JungleAreaEast), + 26 => Some(MapArea::Mountain), + 27 => Some(MapArea::Seaside), + 28 => Some(MapArea::SeabedUpper), + 29 => Some(MapArea::SeabedLower), + 30 => Some(MapArea::GalGryphon), + 31 => Some(MapArea::OlgaFlow), + 32 => Some(MapArea::BarbaRay), + 33 => Some(MapArea::GolDragon), + 34 => Some(MapArea::SeasideNight), + 35 => Some(MapArea::Tower), + + 45 => Some(MapArea::Pioneer2Ep4), + 36 => Some(MapArea::CraterEast), + 37 => Some(MapArea::CraterWest), + 38 => Some(MapArea::CraterSouth), + 39 => Some(MapArea::CraterNorth), + 40 => Some(MapArea::CraterInterior), + 41 => Some(MapArea::SubDesert1), + 42 => Some(MapArea::SubDesert2), + 43 => Some(MapArea::SubDesert3), + 44 => Some(MapArea::SaintMillion), + + _=> None } } @@ -139,7 +142,7 @@ impl MapArea { MapArea::OlgaFlow => Some(9), MapArea::BarbaRay => Some(2), MapArea::GolDragon => Some(5), - MapArea::SeasideNight => Some(7), + MapArea::SeasideNight => Some(7), // TODO: this could also be 9? needs research MapArea::Tower => Some(9), MapArea::CraterEast => Some(2), @@ -205,3 +208,102 @@ impl MapArea { } } } + + +// TODO: rename this to something less silly +#[derive(Debug, Clone)] +pub struct MapAreaMapper(HashMap); + +impl MapAreaMapper { + pub fn get_area_map(&self, map_area: u16) -> Result<&MapArea, MapAreaError> { + self.0.get(&map_area).ok_or(MapAreaError::UnknownMapArea(map_area)) + } + + fn default_ep1_maps() -> MapAreaMapper { + let mut ep1 = HashMap::new(); + ep1.insert(0, MapArea::Pioneer2Ep1); + ep1.insert(1, MapArea::Forest1); + ep1.insert(2, MapArea::Forest2); + ep1.insert(3, MapArea::Caves1); + ep1.insert(4, MapArea::Caves2); + ep1.insert(5, MapArea::Caves3); + ep1.insert(6, MapArea::Mines1); + ep1.insert(7, MapArea::Mines2); + ep1.insert(8, MapArea::Ruins1); + ep1.insert(9, MapArea::Ruins2); + ep1.insert(10, MapArea::Ruins3); + ep1.insert(11, MapArea::Dragon); + ep1.insert(12, MapArea::DeRolLe); + ep1.insert(13, MapArea::VolOpt); + ep1.insert(14, MapArea::DarkFalz); + MapAreaMapper(ep1) + } + + fn default_ep2_maps() -> MapAreaMapper { + let mut ep2 = HashMap::new(); + ep2.insert(0, MapArea::Pioneer2Ep2); + ep2.insert(1, MapArea::VrTempleAlpha); + ep2.insert(2, MapArea::VrTempleBeta); + ep2.insert(3, MapArea::VrSpaceshipAlpha); + ep2.insert(4, MapArea::VrSpaceshipBeta); + ep2.insert(5, MapArea::Cca); + ep2.insert(6, MapArea::JungleAreaNorth); + ep2.insert(7, MapArea::JungleAreaEast); + ep2.insert(8, MapArea::Mountain); + ep2.insert(9, MapArea::Seaside); + ep2.insert(10, MapArea::SeabedUpper); + ep2.insert(11, MapArea::SeabedLower); + ep2.insert(12, MapArea::GalGryphon); + ep2.insert(13, MapArea::OlgaFlow); + ep2.insert(14, MapArea::BarbaRay); + ep2.insert(15, MapArea::GolDragon); + ep2.insert(16, MapArea::SeasideNight); + ep2.insert(17, MapArea::Tower); + MapAreaMapper(ep2) + } + + fn default_ep4_maps() -> MapAreaMapper { + let mut ep4 = HashMap::new(); + ep4.insert(0, MapArea::Pioneer2Ep4); + ep4.insert(1, MapArea::CraterEast); + ep4.insert(2, MapArea::CraterWest); + ep4.insert(3, MapArea::CraterSouth); + ep4.insert(4, MapArea::CraterNorth); + ep4.insert(5, MapArea::CraterInterior); + ep4.insert(6, MapArea::SubDesert1); + ep4.insert(7, MapArea::SubDesert2); + ep4.insert(8, MapArea::SubDesert3); + ep4.insert(9, MapArea::SaintMillion); + MapAreaMapper(ep4) + } + + pub fn new(episode: &Episode) -> MapAreaMapper { + match episode { + Episode::One => MapAreaMapper::default_ep1_maps(), + Episode::Two => MapAreaMapper::default_ep2_maps(), + Episode::Four => MapAreaMapper::default_ep4_maps(), + } + } +} + + +pub struct MapAreaMapperBuilder { + map_areas: HashMap, +} + +impl MapAreaMapperBuilder { + pub fn new() -> MapAreaMapperBuilder { + MapAreaMapperBuilder { + map_areas: HashMap::new() + } + } + + pub fn add(mut self, value: u16, map_area: MapArea) -> MapAreaMapperBuilder { + self.map_areas.insert(value, map_area); + self + } + + pub fn build(self) -> MapAreaMapper { + MapAreaMapper(self.map_areas) + } +} diff --git a/src/ship/map/mod.rs b/src/ship/map/mod.rs index 416ad0c..f57c80c 100644 --- a/src/ship/map/mod.rs +++ b/src/ship/map/mod.rs @@ -1,4 +1,4 @@ -mod area; +pub mod area; mod enemy; mod object; mod variant; diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index edcc249..ae054c2 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -82,8 +82,8 @@ where .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? .as_mut() .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; - let area = MapArea::from_value(&room.mode.episode(), player_drop_item.map_area)?; - item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, ClientItemId(player_drop_item.item_id), (area, player_drop_item.x, player_drop_item.y, player_drop_item.z)).await?; + let area = room.map_areas.get_area_map(player_drop_item.map_area)?; + item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, ClientItemId(player_drop_item.item_id), (*area, player_drop_item.x, player_drop_item.y, player_drop_item.z)).await?; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; let pdi = player_drop_item.clone(); Ok(Box::new(clients_in_area.into_iter() @@ -107,7 +107,7 @@ pub fn drop_coordinates(id: ClientId, .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; client.item_drop_location = Some(ItemDropLocation { - map_area: MapArea::from_value(&room.mode.episode(), drop_coordinates.map_area)?, + map_area: *room.map_areas.get_area_map(drop_coordinates.map_area)?, x: drop_coordinates.x, z: drop_coordinates.z, item_id: ClientItemId(drop_coordinates.item_id), diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs index 320a8bc..785442a 100644 --- a/src/ship/packet/handler/quest.rs +++ b/src/ship/packet/handler/quest.rs @@ -84,6 +84,7 @@ pub fn load_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quests: &Ques .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?.as_mut() .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; room.maps.set_quest_data(quest.enemies.clone(), quest.objects.clone()); + room.map_areas = quest.map_areas.clone(); let bin = quest::quest_header(questmenuselect, &quest.bin_blob, "bin"); let dat = quest::quest_header(questmenuselect, &quest.dat_blob, "dat"); diff --git a/src/ship/quests.rs b/src/ship/quests.rs index 74072e0..b5ee452 100644 --- a/src/ship/quests.rs +++ b/src/ship/quests.rs @@ -11,6 +11,7 @@ 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; +use crate::ship::map::area::{MapAreaMapper, MapAreaMapperBuilder}; #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -46,6 +47,7 @@ pub enum ParseDatError { MapError(#[from] MapAreaError), UnknownDatHeader(u32), CouldNotDetermineEpisode, + InvalidMapAreaId(u16), } const DAT_OBJECT_HEADER_ID: u32 = 1; @@ -59,14 +61,14 @@ enum DatBlock { } -fn read_dat_section_header(cursor: &mut T, episode: &Episode) -> Result { +fn read_dat_section_header(cursor: &mut T, episode: &Episode, map_areas: &MapAreaMapper) -> 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 = MapArea::from_value(episode, area)?; + let map_area = map_areas.get_area_map(area).map_err(|_| ParseDatError::InvalidMapAreaId(area))?; match header { DAT_OBJECT_HEADER_ID => { @@ -83,7 +85,7 @@ fn read_dat_section_header(cursor: &mut T, episode: &Episode) -> 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 => { @@ -96,6 +98,7 @@ fn read_dat_section_header(cursor: &mut T, episode: &Episode) -> fn quest_episode(bin: &[u8]) -> Option { for bytes in bin.windows(3) { + // set_episode if bytes[0] == 0xF8 && bytes[1] == 0xBC { return Some(Episode::from_quest(bytes[2]).ok()?) } @@ -103,11 +106,26 @@ fn quest_episode(bin: &[u8]) -> Option { None } -fn parse_dat(dat: &[u8], episode: &Episode) -> Result<(Vec>, Vec>), ParseDatError> { +fn map_area_mappings(bin: &[u8]) -> MapAreaMapper { + let mut map_areas = MapAreaMapperBuilder::new(); + 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() +} + +fn parse_dat(dat: &[u8], episode: &Episode, map_areas: &MapAreaMapper) -> 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) { + match read_dat_section_header(&mut cursor, episode, map_areas) { Ok(dat_block) => Some(dat_block), Err(err) => { warn!("unknown header in dat: {:?}", err); @@ -126,7 +144,7 @@ fn parse_dat(dat: &[u8], episode: &Episode) -> Result<(Vec>, Ve }, _ => {} } - + (enemies, objects) })) } @@ -151,6 +169,7 @@ pub struct Quest { pub dat_blob: Vec, pub enemies: Vec>, pub objects: Vec>, + pub map_areas: MapAreaMapper, } impl Quest { @@ -162,7 +181,8 @@ impl Quest { let full_description = array_to_utf16(&bin[334..920]); let episode = quest_episode(&bin).ok_or(ParseDatError::CouldNotDetermineEpisode)?; - let (enemies, objects) = parse_dat(&dat, &episode)?; + 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(&bin)?; @@ -179,6 +199,7 @@ impl Quest { dat_blob: prs_dat.into_inner().map_err(|_| QuestLoadError::CouldNotReadMetadata)?, enemies: enemies, objects: objects, + map_areas: map_areas, }) } } @@ -186,6 +207,31 @@ impl Quest { // QuestCollection pub type QuestList = BTreeMap>; +pub fn load_quest(bin_path: PathBuf, dat_path: PathBuf) -> Option { + let dat_file = File::open(PathBuf::from("data/quests/").join(dat_path.clone())) + .map_err(|err| { + warn!("could not load quest file {:?}: {:?}", dat_path, err) + }).ok()?; + //let bin_file = File::open(format!("data/quests/{}", bin_path)) + let bin_file = File::open(PathBuf::from("data/quests/").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(quest_path: PathBuf) -> Result { let mut f = File::open(quest_path).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?; let mut s = String::new(); @@ -198,31 +244,15 @@ pub fn load_quests(quest_path: PathBuf) -> Result { let quests = category_details.quests .into_iter() .filter_map(|quest| { - let dat_file = File::open(format!("data/quests/{}", quest.dat)) - .map_err(|err| { - warn!("could not load quest file {}: {:?}", quest.dat, err) - }).ok()?; - let bin_file = File::open(format!("data/quests/{}", quest.bin)) - .map_err(|err| { - warn!("could not load quest file {}: {:?}", quest.bin, 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 {}/{}: {:?}", quest.bin, quest.dat, err) - }).ok()?; - if used_quest_ids.contains(&quest.id) { - warn!("quest id already exists: {}", quest.id); - return None; - } - used_quest_ids.insert(quest.id); - Some(quest) + load_quest(quest.bin.into(), quest.dat.into()) + .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) + }) }); (QuestCategory{ index: category_details.list_order, @@ -231,3 +261,31 @@ pub fn load_quests(quest_path: PathBuf) -> Result { }, quests.collect()) }).collect()) } + + + + + + + + +#[cfg(test)] +mod test { + 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()).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); + + } +} diff --git a/src/ship/room.rs b/src/ship/room.rs index dadcae7..bf8c594 100644 --- a/src/ship/room.rs +++ b/src/ship/room.rs @@ -6,6 +6,7 @@ use crate::ship::map::Maps; use crate::ship::drops::DropTable; use crate::entity::character::SectionID; use crate::ship::monster::{load_monster_stats_table, MonsterType, MonsterStats}; +use crate::ship::map::area::MapAreaMapper; #[derive(Debug)] pub enum RoomCreationError { @@ -164,6 +165,7 @@ pub struct RoomState { pub random_seed: u32, pub bursting: bool, pub monster_stats: Box>, + pub map_areas: MapAreaMapper, // items on ground // enemy info } @@ -238,6 +240,7 @@ impl RoomState { section_id: section_id, drop_table: Box::new(DropTable::new(room_mode.episode(), room_mode.difficulty(), section_id)), bursting: false, + map_areas: MapAreaMapper::new(&room_mode.episode()), }) } }