mirror of
https://github.com/redstrate/Physis.git
synced 2025-07-20 07:47:45 +00:00
Overhaul file resource handling
This is a big API change, and it's probably not in it's ideal shape right now. Physis makes a big assumption that the pool of game data that you're reading from is in the compressed, SqPack format like in retail. However, as the projects that use Physis expand - so does the scope of data people want to punt into it. For example, we have Icarus that makes it easy to read Excel data but it can only do so through SqPack which makes it less useful in scenarios where you want to read unpacked Excel modded files or perhaps Excel data from the network. So I created a Resource trait that GameData now implements (and it's now called SqPackResource.) In a similar vain, Physis comes with an UnpackedResource and a ResourceResolver to chain multiple Resources together.
This commit is contained in:
parent
c602e6b28f
commit
81fff744f2
6 changed files with 232 additions and 93 deletions
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
use physis::common::Platform;
|
use physis::common::Platform;
|
||||||
use physis::gamedata::GameData;
|
use physis::resource::SqPackResource;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -21,7 +21,7 @@ fn main() {
|
||||||
let destination_path = &args[3];
|
let destination_path = &args[3];
|
||||||
|
|
||||||
// Create a GameData struct, this manages the repositories. It allows us to easily extract files.
|
// Create a GameData struct, this manages the repositories. It allows us to easily extract files.
|
||||||
let mut game_data = GameData::from_existing(Platform::Win32, game_dir);
|
let mut game_data = SqPackResource::from_existing(Platform::Win32, game_dir);
|
||||||
|
|
||||||
// Extract said file:
|
// Extract said file:
|
||||||
let Some(game_file) = game_data.extract(file_path) else {
|
let Some(game_file) = game_data.extract(file_path) else {
|
||||||
|
|
|
@ -11,9 +11,6 @@ pub type ByteSpan<'a> = &'a [u8];
|
||||||
/// Represents a continuous block of memory which is owned.
|
/// Represents a continuous block of memory which is owned.
|
||||||
pub type ByteBuffer = Vec<u8>;
|
pub type ByteBuffer = Vec<u8>;
|
||||||
|
|
||||||
/// Reading and writing game data repositories, such as "ffxiv" and "ex1", and so on.
|
|
||||||
pub mod gamedata;
|
|
||||||
|
|
||||||
/// Parsing game repositories, such as "ffxiv", "ex1" and their version information.
|
/// Parsing game repositories, such as "ffxiv", "ex1" and their version information.
|
||||||
pub mod repository;
|
pub mod repository;
|
||||||
|
|
||||||
|
@ -149,6 +146,9 @@ pub mod lvb;
|
||||||
/// Reading SVB files
|
/// Reading SVB files
|
||||||
pub mod svb;
|
pub mod svb;
|
||||||
|
|
||||||
|
/// File resource handling.
|
||||||
|
pub mod resource;
|
||||||
|
|
||||||
mod bcn;
|
mod bcn;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
|
48
src/resource/mod.rs
Normal file
48
src/resource/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
mod resolver;
|
||||||
|
pub use resolver::ResourceResolver;
|
||||||
|
|
||||||
|
mod sqpack;
|
||||||
|
pub use sqpack::SqPackResource;
|
||||||
|
|
||||||
|
mod unpacked;
|
||||||
|
pub use unpacked::UnpackedResource;
|
||||||
|
|
||||||
|
use crate::ByteBuffer;
|
||||||
|
|
||||||
|
/// Represents a source of files for reading.
|
||||||
|
/// This abstracts away some of the nitty-gritty of where files come from. These could be coming from a compressed archive like SqPack, unpacked files on disk, or even a network.
|
||||||
|
pub trait Resource {
|
||||||
|
/// Reads the file located at `path`. This is returned as an in-memory buffer, and will usually
|
||||||
|
/// have to be further parsed.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```should_panic
|
||||||
|
/// # use physis::resource::{Resource, SqPackResource};
|
||||||
|
/// # use std::io::Write;
|
||||||
|
/// # use physis::common::Platform;
|
||||||
|
/// let mut game = SqPackResource::from_existing(Platform::Win32, "SquareEnix/Final Fantasy XIV - A Realm Reborn/game");
|
||||||
|
/// let data = game.read("exd/root.exl").unwrap();
|
||||||
|
///
|
||||||
|
/// let mut file = std::fs::File::create("root.exl").unwrap();
|
||||||
|
/// file.write(data.as_slice()).unwrap();
|
||||||
|
/// ```
|
||||||
|
fn read(&mut self, path: &str) -> Option<ByteBuffer>;
|
||||||
|
|
||||||
|
/// Checks if a file exists
|
||||||
|
/// While you could abuse `read` to do this, in some Resources they can optimize this since it doesn't read data.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use physis::common::Platform;
|
||||||
|
/// # use physis::resource::{Resource, SqPackResource};
|
||||||
|
/// let mut game = SqPackResource::from_existing(Platform::Win32, "SquareEnix/Final Fantasy XIV - A Realm Reborn/game");
|
||||||
|
/// if game.exists("exd/cid.exl") {
|
||||||
|
/// println!("Cid really does exist!");
|
||||||
|
/// } else {
|
||||||
|
/// println!("Oh noes!");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn exists(&mut self, path: &str) -> bool;
|
||||||
|
}
|
54
src/resource/resolver.rs
Normal file
54
src/resource/resolver.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::ByteBuffer;
|
||||||
|
|
||||||
|
use super::Resource;
|
||||||
|
|
||||||
|
/// Allows chaining multiple FileSources together.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use physis::resource::{ResourceResolver, SqPackResource, UnpackedResource};
|
||||||
|
/// # use physis::common::Platform;
|
||||||
|
/// let sqpack_source = SqPackResource::from_existing(Platform::Win32, "SquareEnix/Final Fantasy XIV - A Realm Reborn/game");
|
||||||
|
/// let file_source = UnpackedResource::from_existing("unpacked/");
|
||||||
|
/// let mut resolver = ResourceResolver::new();
|
||||||
|
/// resolver.add_source(Box::new(file_source)); // first has most priority
|
||||||
|
/// resolver.add_source(Box::new(sqpack_source)); // this is the fallback
|
||||||
|
/// ```
|
||||||
|
pub struct ResourceResolver {
|
||||||
|
resolvers: Vec<Box<dyn Resource>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceResolver {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
resolvers: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_source(&mut self, source: Box<dyn Resource>) {
|
||||||
|
self.resolvers.push(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource for ResourceResolver {
|
||||||
|
fn read(&mut self, path: &str) -> Option<ByteBuffer> {
|
||||||
|
for resolver in &mut self.resolvers {
|
||||||
|
if let Some(bytes) = resolver.read(path) {
|
||||||
|
return Some(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&mut self, path: &str) -> bool {
|
||||||
|
for resolver in &mut self.resolvers {
|
||||||
|
if resolver.exists(path) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
151
src/gamedata.rs → src/resource/sqpack.rs
Executable file → Normal file
151
src/gamedata.rs → src/resource/sqpack.rs
Executable file → Normal file
|
@ -1,40 +1,21 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
use std::{
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
collections::HashMap,
|
||||||
|
fs::{self, DirEntry, ReadDir},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use crate::{
|
||||||
use std::fs;
|
ByteBuffer,
|
||||||
use std::fs::{DirEntry, ReadDir};
|
common::{Language, Platform, read_version},
|
||||||
use std::path::PathBuf;
|
exd::EXD,
|
||||||
|
exh::EXH,
|
||||||
|
exl::EXL,
|
||||||
|
patch::{PatchError, ZiPatch},
|
||||||
|
repository::{Category, Repository, string_to_category},
|
||||||
|
sqpack::{IndexEntry, SqPackData, SqPackIndex},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::ByteBuffer;
|
use super::Resource;
|
||||||
use crate::common::{Language, Platform, read_version};
|
|
||||||
use crate::exd::EXD;
|
|
||||||
use crate::exh::EXH;
|
|
||||||
use crate::exl::EXL;
|
|
||||||
use crate::patch::{PatchError, ZiPatch};
|
|
||||||
use crate::repository::{Category, Repository, string_to_category};
|
|
||||||
use crate::sqpack::{IndexEntry, SqPackData, SqPackIndex};
|
|
||||||
|
|
||||||
/// Framework for operating on game data.
|
|
||||||
pub struct GameData {
|
|
||||||
/// The game directory to operate on.
|
|
||||||
pub game_directory: String,
|
|
||||||
|
|
||||||
/// Repositories in the game directory.
|
|
||||||
pub repositories: Vec<Repository>,
|
|
||||||
|
|
||||||
index_files: HashMap<String, SqPackIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid(path: &str) -> bool {
|
|
||||||
let d = PathBuf::from(path);
|
|
||||||
|
|
||||||
if fs::metadata(d.as_path()).is_err() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Possible actions to repair game files
|
/// Possible actions to repair game files
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -52,20 +33,19 @@ pub enum RepairError<'a> {
|
||||||
FailedRepair(&'a Repository),
|
FailedRepair(&'a Repository),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameData {
|
/// Used to read files from the retail game, in their SqPack-compressed format.
|
||||||
/// Read game data from an existing game installation.
|
pub struct SqPackResource {
|
||||||
///
|
/// The game directory to operate on.
|
||||||
/// This will return a GameData even if the game directory is technically
|
pub game_directory: String,
|
||||||
/// invalid, but it won't have any repositories.
|
|
||||||
///
|
/// Repositories in the game directory.
|
||||||
/// # Example
|
pub repositories: Vec<Repository>,
|
||||||
///
|
|
||||||
/// ```
|
index_files: HashMap<String, SqPackIndex>,
|
||||||
/// # use physis::common::Platform;
|
}
|
||||||
/// use physis::gamedata::GameData;
|
|
||||||
/// GameData::from_existing(Platform::Win32, "$FFXIV/game");
|
impl SqPackResource {
|
||||||
/// ```
|
pub fn from_existing(platform: Platform, directory: &str) -> Self {
|
||||||
pub fn from_existing(platform: Platform, directory: &str) -> GameData {
|
|
||||||
match is_valid(directory) {
|
match is_valid(directory) {
|
||||||
true => {
|
true => {
|
||||||
let mut data = Self {
|
let mut data = Self {
|
||||||
|
@ -138,43 +118,6 @@ impl GameData {
|
||||||
SqPackData::from_existing(dat_path.to_str()?)
|
SqPackData::from_existing(dat_path.to_str()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a file located at `path` exists.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use physis::common::Platform;
|
|
||||||
/// use physis::gamedata::GameData;
|
|
||||||
/// # let mut game = GameData::from_existing(Platform::Win32, "SquareEnix/Final Fantasy XIV - A Realm Reborn/game");
|
|
||||||
/// if game.exists("exd/cid.exl") {
|
|
||||||
/// println!("Cid really does exist!");
|
|
||||||
/// } else {
|
|
||||||
/// println!("Oh noes!");
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn exists(&mut self, path: &str) -> bool {
|
|
||||||
let Some(_) = self.get_index_filenames(path) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.find_entry(path).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts the file located at `path`. This is returned as an in-memory buffer, and will usually
|
|
||||||
/// have to be further parsed.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```should_panic
|
|
||||||
/// # use physis::gamedata::GameData;
|
|
||||||
/// # use std::io::Write;
|
|
||||||
/// use physis::common::Platform;
|
|
||||||
/// # let mut game = GameData::from_existing(Platform::Win32, "SquareEnix/Final Fantasy XIV - A Realm Reborn/game");
|
|
||||||
/// let data = game.extract("exd/root.exl").unwrap();
|
|
||||||
///
|
|
||||||
/// let mut file = std::fs::File::create("root.exl").unwrap();
|
|
||||||
/// file.write(data.as_slice()).unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn extract(&mut self, path: &str) -> Option<ByteBuffer> {
|
pub fn extract(&mut self, path: &str) -> Option<ByteBuffer> {
|
||||||
let slice = self.find_entry(path);
|
let slice = self.find_entry(path);
|
||||||
match slice {
|
match slice {
|
||||||
|
@ -423,19 +366,51 @@ impl GameData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Resource for SqPackResource {
|
||||||
|
fn read(&mut self, path: &str) -> Option<ByteBuffer> {
|
||||||
|
let slice = self.find_entry(path);
|
||||||
|
match slice {
|
||||||
|
Some((entry, chunk)) => {
|
||||||
|
let mut dat_file = self.get_dat_file(path, chunk, entry.data_file_id.into())?;
|
||||||
|
|
||||||
|
dat_file.read_from_offset(entry.offset)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&mut self, path: &str) -> bool {
|
||||||
|
let Some(_) = self.get_index_filenames(path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.find_entry(path).is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid(path: &str) -> bool {
|
||||||
|
let d = PathBuf::from(path);
|
||||||
|
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::repository::Category::*;
|
use crate::repository::Category::*;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn common_setup_data() -> GameData {
|
fn common_setup_data() -> SqPackResource {
|
||||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
d.push("resources/tests");
|
d.push("resources/tests");
|
||||||
d.push("valid_sqpack");
|
d.push("valid_sqpack");
|
||||||
d.push("game");
|
d.push("game");
|
||||||
|
|
||||||
GameData::from_existing(Platform::Win32, d.to_str().unwrap())
|
SqPackResource::from_existing(Platform::Win32, d.to_str().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
62
src/resource/unpacked.rs
Normal file
62
src/resource/unpacked.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::ByteBuffer;
|
||||||
|
|
||||||
|
use super::Resource;
|
||||||
|
|
||||||
|
/// Used to read unpacked files from a directory.
|
||||||
|
pub struct UnpackedResource {
|
||||||
|
base_directory: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnpackedResource {
|
||||||
|
pub fn from_existing(base_directory: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
base_directory: base_directory.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource for UnpackedResource {
|
||||||
|
fn read(&mut self, path: &str) -> Option<ByteBuffer> {
|
||||||
|
let mut new_path = PathBuf::from(&self.base_directory);
|
||||||
|
new_path.push(path);
|
||||||
|
|
||||||
|
std::fs::read(new_path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&mut self, path: &str) -> bool {
|
||||||
|
let mut new_path = PathBuf::from(&self.base_directory);
|
||||||
|
new_path.push(path);
|
||||||
|
|
||||||
|
std::fs::exists(new_path).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn common_setup_data() -> UnpackedResource {
|
||||||
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
d.push("resources/tests");
|
||||||
|
|
||||||
|
UnpackedResource::from_existing(d.to_str().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_files() {
|
||||||
|
let mut data = common_setup_data();
|
||||||
|
|
||||||
|
assert!(data.read("empty_planlive.lgb").is_some());
|
||||||
|
assert!(data.read("non_existent.lgb").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exist_files() {
|
||||||
|
let mut data = common_setup_data();
|
||||||
|
|
||||||
|
assert_eq!(data.exists("empty_planlive.lgb"), true);
|
||||||
|
assert_eq!(data.exists("non_existent.lgb"), false);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue