jake
5 years ago
11 changed files with 714 additions and 99 deletions
-
1Cargo.toml
-
46src/ship/map.rs
-
1src/ship/mod.rs
-
1src/ship/packet/builder/mod.rs
-
47src/ship/packet/builder/quest.rs
-
1src/ship/packet/handler/mod.rs
-
209src/ship/packet/handler/quest.rs
-
26src/ship/packet/handler/room.rs
-
224src/ship/quests.rs
-
11src/ship/room.rs
-
88src/ship/ship.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<Quest>) -> 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),
|
|||
}
|
|||
}
|
@ -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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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()))
|
|||
}
|
|||
}
|
@ -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<QuestListEntry>,
|
|||
}
|
|||
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
|||
struct QuestListConfig {
|
|||
questlist: HashMap<String, Vec<QuestListEntry>>,
|
|||
}
|
|||
|
|||
#[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<Option<MapObject>>),
|
|||
Enemy(Vec<Option<MapEnemy>>),
|
|||
Wave,
|
|||
}
|
|||
|
|||
|
|||
fn read_dat_section_header<T: Read + Seek>(cursor: &mut T, episode: &Episode) -> Result<DatBlock, ParseDatError> {
|
|||
let header = cursor.read_u32::<LittleEndian>()?;
|
|||
let offset = cursor.read_u32::<LittleEndian>()?;
|
|||
let area = cursor.read_u32::<LittleEndian>()?;
|
|||
let length = cursor.read_u32::<LittleEndian>()?;
|
|||
|
|||
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<Episode> {
|
|||
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<Option<MapEnemy>>, Vec<Option<MapObject>>), 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<u8>,
|
|||
pub dat_blob: Vec<u8>,
|
|||
pub enemies: Vec<Option<MapEnemy>>,
|
|||
pub objects: Vec<Option<MapObject>>,
|
|||
}
|
|||
|
|||
impl Quest {
|
|||
fn from_bin_dat(bin: Vec<u8>, dat: Vec<u8>) -> Result<Quest, QuestLoadError> {
|
|||
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<QuestCategory, Vec<Quest>>;
|
|||
|
|||
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<String, QuestListCategory> = 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()
|
|||
|
|||
}
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue