Skip to content

Commit

Permalink
Added instruction pattern matching, refactored some
Browse files Browse the repository at this point in the history
  • Loading branch information
vswarte committed Jul 11, 2024
1 parent accac1d commit 7432fb2
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 138 deletions.
22 changes: 15 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
[package]
name = "eldenring-alt-saves"
version = "1.3.2"
version = "1.4.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"

[dependencies]
log = "0.4.1"
serde = { version = "1.0.160", features = ["derive"]}
toml = "0.7.2"
broadsword = { git = "https://github.com/vswarte/broadsword.git" }
detour = { git = "https://github.com/veeenu/detour-rs.git", branch = "master" }

[dependencies.windows]
version = "0.48.0"
features = [
"Win32_Foundation",
]

[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
[dependencies.retour]
version = "0.3"
features = [
"static-detour",
]

[patch.crates-io]
libudis86-sys = { git = 'https://github.com/vars1ty/libudis86-sys.git' }
24 changes: 13 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,38 @@ const CONFIG_FILE: &str = "./altsaves.toml";

static mut CONFIG: sync::OnceLock<Config> = sync::OnceLock::new();

#[derive(Clone, Deserialize)]
#[derive(Deserialize)]
pub struct Config {
pub extension: String,
pub seamless_extension: Option<String>,
}

impl Default for Config {
fn default() -> Self {
Config { extension: ".mod".to_string(), seamless_extension: Some(".mod.co2".to_string()) }
Config {
extension: ".mod".to_string(),
seamless_extension: Some(".mod.co2".to_string())
}
}
}

fn get_config_file() -> Config {
fn get_config_file() -> &'static Config {
unsafe {
CONFIG.get_or_init(|| read_config_file().unwrap_or_else(|| Config::default())).clone()
CONFIG.get_or_init(|| read_config_file().unwrap_or_default())
}
}

fn read_config_file() -> Option<Config> {
path::absolute(path::PathBuf::from(CONFIG_FILE))
.map(|p| fs::read_to_string(p).ok()).ok()
.flatten()
.map(|f| toml::from_str(f.as_str()).ok())
.flatten()
.and_then(|f| toml::from_str(f.as_str()).ok())
}

pub fn get_rewrite_extension() -> String {
get_config_file().extension.clone()
pub fn get_rewrite_extension() -> &'static str {
get_config_file().extension.as_ref()
}

pub fn get_seamless_rewrite_extension() -> Option<String> {
get_config_file().seamless_extension.clone()
}
pub fn get_seamless_rewrite_extension() -> Option<&'static String> {
get_config_file().seamless_extension.as_ref()
}
89 changes: 89 additions & 0 deletions src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use crate::config::{get_rewrite_extension, get_seamless_rewrite_extension};

use std::mem::transmute;

use broadsword::runtime;
use retour::static_detour;
use windows::{core::{HSTRING, PCWSTR}, Win32::Foundation::HANDLE};

const SAVEGAME_EXTENSION: &str = ".sl2";
const SAVEGAME_BACKUP_EXTENSION: &str = ".sl2.bak";
const SC_SAVEGAME_EXTENSION: &str = ".co2";
const SC_SAVEGAME_BACKUP_EXTENSION: &str = ".co2.bak";

static_detour! {
static CREATE_FILE_W_HOOK: unsafe extern "C" fn(PCWSTR, u32, u32, u64, u32, u32, HANDLE) -> u64;
}

pub fn hook() {
// Hook Kernel32's CreateFileW since that is responsible for opening file
// handles, and it's nice and documented.
let create_file_w = runtime::get_module_symbol("kernel32", "CreateFileW")
.expect("Could not find CreateFileW");

unsafe {
CREATE_FILE_W_HOOK
.initialize(
transmute(create_file_w),
create_file_hook,
)
.unwrap();

CREATE_FILE_W_HOOK.enable().unwrap();
}
}

/// Handle an actual invoke of CreateFileW and do necessary rewrites.
fn create_file_hook(
path: PCWSTR,
desired_access: u32,
share_mode: u32,
security_attributes: u64,
creation_disposition: u32,
flags_and_attributes: u32,
template_file: HANDLE,
) -> u64 {
let patched_path = unsafe { transform_path(path) }
.map(HSTRING::from);

let result_path = match patched_path {
None => path,
Some(s) => PCWSTR::from_raw(s.as_ptr()),
};

unsafe {
CREATE_FILE_W_HOOK.call(
result_path,
desired_access,
share_mode,
security_attributes,
creation_disposition,
flags_and_attributes,
template_file,
)
}
}

/// Transforms the input path for CreateFileW, yields Some() with a new path or
/// None if no rewriting was necessary.
unsafe fn transform_path(path: PCWSTR) -> Option<String> {
// TODO: this logic can be broken up for readability
let path_string = path.to_string()
.expect("Could not convert PCWSTR into string");

if path_string.ends_with(SAVEGAME_EXTENSION) ||
path_string.ends_with(SAVEGAME_BACKUP_EXTENSION) {

Some(path_string.replace(SAVEGAME_EXTENSION, get_rewrite_extension()))
} else if path_string.ends_with(SC_SAVEGAME_EXTENSION) ||
path_string.ends_with(SC_SAVEGAME_BACKUP_EXTENSION) {

let extension = get_seamless_rewrite_extension()
.map(|f| f.as_str())
.unwrap_or(get_rewrite_extension());

Some(path_string.replace(SC_SAVEGAME_EXTENSION, extension))
} else {
None
}
}
164 changes: 44 additions & 120 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,134 +1,58 @@
#![feature(absolute_path)]

use std::mem;

mod config;
mod file;
mod regulation;

use broadsword::dll;
use broadsword::runtime;
use detour::static_detour;
use broadsword::runtime::get_module_handle;
use windows::core::{HSTRING, PCWSTR};
use windows::Win32::Foundation::HANDLE;

use crate::config::{get_rewrite_extension, get_seamless_rewrite_extension};

const SAVEGAME_EXTENSION: &str = ".sl2";
const SAVEGAME_BACKUP_EXTENSION: &str = ".sl2.bak";
const SC_SAVEGAME_EXTENSION: &str = ".co2";
const SC_SAVEGAME_BACKUP_EXTENSION: &str = ".co2.bak";

const REGBIN_CHECK_FLAG_IBO: usize = 0x3b40052;
const REGULATIONMANAGER_CONSTRUCTOR_IBO: usize = 0xe076c0;
use std::slice;

static_detour! {
static CREATE_FILE_W_HOOK: unsafe extern "system" fn(PCWSTR, u32, u32, u64, u32, u32, HANDLE) -> u64;
static REGULATIONMANAGER_CONSTRUCTOR: unsafe extern "system" fn(u64, u64) -> u64;
}
use broadsword::{dll, runtime, scanner};

#[dll::entrypoint]
pub fn entry(_: usize) -> bool {
apply_file_hook();
apply_regulation_hook();
file::hook();
regulation::hook();
true
}

fn apply_regulation_hook() {
let regulationmanager_constructor = get_main_module() + REGULATIONMANAGER_CONSTRUCTOR_IBO;

unsafe {
REGULATIONMANAGER_CONSTRUCTOR
.initialize(
mem::transmute(regulationmanager_constructor),
|allocated_space: u64, param_2: u64| {
let result = REGULATIONMANAGER_CONSTRUCTOR.call(allocated_space, param_2);
patch_regbin_check();
result
}
)
.unwrap();

REGULATIONMANAGER_CONSTRUCTOR.enable().unwrap();
}
}

// Overwrites the flag that seems to determine if the regulation bin file should be checked against
// a particular hash. This check causes new save files to throw errors when the regbin has been
// changed.
fn patch_regbin_check() {
let ptr = get_main_module() + REGBIN_CHECK_FLAG_IBO;
unsafe { *(ptr as *mut u8) = 0x0 };
/// Takes an instruction pattern and looks for its location
pub fn match_instruction_pattern(pattern: &str) -> Option<PatternResult> {
// Find .text section details since that's where the code lives
let text_section = runtime::get_module_section_range("eldenring.exe", ".text")
.or_else(|_| runtime::get_module_section_range("start_protected_game.exe", ".text"))
.unwrap();

// Represent search area as a slice
let scan_slice = unsafe {
slice::from_raw_parts(
text_section.start as *const u8,
text_section.end - text_section.start,
)
};

let pattern = scanner::Pattern::from_bit_pattern(pattern).unwrap();

scanner::simple::scan(scan_slice, &pattern)
// TODO: this kinda of rebasing can be done in broadsword probably
.map(|result| PatternResult {
location: text_section.start + result.location,
captures: result.captures.into_iter()
.map(|capture| {
PatternCapture {
location: text_section.start + capture.location,
bytes: capture.bytes,
}
})
.collect()
})
}

fn apply_file_hook() {
let create_file_w = runtime::get_module_symbol("kernel32", "CreateFileW")
.expect("Could not find CreateFileW");

unsafe {
CREATE_FILE_W_HOOK
.initialize(
mem::transmute(create_file_w),
move |path: PCWSTR,
desired_access: u32,
share_mode: u32,
security_attributes: u64,
creation_disposition: u32,
flags_and_attributes: u32,
template_file: HANDLE| {

// Doing this here to ensure the string isn't dropped until after the fn call
// otherwise the string's source is dropped before the pointer is consumed.
let patched_path = transform_path(path)
.map(HSTRING::from);

let effective_path = match patched_path {
None => path,
Some(s) => PCWSTR::from_raw(s.as_ptr()),
};

CREATE_FILE_W_HOOK.call(
effective_path,
desired_access,
share_mode,
security_attributes,
creation_disposition,
flags_and_attributes,
template_file,
)
},
)
.unwrap();

CREATE_FILE_W_HOOK.enable().unwrap();
}
}

// TODO: Rewrites can be cached but is it worth the performance gain with how little it's called?
unsafe fn transform_path(path: PCWSTR) -> Option<String> {
let path_string = path.to_string()
.expect("Could not convert PCWSTR into string");

if path_string.ends_with(SAVEGAME_EXTENSION) || path_string.ends_with(SAVEGAME_BACKUP_EXTENSION) {
Some(path_string.clone().replace(
SAVEGAME_EXTENSION,
get_rewrite_extension().as_str(),
))
} else if path_string.ends_with(SC_SAVEGAME_EXTENSION) || path_string.ends_with(SC_SAVEGAME_BACKUP_EXTENSION) {
let extension = get_seamless_rewrite_extension()
.unwrap_or(get_rewrite_extension());

Some(path_string.clone().replace(
SC_SAVEGAME_EXTENSION,
extension.as_str(),
))
} else {
None
}
#[derive(Debug)]
pub struct PatternResult {
location: usize,
captures: Vec<PatternCapture>,
}

/// Attempts to retrieve the main module of the game
pub fn get_main_module() -> usize {
get_module_handle("eldenring.exe")
.or_else(|_| get_module_handle("start_protected_game.exe"))
.expect("Could not locate main module")
#[derive(Debug)]
pub struct PatternCapture {
location: usize,
bytes: Vec<u8>,
}
Loading

0 comments on commit 7432fb2

Please sign in to comment.