diff --git a/Cargo.toml b/Cargo.toml index ae1f141..0f86474 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,5 @@ byteorder = "1" enum-utils = "0.1.2" derive_more = { version = "0.99.3", features = ["display"]} thiserror = "1.0.15" +ages-prs = "0.1" diff --git a/src/ship/map.rs b/src/ship/map.rs index 4953833..34bc1fd 100644 --- a/src/ship/map.rs +++ b/src/ship/map.rs @@ -524,112 +524,123 @@ impl MapVariant { } -fn objects_from_map_data(path: PathBuf, episode: &Episode, map_variant: &MapVariant) -> Vec> { - let mut cursor = File::open(path.clone()).unwrap(); +pub fn objects_from_stream(cursor: &mut impl Read, episode: &Episode, map_area: &MapArea) -> Vec> { let mut object_data = Vec::new(); - while let Ok(raw_object) = RawMapObject::from_byte_stream(&mut cursor) { - let object = MapObject::from_raw(raw_object.clone(), *episode, &map_variant.map); + while let Ok(raw_object) = RawMapObject::from_byte_stream(cursor) { + let object = MapObject::from_raw(raw_object.clone(), *episode, map_area); object_data.push(object.ok()); - } object_data } +fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) -> Vec> { + let mut cursor = File::open(path.clone()).unwrap(); + objects_from_stream(&mut cursor, episode, map_area) +} -fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode) -> Vec> { - let path = map_variant.dat_file(); - let mut cursor = File::open(path).unwrap(); +fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec> { + let enemy = MapEnemy::from_raw(raw_enemy, episode, map_area); + enemy + .map_or(vec![None], |monster| { + let mut monsters = Vec::new(); + monsters.push(Some(monster)); + + match monster.monster { + MonsterType::Monest => { + for _ in 0..30 { + monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area))); + } + }, + MonsterType::PofuillySlime => { + for _ in 0..4 { + monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))); + } + }, + MonsterType::PanArms => { + monsters.push(Some(MapEnemy::new(MonsterType::Hidoom, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::Migium, monster.map_area))); + }, + MonsterType::SinowBeat => { + for _ in 0..4 { + monsters.push(Some(MapEnemy::new(MonsterType::SinowBeat, monster.map_area))); + } + }, + MonsterType::SinowGold => { + for _ in 0..4 { + monsters.push(Some(MapEnemy::new(MonsterType::SinowGold, monster.map_area))); + } + }, + MonsterType::Canane => { + for _ in 0..8 { + monsters.push(Some(MapEnemy::new(MonsterType::RingCanadine, monster.map_area))); + } + }, + MonsterType::ChaosSorcerer => { + monsters.push(Some(MapEnemy::new(MonsterType::BeeR, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::BeeL, monster.map_area))); + }, + MonsterType::Bulclaw => { + for _ in 0..4 { + monsters.push(Some(MapEnemy::new(MonsterType::Claw, monster.map_area))); + } + }, + MonsterType::DeRolLe => { + for _ in 0..10 { + monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeBody, monster.map_area))); + } + for _ in 0..9 { + monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeMine, monster.map_area))); + } + }, + MonsterType::VolOptPartA => { + for _ in 0..6 { + monsters.push(Some(MapEnemy::new(MonsterType::VolOptPillar, monster.map_area))); + } + for _ in 0..24 { + monsters.push(Some(MapEnemy::new(MonsterType::VolOptMonitor, monster.map_area))); + } + for _ in 0..2 { + monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area))); + } + monsters.push(Some(MapEnemy::new(MonsterType::VolOptAmp, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::VolOptCore, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area))); + }, + // TOOD: this cares about difficulty (theres an ult-specific darvant?) + MonsterType::DarkFalz => { + for _ in 0..509 { + monsters.push(Some(MapEnemy::new(MonsterType::Darvant, monster.map_area))); + } + monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz3, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz2, monster.map_area))); + monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz1, monster.map_area))); + }, + _ => { + for _ in 0..raw_enemy.children { + monsters.push(Some(MapEnemy::new(monster.monster, monster.map_area))); + } + } + } + monsters + }) +} + + +pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode) -> Vec> { let mut enemy_data = Vec::new(); - while let Ok(enemy) = RawMapEnemy::from_byte_stream(&mut cursor) { - let new_enemy = MapEnemy::from_raw(enemy, episode, &map_variant.map); - enemy_data.append(&mut new_enemy - .map_or(vec![None], |monster| { - let mut monsters = Vec::new(); - monsters.push(Some(monster)); - - match monster.monster { - MonsterType::Monest => { - for _ in 0..30 { - monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area))); - } - }, - MonsterType::PofuillySlime => { - for _ in 0..4 { - monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area))); - } - }, - MonsterType::PanArms => { - monsters.push(Some(MapEnemy::new(MonsterType::Hidoom, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::Migium, monster.map_area))); - }, - MonsterType::SinowBeat => { - for _ in 0..4 { - monsters.push(Some(MapEnemy::new(MonsterType::SinowBeat, monster.map_area))); - } - }, - MonsterType::SinowGold => { - for _ in 0..4 { - monsters.push(Some(MapEnemy::new(MonsterType::SinowGold, monster.map_area))); - } - }, - MonsterType::Canane => { - for _ in 0..8 { - monsters.push(Some(MapEnemy::new(MonsterType::RingCanadine, monster.map_area))); - } - }, - MonsterType::ChaosSorcerer => { - monsters.push(Some(MapEnemy::new(MonsterType::BeeR, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::BeeL, monster.map_area))); - }, - MonsterType::Bulclaw => { - for _ in 0..4 { - monsters.push(Some(MapEnemy::new(MonsterType::Claw, monster.map_area))); - } - }, - MonsterType::DeRolLe => { - for _ in 0..10 { - monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeBody, monster.map_area))); - } - for _ in 0..9 { - monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeMine, monster.map_area))); - } - }, - MonsterType::VolOptPartA => { - for _ in 0..6 { - monsters.push(Some(MapEnemy::new(MonsterType::VolOptPillar, monster.map_area))); - } - for _ in 0..24 { - monsters.push(Some(MapEnemy::new(MonsterType::VolOptMonitor, monster.map_area))); - } - for _ in 0..2 { - monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area))); - } - monsters.push(Some(MapEnemy::new(MonsterType::VolOptAmp, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::VolOptCore, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area))); - }, - // TOOD: this cares about difficulty (theres an ult-specific darvant?) - MonsterType::DarkFalz => { - for _ in 0..509 { - monsters.push(Some(MapEnemy::new(MonsterType::Darvant, monster.map_area))); - } - monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz3, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz2, monster.map_area))); - monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz1, monster.map_area))); - }, - _ => { - for _ in 0..enemy.children { - monsters.push(Some(MapEnemy::new(monster.monster, monster.map_area))); - } - } - } - monsters - })); + while let Ok(enemy) = RawMapEnemy::from_byte_stream(cursor) { + enemy_data.append(&mut parse_enemy(episode, map_area, enemy)); } enemy_data } +fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode) -> Vec> { + let path = map_variant.dat_file(); + let mut cursor = File::open(path).unwrap(); + enemy_data_from_stream(&mut cursor, &map_variant.map, episode) +} + #[derive(Error, Debug)] #[error("")] @@ -675,7 +686,7 @@ impl Maps { enemy_data }), object_data: map_variants.iter().map(|map_variant| { - objects_from_map_data(map_variant.obj_file().into(), &episode, &map_variant) + objects_from_map_data(map_variant.obj_file().into(), &episode, &map_variant.map) }).flatten().collect(), map_variants: map_variants, }; @@ -702,6 +713,11 @@ impl Maps { header }) } + + pub fn set_quest_data(&mut self, enemies: Vec>, objects: Vec>) { + self.enemy_data = enemies; + self.object_data = objects; + } } diff --git a/src/ship/mod.rs b/src/ship/mod.rs index c0b74b2..4a9aa5a 100644 --- a/src/ship/mod.rs +++ b/src/ship/mod.rs @@ -8,3 +8,4 @@ pub mod map; pub mod monster; pub mod drops; pub mod packet; +pub mod quests; diff --git a/src/ship/packet/builder/mod.rs b/src/ship/packet/builder/mod.rs index 0bcfc3c..2ce4837 100644 --- a/src/ship/packet/builder/mod.rs +++ b/src/ship/packet/builder/mod.rs @@ -1,6 +1,7 @@ pub mod lobby; pub mod message; pub mod room; +pub mod quest; use libpso::character::character::Inventory; use libpso::packet::ship::{PlayerHeader, PlayerInfo}; diff --git a/src/ship/packet/builder/quest.rs b/src/ship/packet/builder/quest.rs new file mode 100644 index 0000000..9a33696 --- /dev/null +++ b/src/ship/packet/builder/quest.rs @@ -0,0 +1,47 @@ +use crate::ship::quests::{Quest, QuestList}; +use crate::ship::ship::{QUEST_CATEGORY_MENU_ID, QUEST_SELECT_MENU_ID}; +use libpso::packet::ship::*; +use libpso::utf8_to_utf16_array; + + +pub fn quest_category_list(quests: &QuestList) -> QuestCategoryList { + let categories = quests.iter() + .enumerate() + .map(|(i, (category, _))| { + QuestCategory { + menu_id: QUEST_CATEGORY_MENU_ID, + option_id: i as u32, + name: utf8_to_utf16_array!(category.name, 32), + description: utf8_to_utf16_array!(category.description, 122), + } + }) + .collect(); + + QuestCategoryList { + quest_categories: categories, + } +} + +pub fn quest_list(category_id: u32, quests: &Vec) -> QuestOptionList { + let quest_entries = quests.iter() + .map(|quest| { + QuestEntry { + menu_id: QUEST_SELECT_MENU_ID, + category_id: category_id as u16, + quest_id: quest.id, + name: utf8_to_utf16_array!(quest.name, 32), + description: utf8_to_utf16_array!(quest.description, 122), + } + }) + .collect(); + + QuestOptionList { + quests: quest_entries, + } +} + +pub fn quest_detail(quest: &Quest) -> QuestDetail { + QuestDetail { + description: utf8_to_utf16_array!(quest.full_description, 288), + } +} diff --git a/src/ship/packet/handler/mod.rs b/src/ship/packet/handler/mod.rs index 2021066..192c8ab 100644 --- a/src/ship/packet/handler/mod.rs +++ b/src/ship/packet/handler/mod.rs @@ -5,3 +5,4 @@ pub mod lobby; pub mod message; pub mod room; pub mod settings; +pub mod quest; diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs new file mode 100644 index 0000000..ca5cdf9 --- /dev/null +++ b/src/ship/packet/handler/quest.rs @@ -0,0 +1,209 @@ +use std::collections::HashMap; +use std::convert::TryInto; +use std::io::{Cursor, Read, Seek, SeekFrom}; +use libpso::packet::ship::*; +use crate::common::serverstate::ClientId; +use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms}; +use crate::ship::quests::QuestList; +use crate::ship::location::{ClientLocation, ClientLocationError}; +use crate::ship::packet::builder::quest; +use libpso::utf8_to_array; +use libpso::util::array_to_utf8; + +// TOOD: enum +const DATATYPE_BIN: u16 = 1; +const DATATYPE_DAT: u16 = 2; + + +fn parse_filename(filename_bytes: &[u8; 16]) -> Result<(u16, u16, u16), ShipError> { + let filename = array_to_utf8(*filename_bytes).map_err(|_| ShipError::InvalidQuestFilename("NOT UTF8".to_string()))?; + let (filename, suffix) = { + let mut s = filename.splitn(2, '.'); + (s.next().ok_or(ShipError::InvalidQuestFilename(filename.to_owned()))?, + s.next().ok_or(ShipError::InvalidQuestFilename(filename.to_owned()))?) + }; + + let datatype = match suffix { + "bin" => DATATYPE_BIN, + "dat" => DATATYPE_DAT, + _ => return Err(ShipError::InvalidQuestFilename(filename.to_owned())) + }; + + let (category, quest) = { + let mut s = filename.splitn(2, '-'); + (s.next().and_then(|k| k.parse().ok()).ok_or(ShipError::InvalidQuestFilename(filename.to_owned()))?, + s.next().and_then(|k| k.parse().ok()).ok_or(ShipError::InvalidQuestFilename(filename.to_owned()))?) + }; + + Ok((category, quest, datatype)) +} + + +pub fn send_quest_category_list(id: ClientId, quests: &QuestList) -> Result + Send>, ShipError> { + let qcl = quest::quest_category_list(quests); + Ok(Box::new(vec![(id, SendShipPacket::QuestCategoryList(qcl))].into_iter())) +} + +pub fn select_quest_category(id: ClientId, menuselect: &MenuSelect, quests: &QuestList) -> Result + Send>, ShipError> { + let (_, category_quests) = quests.iter() + .nth(menuselect.item as usize) + .ok_or(ShipError::InvalidQuestCategory(menuselect.item))?; + + let ql = quest::quest_list(menuselect.item, category_quests); + + Ok(Box::new(vec![(id, SendShipPacket::QuestOptionList(ql))].into_iter())) +} + + +pub fn quest_detail(id: ClientId, questdetailrequest: &QuestDetailRequest, quests: &QuestList) -> Result + Send>, ShipError> { + let (_, category_quests) = quests.iter() + .nth(questdetailrequest.category as usize) + .ok_or(ShipError::InvalidQuestCategory(questdetailrequest.category as u32))?; + + let quest = category_quests.iter() + .find(|q| { + q.id == questdetailrequest.quest as u16 + }).ok_or(ShipError::InvalidQuest(questdetailrequest.quest as u32))?; + + let qd = quest::quest_detail(&quest); + + Ok(Box::new(vec![(id, SendShipPacket::QuestDetail(qd))].into_iter())) +} + +pub fn load_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quests: &QuestList, clients: &mut Clients, client_location: &ClientLocation, rooms: &mut Rooms) + -> Result + Send>, ShipError> { + let (_, category_quests) = quests.iter() + .nth(questmenuselect.category as usize) + .ok_or(ShipError::InvalidQuestCategory(questmenuselect.category as u32))?; + + let quest = category_quests.iter() + .find(|q| { + q.id == questmenuselect.quest as u16 + }).ok_or(ShipError::InvalidQuest(questmenuselect.quest as u32))?; + + let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; + let room = rooms.get_mut(room_id.0) + .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()); + + let bin_path = format!("{}-{}.bin", questmenuselect.category, questmenuselect.quest); + let bin = QuestHeader { + unknown1: [0; 0x24], + filename: utf8_to_array!(bin_path, 16), + length: quest.bin_blob.len() as u32, + name: utf8_to_array!("quest.bin", 16), + unknown2: [0; 8], + }; + + let dat_path = format!("{}-{}.dat", questmenuselect.category, questmenuselect.quest); + let dat = QuestHeader { + unknown1: [0; 0x24], + filename: utf8_to_array!(dat_path, 16), + length: quest.dat_blob.len() as u32, + name: utf8_to_array!("quest.dat", 16), + unknown2: [0; 8], + }; + + let area_clients = client_location.get_all_clients_by_client(id).map_err(|err| -> ClientLocationError { err.into() })?; + area_clients.iter().for_each(|c| { + clients.get_mut(&c.client).map(|client| { + client.done_loading_quest = false; + }); + }); + Ok(Box::new(area_clients.into_iter().map(move |c| { + vec![(c.client, SendShipPacket::QuestHeader(bin.clone())), (c.client, SendShipPacket::QuestHeader(dat.clone()))] + }).flatten())) + + //Ok(Box::new(vec![(id, SendShipPacket::QuestHeader(bin)), (id, SendShipPacket::QuestHeader(dat))].into_iter())) +} + +pub fn quest_file_request(id: ClientId, quest_file_request: &QuestFileRequest, quests: &QuestList) -> Result + Send>, ShipError> { + let (category_id, quest_id, datatype) = parse_filename(&quest_file_request.filename)?; + let (_, category_quests) = quests.iter() + .nth(category_id as usize) + .ok_or(ShipError::InvalidQuestCategory(category_id as u32))?; + + let quest = category_quests.iter() + .find(|q| { + q.id == quest_id as u16 + }).ok_or(ShipError::InvalidQuest(quest_id as u32))?; + + // quest.Bin quest.Dat + let blob = match datatype { + DATATYPE_BIN => &quest.bin_blob, + DATATYPE_DAT => &quest.dat_blob, + _ => panic!() + }; + let mut blob_cursor = Cursor::new(blob); + + let mut subblob = [0u8; 0x400]; + let blob_length = blob_cursor.read(&mut subblob)?; + let qc = QuestChunk { + chunk_num: 0, + filename: quest_file_request.filename, + blob: subblob, + blob_length: blob_length as u32, + unknown: 0, + }; + + Ok(Box::new(vec![(id, SendShipPacket::QuestChunk(qc))].into_iter())) +} + +pub fn quest_chunk_ack(id: ClientId, quest_chunk_ack: &QuestChunkAck, quests: &QuestList) -> Result + Send>, ShipError> { + let (category_id, quest_id, datatype) = parse_filename(&quest_chunk_ack.filename)?; + let (_, category_quests) = quests.iter() + .nth(category_id as usize) + .ok_or(ShipError::InvalidQuestCategory(category_id as u32))?; + + let quest = category_quests.iter() + .find(|q| { + q.id == quest_id + }).ok_or(ShipError::InvalidQuest(quest_id as u32))?; + + let blob = match datatype { + DATATYPE_BIN => &quest.bin_blob, + DATATYPE_DAT => &quest.dat_blob, + _ => panic!() + }; + + let mut blob_cursor = Cursor::new(blob); + blob_cursor.seek(SeekFrom::Start((quest_chunk_ack.chunk_num as u64 + 1) * 0x400)); + let mut subblob = [0u8; 0x400]; + let blob_length = blob_cursor.read(&mut subblob)?; + if blob_length == 0 { + return Ok(Box::new(None.into_iter())); + } + let qc = QuestChunk { + chunk_num: quest_chunk_ack.chunk_num + 1, + filename: quest_chunk_ack.filename, + blob: subblob, + blob_length: blob_length as u32, + unknown: 0, + }; + + Ok(Box::new(vec![(id, SendShipPacket::QuestChunk(qc))].into_iter())) +} + +pub fn done_loading_quest(id: ClientId, quests: &QuestList, clients: &mut Clients, client_location: &ClientLocation) + -> Result + Send>, ShipError> { + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + client.done_loading_quest = true; + let area_clients = client_location.get_all_clients_by_client(id).map_err(|err| -> ClientLocationError { err.into() })?; + let all_loaded = area_clients.iter().all(|c| { + clients.get(&c.client) + .map(|client| { + client.done_loading_quest + }) + .unwrap_or(false) + }); + + if all_loaded { + Ok(Box::new(area_clients.into_iter().map(|c| { + (c.client, SendShipPacket::DoneLoadingQuest(DoneLoadingQuest {})) + }))) + } + else { + Ok(Box::new(None.into_iter())) + } +} diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs index 696dbb9..da26b35 100644 --- a/src/ship/packet/handler/room.rs +++ b/src/ship/packet/handler/room.rs @@ -1,4 +1,5 @@ use libpso::packet::ship::*; +use libpso::packet::messages::*; use crate::common::serverstate::ClientId; use crate::common::leveltable::CharacterLevelTable; use crate::ship::ship::{SendShipPacket, ShipError, Rooms, Clients}; @@ -110,10 +111,35 @@ pub fn done_bursting(id: ClientId, let room = rooms.get_mut(room_id.0).unwrap().as_mut().unwrap(); room.bursting = false; } + Box::new(client_location.get_client_neighbors(id).unwrap().into_iter() + .map(move |client| { + vec![ + //(client.client, SendShipPacket::BurstDone72(BurstDone72::new())), + (client.client, SendShipPacket::Message(Message::new(GameMessage::BurstDone(BurstDone { + client: 0, + target: 0 + })))), + ] + }).flatten()) +} + +pub fn done_bursting2(id: ClientId, + client_location: &ClientLocation, + rooms: &mut Rooms) + -> Box + Send> { + let area = client_location.get_area(id).unwrap(); + if let RoomLobby::Room(room_id) = area { + let room = rooms.get_mut(room_id.0).unwrap().as_mut().unwrap(); + room.bursting = false; + } Box::new(client_location.get_client_neighbors(id).unwrap().into_iter() .map(move |client| { vec![ (client.client, SendShipPacket::BurstDone72(BurstDone72::new())), + /*(client.client, SendShipPacket::Message(Message::new(GameMessage::BurstDone(BurstDone { + client: 1, + target: 0 + })))),*/ ] }).flatten()) } diff --git a/src/ship/quests.rs b/src/ship/quests.rs new file mode 100644 index 0000000..6d10735 --- /dev/null +++ b/src/ship/quests.rs @@ -0,0 +1,224 @@ +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, TryFrom}; +use thiserror::Error; +use serde::{Serialize, Deserialize}; +use ages_prs::{LegacyPrsDecoder, LegacyPrsEncoder}; +use byteorder::{LittleEndian, ReadBytesExt}; +use libpso::packet::ship::QuestChunk; +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; + + +#[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("")] +enum ParseDatError { + IoError(#[from] std::io::Error), + MapError(#[from] MapAreaError), + UnknownDatHeader(u32), + CouldNotDetermineEpisode, +} + +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) -> Result { + let header = cursor.read_u32::()?; + let offset = cursor.read_u32::()?; + let area = cursor.read_u32::()?; + let length = cursor.read_u32::()?; + + let map_area = MapArea::from_value(episode, area)?; + + match header { + DAT_OBJECT_HEADER_ID => { + let mut obj_data = vec![0u8; length as usize]; + cursor.read(&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(&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) { + if bytes[0] == 0xF8 && bytes[1] == 0xBC { + warn!("ep? {:?}", bytes[2]); + return Some(Episode::from_quest(bytes[2]).ok()?) + } + } + None +} + +fn parse_dat(dat: &[u8], episode: &Episode) -> 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) { + 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)] +#[error("")] +enum QuestLoadError { + ParseDatError(#[from] ParseDatError), +} + +#[derive(Debug)] +pub struct Quest { + pub name: String, + pub description: String, + pub full_description: String, + pub language: u16, + pub id: u16, + pub bin_blob: Vec, + pub dat_blob: Vec, + pub enemies: Vec>, + pub objects: Vec>, +} + +impl Quest { + fn from_bin_dat(bin: Vec, dat: Vec) -> Result { + let id = u16::from_le_bytes(bin[16..18].try_into().unwrap()); + let language = u16::from_le_bytes(bin[18..20].try_into().unwrap()); + 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 (enemies, objects) = parse_dat(&dat, &episode)?; + + let mut prs_bin = LegacyPrsEncoder::new(Vec::new()); + prs_bin.write(&bin); + let mut prs_dat = LegacyPrsEncoder::new(Vec::new()); + prs_dat.write(&dat); + + Ok(Quest { + name: name, + description: description, + full_description: full_description, + id: id, + language: language, + bin_blob: prs_bin.into_inner().unwrap(), + dat_blob: prs_dat.into_inner().unwrap(), + enemies: enemies, + objects: objects, + }) + } +} + +// QuestCollection +pub type QuestList = BTreeMap>; + +pub fn load_quests(quest_path: PathBuf) -> QuestList { + let mut f = File::open(quest_path).unwrap(); + let mut s = String::new(); + f.read_to_string(&mut s); + + let mut used_quest_ids = BTreeSet::new(); + let ql: BTreeMap = toml::from_str(s.as_str()).unwrap(); + + ql.into_iter().map(|(category, category_details)| { + let quests = category_details.quests + .into_iter() + .filter_map(|quest| { + warn!("{:?}", quest.bin); + let dat_file = File::open(format!("data/quests/{}", quest.dat)).unwrap(); + let bin_file = File::open(format!("data/quests/{}", quest.bin)).unwrap(); + 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).unwrap(); + bin_prs.read_to_end(&mut bin).unwrap(); + + let quest = Quest::from_bin_dat(bin, dat).unwrap(); + if used_quest_ids.contains(&quest.id) { + return None; + } + used_quest_ids.insert(quest.id); + Some(quest) + }); + (QuestCategory{ + index: category_details.list_order, + name: category, + description: category_details.description, + }, quests.collect()) + }).collect() + +} diff --git a/src/ship/room.rs b/src/ship/room.rs index 7f8864c..25d3fa3 100644 --- a/src/ship/room.rs +++ b/src/ship/room.rs @@ -46,6 +46,17 @@ impl Into for Episode { } } +impl Episode { + pub fn from_quest(value: u8) -> Result { + match value { + 0 => Ok(Episode::One), + 1 => Ok(Episode::Two), + 2 => Ok(Episode::Four), + _ => Err(RoomCreationError::InvalidEpisode(value)) + } + } +} + #[derive(Debug, Copy, Clone, derive_more::Display)] pub enum Difficulty { Normal, diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 1257fde..0d3ad6f 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -12,7 +12,6 @@ use libpso::crypto::bb::PSOBBCipher; use libpso::packet::ship::{BLOCK_MENU_ID, ROOM_MENU_ID}; - use crate::common::cipherkeys::{ELSEWHERE_PRIVATE_KEY, ELSEWHERE_PARRAY}; use crate::common::serverstate::{SendServerPacket, RecvServerPacket, ServerState, OnConnect, ClientId}; use crate::common::leveltable::CharacterLevelTable; @@ -25,10 +24,13 @@ use crate::ship::location::{ClientLocation, RoomLobby, MAX_ROOMS, ClientLocation use crate::ship::items; use crate::ship::room; +use crate::ship::quests; use crate::ship::map::{MapsError, MapAreaError, MapArea}; use crate::ship::packet::handler; pub const SHIP_PORT: u16 = 23423; +pub const QUEST_CATEGORY_MENU_ID: u32 = 0xA2; +pub const QUEST_SELECT_MENU_ID: u32 = 0xA3; pub type Rooms = [Option; MAX_ROOMS]; pub type Clients = HashMap; @@ -51,6 +53,10 @@ pub enum ShipError { ItemManagerError(#[from] items::ItemManagerError), ItemDropLocationNotSet, BoxAlreadyDroppedItem(ClientId, u16), + InvalidQuestCategory(u32), + InvalidQuest(u32), + InvalidQuestFilename(String), + IoError(#[from] std::io::Error), } #[derive(Debug)] @@ -71,18 +77,33 @@ pub enum RecvShipPacket { Like62ButCooler(Like62ButCooler), ClientCharacterData(ClientCharacterData), DoneBursting(DoneBursting), + DoneBursting2(DoneBursting2), LobbySelect(LobbySelect), + RequestQuestList(RequestQuestList), + MenuDetail(MenuDetail), + QuestDetailRequest(QuestDetailRequest), + QuestMenuSelect(QuestMenuSelect), + QuestFileRequest(QuestFileRequest), + QuestChunkAck(QuestChunkAck), + DoneLoadingQuest(DoneLoadingQuest), } impl RecvServerPacket for RecvShipPacket { fn from_bytes(data: &[u8]) -> Result { match u16::from_le_bytes([data[2], data[3]]) { 0x93 => Ok(RecvShipPacket::Login(Login::from_bytes(data)?)), - 0x10 => match data[0] { - 16 => Ok(RecvShipPacket::MenuSelect(MenuSelect::from_bytes(data)?)), - 48 => Ok(RecvShipPacket::RoomPasswordReq(RoomPasswordReq::from_bytes(data)?)), + 0x09 => match data[8] as u32 { + QUEST_SELECT_MENU_ID => Ok(RecvShipPacket::QuestDetailRequest(QuestDetailRequest::from_bytes(data)?)), + _ => Ok(RecvShipPacket::MenuDetail(MenuDetail::from_bytes(data)?)), + } + 0x10 => match (data[0], data[8] as u32) { + (16, QUEST_SELECT_MENU_ID) => Ok(RecvShipPacket::QuestMenuSelect(QuestMenuSelect::from_bytes(data)?)), + (16, _) => Ok(RecvShipPacket::MenuSelect(MenuSelect::from_bytes(data)?)), + (48, _) => Ok(RecvShipPacket::RoomPasswordReq(RoomPasswordReq::from_bytes(data)?)), _ => Err(PacketParseError::WrongPacketForServerType(u16::from_le_bytes([data[2], data[3]]), data.to_vec())), }, + 0x13 => Ok(RecvShipPacket::QuestChunkAck(QuestChunkAck::from_bytes(data)?)), + 0x44 => Ok(RecvShipPacket::QuestFileRequest(QuestFileRequest::from_bytes(data)?)), 0x61 => Ok(RecvShipPacket::CharData(CharData::from_bytes(data)?)), 0x60 => Ok(RecvShipPacket::Message(Message::from_bytes(data)?)), 0x62 => Ok(RecvShipPacket::DirectMessage(DirectMessage::from_bytes(data)?)), @@ -96,7 +117,10 @@ impl RecvServerPacket for RecvShipPacket { 0x6D => Ok(RecvShipPacket::Like62ButCooler(Like62ButCooler::from_bytes(data)?)), 0x98 => Ok(RecvShipPacket::ClientCharacterData(ClientCharacterData::from_bytes(data)?)), 0x6F => Ok(RecvShipPacket::DoneBursting(DoneBursting::from_bytes(data)?)), + 0x16F => Ok(RecvShipPacket::DoneBursting2(DoneBursting2::from_bytes(data)?)), 0x84 => Ok(RecvShipPacket::LobbySelect(LobbySelect::from_bytes(data)?)), + 0xA2 => Ok(RecvShipPacket::RequestQuestList(RequestQuestList::from_bytes(data)?)), + 0xAC => Ok(RecvShipPacket::DoneLoadingQuest(DoneLoadingQuest::from_bytes(data)?)), _ => Err(PacketParseError::WrongPacketForServerType(u16::from_le_bytes([data[2], data[3]]), data.to_vec())) } } @@ -125,7 +149,14 @@ pub enum SendShipPacket { Like62ButCooler(Like62ButCooler), BurstDone72(BurstDone72), DoneBursting(DoneBursting), + DoneBursting2(DoneBursting2), LobbyList(LobbyList), + QuestCategoryList(QuestCategoryList), + QuestOptionList(QuestOptionList), + QuestDetail(QuestDetail), + QuestHeader(QuestHeader), + QuestChunk(QuestChunk), + DoneLoadingQuest(DoneLoadingQuest), } impl SendServerPacket for SendShipPacket { @@ -152,7 +183,14 @@ impl SendServerPacket for SendShipPacket { SendShipPacket::Like62ButCooler(pkt) => pkt.as_bytes(), SendShipPacket::BurstDone72(pkt) => pkt.as_bytes(), SendShipPacket::DoneBursting(pkt) => pkt.as_bytes(), + SendShipPacket::DoneBursting2(pkt) => pkt.as_bytes(), SendShipPacket::LobbyList(pkt) => pkt.as_bytes(), + SendShipPacket::QuestCategoryList(pkt) => pkt.as_bytes(), + SendShipPacket::QuestOptionList(pkt) => pkt.as_bytes(), + SendShipPacket::QuestDetail(pkt) => pkt.as_bytes(), + SendShipPacket::QuestHeader(pkt) => pkt.as_bytes(), + SendShipPacket::QuestChunk(pkt) => pkt.as_bytes(), + SendShipPacket::DoneLoadingQuest(pkt) => pkt.as_bytes(), } } } @@ -165,6 +203,12 @@ pub struct ItemDropLocation { pub item_id: items::ClientItemId, } +pub struct LoadingQuest { + pub header_bin: Option, + pub header_dat: Option, + //pub quest_chunk_bin: Option>>, +} + pub struct ClientState { pub user: UserAccountEntity, pub settings: UserSettingsEntity, @@ -173,6 +217,8 @@ pub struct ClientState { //guildcard: GuildCard, pub block: u32, pub item_drop_location: Option, + pub done_loading_quest: bool, + //pub loading_quest: Option, } impl ClientState { @@ -184,6 +230,7 @@ impl ClientState { session: session, block: 1, item_drop_location: None, + done_loading_quest: false, } } } @@ -197,6 +244,7 @@ pub struct ShipServerState { name: String, rooms: Rooms, item_manager: items::ItemManager, + quests: quests::QuestList, } impl ShipServerState { @@ -209,6 +257,7 @@ impl ShipServerState { name: "Sona-Nyl".into(), rooms: [None; MAX_ROOMS], item_manager: items::ItemManager::new(), + quests: quests::load_quests("data/quests.toml".into()), } } @@ -288,13 +337,26 @@ impl ServerState for ShipServerState { RecvShipPacket::Login(login) => { Box::new(handler::auth::validate_login(id, login, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager, &self.name)?.into_iter().map(move |pkt| (id, pkt))) }, + RecvShipPacket::QuestDetailRequest(questdetailrequest) => { + match questdetailrequest.menu { + QUEST_SELECT_MENU_ID => handler::quest::quest_detail(id, questdetailrequest, &self.quests)?, + _ => unreachable!(), + } + }, RecvShipPacket::MenuSelect(menuselect) => { match menuselect.menu { BLOCK_MENU_ID => Box::new(handler::lobby::block_selected(id, menuselect, &mut self.clients, &self.item_manager, &self.level_table)?.into_iter().map(move |pkt| (id, pkt))), ROOM_MENU_ID => handler::room::join_room(id, menuselect, &mut self.client_location, &mut self.clients, &mut self.item_manager, &self.level_table, &mut self.rooms)?, + QUEST_CATEGORY_MENU_ID => handler::quest::select_quest_category(id, menuselect, &self.quests)?, _ => unreachable!(), } }, + RecvShipPacket::QuestMenuSelect(questmenuselect) => { + handler::quest::load_quest(id, questmenuselect, &self.quests, &mut self.clients, &self.client_location, &mut self.rooms)? + }, + RecvShipPacket::MenuDetail(_menudetail) => { + unreachable!(); + }, RecvShipPacket::RoomPasswordReq(room_password_req) => { if room_password_req.password == self.rooms[room_password_req.item as usize].as_ref() .ok_or(ShipError::InvalidRoom(room_password_req.item))? @@ -349,9 +411,25 @@ impl ServerState for ShipServerState { RecvShipPacket::DoneBursting(_) => { handler::room::done_bursting(id, &self.client_location, &mut self.rooms) }, + RecvShipPacket::DoneBursting2(_) => { + Box::new(None.into_iter()) + //handler::room::done_bursting2(id, &self.client_location, &mut self.rooms) + }, RecvShipPacket::LobbySelect(pkt) => { Box::new(handler::lobby::change_lobby(id, pkt.lobby, &mut self.client_location, &self.clients, &mut self.item_manager, &self.level_table, &mut self.rooms, &mut self.entity_gateway)?.into_iter()) - } + }, + RecvShipPacket::RequestQuestList(_) => { + handler::quest::send_quest_category_list(id, &self.quests)? + }, + RecvShipPacket::QuestFileRequest(quest_file_request) => { + handler::quest::quest_file_request(id, quest_file_request, &self.quests)? + }, + RecvShipPacket::QuestChunkAck(quest_chunk_ack) => { + handler::quest::quest_chunk_ack(id, quest_chunk_ack, &self.quests)? + }, + RecvShipPacket::DoneLoadingQuest(_) => { + handler::quest::done_loading_quest(id, &self.quests, &mut self.clients, &self.client_location)? + }, }) }