326 lines
11 KiB
Rust
326 lines
11 KiB
Rust
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<QuestListEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct QuestListConfig {
|
|
questlist: HashMap<String, Vec<QuestListEntry>>,
|
|
}
|
|
|
|
#[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<Option<MapObject>>),
|
|
Enemy(Vec<Option<MapEnemy>>),
|
|
Wave,
|
|
}
|
|
|
|
|
|
fn read_dat_section_header<T: Read + Seek>(cursor: &mut T, episode: &Episode, map_areas: &MapAreaLookup) -> Result<DatBlock, ParseDatError> {
|
|
let header = cursor.read_u32::<LittleEndian>()?;
|
|
let _offset = cursor.read_u32::<LittleEndian>()?;
|
|
let area = cursor.read_u16::<LittleEndian>()?;
|
|
let _unknown1 = cursor.read_u16::<LittleEndian>()?;
|
|
let length = cursor.read_u32::<LittleEndian>()?;
|
|
|
|
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<Episode> {
|
|
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<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, 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<Vec<u8>>,
|
|
pub dat_blob: Arc<Vec<u8>>,
|
|
pub enemies: Vec<Option<MapEnemy>>, // TODO: Arc?
|
|
pub objects: Vec<Option<MapObject>>, // TODO: Arc?
|
|
pub map_areas: MapAreaLookup, // TODO: Arc?
|
|
}
|
|
|
|
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().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<QuestCategory, Vec<Quest>>;
|
|
|
|
pub fn load_quest(bin_path: PathBuf, dat_path: PathBuf, quest_path: PathBuf) -> Option<Quest> {
|
|
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<QuestList, QuestLoadError> {
|
|
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<String, QuestListCategory> = 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<QuestList, QuestLoadError> {
|
|
match mode {
|
|
RoomMode::Single {episode, difficulty } => {
|
|
load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "single", "quests.toml"]))
|
|
},
|
|
RoomMode::Multi {episode, difficulty } => {
|
|
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<QuestList, QuestLoadError> {
|
|
match mode {
|
|
RoomMode::Single {episode, difficulty } => {
|
|
load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "government", "quests.toml"]))
|
|
},
|
|
RoomMode::Multi {episode, difficulty } => {
|
|
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);
|
|
|
|
}
|
|
}
|