mirror of
https://github.com/redstrate/Miscel.git
synced 2025-05-20 12:07:46 +00:00
Add initial files
This commit is contained in:
commit
7b08f54bb6
7 changed files with 676 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
198
Cargo.lock
generated
Normal file
198
Cargo.lock
generated
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miscel"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-deps"
|
||||||
|
version = "7.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-expr",
|
||||||
|
"heck",
|
||||||
|
"pkg-config",
|
||||||
|
"toml",
|
||||||
|
"version-compare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.12.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "miscel"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
system-deps = "7"
|
||||||
|
|
||||||
|
[package.metadata.system-deps]
|
||||||
|
libunshield = { version = "1.4", feature = "game_install" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
# enables game installation support using unshield (only supported on Linux and macOS)
|
||||||
|
game_install = []
|
||||||
|
|
||||||
|
[dependencies]
|
63
src/execlookup.rs
Normal file
63
src/execlookup.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn from_u16(from: &mut [u16]) -> &[u8] {
|
||||||
|
#[cfg(target_endian = "little")]
|
||||||
|
from.iter_mut().for_each(|word| *word = word.to_be());
|
||||||
|
|
||||||
|
let ptr: *const u8 = from.as_ptr().cast();
|
||||||
|
let len = from.len().checked_mul(2).unwrap();
|
||||||
|
|
||||||
|
unsafe { std::slice::from_raw_parts(ptr, len) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_needle(installer_file: &[u8], needle: &str) -> Option<String> {
|
||||||
|
let mut needle: Vec<u16> = needle.encode_utf16().collect();
|
||||||
|
let bytes = from_u16(&mut needle);
|
||||||
|
|
||||||
|
let mut position = installer_file
|
||||||
|
.windows(bytes.len())
|
||||||
|
.position(|window| window == bytes)?;
|
||||||
|
|
||||||
|
let parse_char_at_position = |position: usize| {
|
||||||
|
let upper = installer_file[position];
|
||||||
|
let lower = installer_file[position + 1];
|
||||||
|
|
||||||
|
let result = char::decode_utf16([((upper as u16) << 8) | lower as u16])
|
||||||
|
.map(|r| r.map_err(|e| e.unpaired_surrogate()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
result[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut string: String = String::new();
|
||||||
|
|
||||||
|
let mut last_char = parse_char_at_position(position);
|
||||||
|
while last_char.is_ok() && last_char.unwrap() != '\0' {
|
||||||
|
string.push(last_char.unwrap());
|
||||||
|
|
||||||
|
position += 2;
|
||||||
|
last_char = parse_char_at_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the frontier URL from ffxivlauncher.exe
|
||||||
|
pub fn extract_frontier_url(launcher_path: &str) -> Option<String> {
|
||||||
|
let installer_file = fs::read(launcher_path).unwrap();
|
||||||
|
|
||||||
|
// New Frontier URL format
|
||||||
|
if let Some(url) = find_needle(&installer_file, "https://launcher.finalfantasyxiv.com") {
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old Frontier URL format
|
||||||
|
if let Some(url) = find_needle(&installer_file, "https://frontier.ffxiv.com") {
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
239
src/existing_dirs.rs
Normal file
239
src/existing_dirs.rs
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
// Rust deprecating this is stupid, I don't want to use a crate here
|
||||||
|
#[allow(deprecated)]
|
||||||
|
use std::env::home_dir;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::read_dir;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Where the existing installation came from
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub enum ExistingInstallType {
|
||||||
|
/// Installed via the official launcher
|
||||||
|
OfficialLauncher,
|
||||||
|
/// Installed via XIVQuickLauncher
|
||||||
|
XIVQuickLauncher,
|
||||||
|
/// Installed via XIVLauncherCore
|
||||||
|
XIVLauncherCore,
|
||||||
|
/// Installed via XIVOnMac
|
||||||
|
XIVOnMac,
|
||||||
|
/// Installed via Astra
|
||||||
|
Astra,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An existing install location on disk
|
||||||
|
pub struct ExistingGameDirectory {
|
||||||
|
/// The application where this installation was from
|
||||||
|
pub install_type: ExistingInstallType,
|
||||||
|
/// The path to the "main folder" where "game" and "boot" sits
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds existing installations on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
|
||||||
|
pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
|
||||||
|
let mut install_dirs = Vec::new();
|
||||||
|
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"linux" => {
|
||||||
|
// Official install (Wine)
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir(".wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Official install (Steam)
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir(
|
||||||
|
".steam/steam/steamapps/common/FINAL FANTASY XIV - A Realm Reborn",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// XIVLauncherCore location
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::XIVLauncherCore,
|
||||||
|
path: from_home_dir(".xlcore/ffxiv"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Astra location. But we have to iterate through each UUID.
|
||||||
|
if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/game/")) {
|
||||||
|
entries
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|entry| {
|
||||||
|
let Ok(meta) = entry.metadata() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
if meta.is_dir() {
|
||||||
|
return vec![entry.path()];
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
.for_each(|path| {
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::Astra,
|
||||||
|
path: path.into_os_string().into_string().unwrap(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"macos" => {
|
||||||
|
// Official Launcher (macOS)
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir("Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: add XIV on Mac
|
||||||
|
}
|
||||||
|
"windows" => {
|
||||||
|
// Official install (Wine)
|
||||||
|
install_dirs.push(ExistingGameDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Add Astra
|
||||||
|
}
|
||||||
|
&_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dirs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|dir| is_valid_game_dir(&dir.path))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An existing user directory
|
||||||
|
pub struct ExistingUserDirectory {
|
||||||
|
/// The application where this directory was from
|
||||||
|
pub install_type: ExistingInstallType,
|
||||||
|
/// The path to the user folder
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds existing user folders on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
|
||||||
|
pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
|
||||||
|
let mut user_dirs = Vec::new();
|
||||||
|
#[allow(deprecated)] // We still want std::env::home_dir
|
||||||
|
let Some(_) = home_dir() else {
|
||||||
|
return user_dirs;
|
||||||
|
};
|
||||||
|
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"linux" => {
|
||||||
|
// Official install (Wine)
|
||||||
|
user_dirs.push(ExistingUserDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// XIVLauncherCore location
|
||||||
|
user_dirs.push(ExistingUserDirectory {
|
||||||
|
install_type: ExistingInstallType::XIVLauncherCore,
|
||||||
|
path: from_home_dir(".xlcore/ffxivConfig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Astra location. But we have to iterate through each UUID.
|
||||||
|
if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/user/")) {
|
||||||
|
entries
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|entry| {
|
||||||
|
let Ok(meta) = entry.metadata() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
if meta.is_dir() {
|
||||||
|
return vec![entry.path()];
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
.for_each(|path| {
|
||||||
|
user_dirs.push(ExistingUserDirectory {
|
||||||
|
install_type: ExistingInstallType::Astra,
|
||||||
|
path: path.into_os_string().into_string().unwrap(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"macos" => {
|
||||||
|
// Official install (Wine)
|
||||||
|
user_dirs.push(ExistingUserDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Add XIV on Mac?
|
||||||
|
}
|
||||||
|
"windows" => {
|
||||||
|
// Official install
|
||||||
|
user_dirs.push(ExistingUserDirectory {
|
||||||
|
install_type: ExistingInstallType::OfficialLauncher,
|
||||||
|
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Add Astra
|
||||||
|
}
|
||||||
|
&_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_dirs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|dir| is_valid_user_dir(&dir.path))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_home_dir(path: &'static str) -> String {
|
||||||
|
#[allow(deprecated)] // We still want std::env::home_dir
|
||||||
|
let mut new_path = home_dir().unwrap();
|
||||||
|
new_path.push(path);
|
||||||
|
new_path.into_os_string().into_string().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_game_dir(path: &String) -> bool {
|
||||||
|
let mut d = PathBuf::from(path);
|
||||||
|
|
||||||
|
// Check for the dir itself
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "game"
|
||||||
|
d.push("game");
|
||||||
|
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "boot"
|
||||||
|
d.pop();
|
||||||
|
d.push("boot");
|
||||||
|
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_user_dir(path: &String) -> bool {
|
||||||
|
let mut d = PathBuf::from(path);
|
||||||
|
|
||||||
|
// Check for the dir itself
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "FFXIV.cfg"
|
||||||
|
d.push("FFXIV.cfg");
|
||||||
|
|
||||||
|
if fs::metadata(d.as_path()).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
148
src/installer.rs
Normal file
148
src/installer.rs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString, NulError};
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
|
const FILES_TO_EXTRACT: [&str; 3] = ["data1.cab", "data1.hdr", "data2.cab"];
|
||||||
|
|
||||||
|
const BOOT_COMPONENT_FILES: [&str; 18] = [
|
||||||
|
"cef_license.txt",
|
||||||
|
"FFXIV.ico",
|
||||||
|
"ffxivboot.exe",
|
||||||
|
"ffxivboot.ver",
|
||||||
|
"ffxivboot64.exe",
|
||||||
|
"ffxivconfig.exe",
|
||||||
|
"ffxivconfig64.exe",
|
||||||
|
"ffxivlauncher.exe",
|
||||||
|
"ffxivlauncher64.exe",
|
||||||
|
"ffxivsysinfo.exe",
|
||||||
|
"ffxivsysinfo64.exe",
|
||||||
|
"ffxivupdater.exe",
|
||||||
|
"ffxivupdater64.exe",
|
||||||
|
"FFXIV_sysinfo.ico",
|
||||||
|
"icudt.dll",
|
||||||
|
"libcef.dll",
|
||||||
|
"license.txt",
|
||||||
|
"locales/reserved.txt",
|
||||||
|
];
|
||||||
|
|
||||||
|
const GAME_COMPONENT_FILES: [&str; 1] = ["ffxivgame.ver"];
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct Unshield {
|
||||||
|
_private: [u8; 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn unshield_open(filename: *const c_char) -> *mut Unshield;
|
||||||
|
fn unshield_close(unshield: *mut Unshield);
|
||||||
|
|
||||||
|
fn unshield_set_log_level(level: i32);
|
||||||
|
|
||||||
|
fn unshield_file_count(unshield: *mut Unshield) -> i32;
|
||||||
|
fn unshield_file_name(unshield: *mut Unshield, index: i32) -> *const c_char;
|
||||||
|
fn unshield_file_save(unshield: *mut Unshield, index: i32, filename: *const c_char) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InstallError {
|
||||||
|
IOFailure,
|
||||||
|
FFIFailure,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for InstallError {
|
||||||
|
fn from(_: std::io::Error) -> Self {
|
||||||
|
InstallError::IOFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NulError> for InstallError {
|
||||||
|
fn from(_: NulError) -> Self {
|
||||||
|
InstallError::FFIFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs the game from the provided retail installer.
|
||||||
|
pub fn install_game(installer_path: &str, game_directory: &str) -> Result<(), InstallError> {
|
||||||
|
let installer_file = fs::read(installer_path).unwrap();
|
||||||
|
|
||||||
|
let mut last_position = 0;
|
||||||
|
let mut last_filename = "";
|
||||||
|
for filename in FILES_TO_EXTRACT {
|
||||||
|
let needle = format!("Disk1\\{}", filename);
|
||||||
|
|
||||||
|
let position = installer_file
|
||||||
|
.windows(needle.len())
|
||||||
|
.position(|window| window == needle.as_str().as_bytes());
|
||||||
|
if position == None {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = position.unwrap();
|
||||||
|
|
||||||
|
if last_position != 0 {
|
||||||
|
let mut temp_dir = std::env::temp_dir();
|
||||||
|
temp_dir.push(last_filename);
|
||||||
|
|
||||||
|
let mut new_file = File::create(temp_dir).unwrap();
|
||||||
|
|
||||||
|
if last_filename == "data1.hdr" {
|
||||||
|
new_file.write_all(&installer_file[last_position + 30..position - 42])?;
|
||||||
|
} else {
|
||||||
|
new_file.write_all(&installer_file[last_position + 33..position - 42])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_position = position;
|
||||||
|
last_filename = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut temp_dir = std::env::temp_dir();
|
||||||
|
temp_dir.push(last_filename);
|
||||||
|
|
||||||
|
let mut new_file = File::create(temp_dir).unwrap();
|
||||||
|
|
||||||
|
new_file.write_all(&installer_file[last_position + 33..installer_file.len() - 42])?;
|
||||||
|
|
||||||
|
fs::create_dir_all(format!("{game_directory}/boot"))?;
|
||||||
|
fs::create_dir_all(format!("{game_directory}/game"))?;
|
||||||
|
|
||||||
|
// set unshield to shut up
|
||||||
|
unsafe { unshield_set_log_level(0) };
|
||||||
|
|
||||||
|
let mut temp_dir = std::env::temp_dir();
|
||||||
|
temp_dir.push("data1.cab");
|
||||||
|
let temp_dir_string = CString::new(temp_dir.to_str().unwrap())?;
|
||||||
|
|
||||||
|
let unshield = unsafe { unshield_open(temp_dir_string.as_ptr()) };
|
||||||
|
let file_count = unsafe { unshield_file_count(unshield) };
|
||||||
|
|
||||||
|
for i in 0..file_count {
|
||||||
|
let filename = unsafe { CStr::from_ptr(unshield_file_name(unshield, i)).to_string_lossy() };
|
||||||
|
|
||||||
|
for boot_name in BOOT_COMPONENT_FILES {
|
||||||
|
if boot_name == filename {
|
||||||
|
let save_filename = format!("{game_directory}/boot/{boot_name}");
|
||||||
|
let save_filename_c = CString::new(save_filename)?;
|
||||||
|
unsafe { unshield_file_save(unshield, i, save_filename_c.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for game_name in GAME_COMPONENT_FILES {
|
||||||
|
if game_name == filename {
|
||||||
|
let save_filename = format!("{game_directory}/game/{game_name}");
|
||||||
|
let save_filename_c = CString::new(save_filename)?;
|
||||||
|
unsafe { unshield_file_save(unshield, i, save_filename_c.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
unshield_close(unshield);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/// Find existing installation directories
|
||||||
|
pub mod existing_dirs;
|
||||||
|
|
||||||
|
/// Reading data from executables
|
||||||
|
pub mod execlookup;
|
||||||
|
|
||||||
|
/// Initializing a new retail game install from the official retail installer. No execution required!
|
||||||
|
#[cfg(feature = "game_install")]
|
||||||
|
pub mod installer;
|
Loading…
Add table
Reference in a new issue