Skip to content

Commit

Permalink
Show mint mods in public server list
Browse files Browse the repository at this point in the history
  • Loading branch information
trumank committed Dec 17, 2023
1 parent 940e7aa commit e6d3ebd
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 122 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ anyhow = { version = "1.0.75", features = ["backtrace"] }
patternsleuth = { git = "https://github.com/trumank/patternsleuth" }
steamlocate = "2.0.0-alpha.0"
repak = { git = "https://github.com/trumank/repak" }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
itertools = "0.12.0"

[package]
name = "mint"
Expand Down Expand Up @@ -60,8 +63,8 @@ rfd = "0.12.1"
rust-ini = "0.20.0"
self_update = { version = "0.39.0", default-features = false, features = ["archive-zip", "rustls"] }
semver = "1.0.20"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
serde.workspace = true
serde_json.workspace = true
sha2 = "0.10.8"
steamlocate.workspace = true
task-local-extensions = "0.1.4"
Expand Down
Binary file modified assets/integration.pak
Binary file not shown.
3 changes: 3 additions & 0 deletions hook/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ crate-type = ["cdylib"]

[dependencies]
anyhow.workspace = true
repak.workspace = true
serde.workspace = true
serde_json.workspace = true
patternsleuth = { workspace = true, features = ["process-internal"] }
retour = { version = "0.3.1", features = ["static-detour"] }
hook_resolvers = { path = "../hook_resolvers" }
Expand Down
55 changes: 51 additions & 4 deletions hook/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::{
ffi::{c_void, OsString},
io::BufReader,
path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use mint_lib::DRGInstallationType;
use mint_lib::{mod_info::Meta, DRGInstallationType};
use retour::static_detour;
use windows::Win32::{
Foundation::HMODULE,
Expand Down Expand Up @@ -60,9 +61,12 @@ unsafe fn patch() -> Result<()> {
.and_then(Path::parent)
.map(|p| p.join("Content/Paks/mods_P.pak"))
.context("could not determine pak path")?;
if !pak_path.exists() {
return Ok(());
}

let mut pak_reader = BufReader::new(std::fs::File::open(pak_path)?);
let pak = repak::PakBuilder::new().reader(&mut pak_reader)?;

let meta_buf = pak.get("meta.json", &mut pak_reader)?;
let meta: Meta = serde_json::from_slice(&meta_buf)?;

let installation_type = DRGInstallationType::from_exe_path()?;

Expand All @@ -85,6 +89,47 @@ unsafe fn patch() -> Result<()> {
.enable()?;
}

if let Ok(server_mods) = resolution.server_mods {
let mods_fname = server_mods.mods_fname.0;
let set_fstring = server_mods.set_fstring.0;
USessionHandlingFSDFillSessionSetting
.initialize(
std::mem::transmute(server_mods.fill_session_setting.0),
move |world, game_settings, full_server| {
USessionHandlingFSDFillSessionSetting.call(world, game_settings, full_server);

#[derive(serde::Serialize)]
struct Wrapper {
name: String,
version: String,
category: i32,
}

let s = serde_json::to_string(&vec![Wrapper {
name: meta.to_server_list_string(),
version: "mint".into(),
category: 0,
}])
.unwrap();

let buf = s.encode_utf16().chain([0]).collect::<Vec<_>>();
let s = FString::from_slice(&buf);

type Fn = unsafe extern "system" fn(
*const c_void,
*const c_void,
*const FString,
u32,
);

let f: Fn = std::mem::transmute(set_fstring);

f(game_settings, *(mods_fname as *const *const c_void), &s, 3);
},
)?
.enable()?;
}

match installation_type {
DRGInstallationType::Steam => {
if let Ok(address) = resolution.disable {
Expand Down Expand Up @@ -217,6 +262,8 @@ static_detour! {
static SaveGameToSlot: unsafe extern "system" fn(*const USaveGame, *const FString, i32) -> bool;
static LoadGameFromSlot: unsafe extern "system" fn(*const FString, i32) -> *const USaveGame;
static DoesSaveGameExist: unsafe extern "system" fn(*const FString, i32) -> bool;
static USessionHandlingFSDFillSessionSetting: unsafe extern "system" fn(*const c_void, *mut c_void, bool);
static FOnlineSessionSettingsSetFString: unsafe extern "system" fn(*const c_void, *const c_void, *const FString, u32);
}

#[allow(non_upper_case_globals)]
Expand Down
54 changes: 54 additions & 0 deletions hook_resolvers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use patternsleuth::resolvers::futures::future::join_all;
use patternsleuth::resolvers::unreal::*;
use patternsleuth::resolvers::*;
use patternsleuth::scanner::Pattern;
use patternsleuth::MemoryAccessorTrait;

#[derive(Debug)]
pub struct GetServerName(pub usize);
Expand All @@ -22,6 +23,49 @@ impl_resolver_singleton!(GetServerName, |ctx| async {
))
});

#[derive(Debug)]
pub struct FOnlineSessionSettingsSetFString(pub usize);
impl_resolver_singleton!(FOnlineSessionSettingsSetFString, |ctx| async {
let patterns = ["48 89 5C 24 ?? 48 89 54 24 ?? 55 56 57 48 83 EC 40 49 8B F8 48 8D 69"];
let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await;
Ok(Self(ensure_one(res.into_iter().flatten())?))
});

#[derive(Debug)]
pub struct USessionHandlingFSDFillSessionSettting(pub usize);
impl_resolver_singleton!(USessionHandlingFSDFillSessionSettting, |ctx| async {
let patterns = ["48 89 5C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 55 41 54 41 55 41 56 41 57 48 8B EC 48 83 EC 50 48 8B B9"];
let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await;
Ok(Self(ensure_one(res.into_iter().flatten())?))
});

#[derive(Debug)]
pub struct ModsFName(pub usize);
impl_resolver_singleton!(ModsFName, |ctx| async {
let strings = ctx
.scan(
Pattern::from_bytes("Mods\0".encode_utf16().flat_map(u16::to_le_bytes).collect())
.unwrap(),
)
.await;

let refs = join_all(strings.iter().map(|s| {
ctx.scan(
Pattern::new(format!(
"41 b8 01 00 00 00 48 8d 15 X0x{s:X} 48 8d 0d | ?? ?? ?? ?? e9 ?? ?? ?? ??"
))
.unwrap(),
)
}))
.await;

Ok(Self(try_ensure_one(
refs.iter()
.flatten()
.map(|a| Ok(ctx.image().memory.rip4(*a)?)),
)?))
});

#[derive(Debug)]
pub struct Disable(pub usize);
impl_resolver_singleton!(Disable, |ctx| async {
Expand Down Expand Up @@ -53,6 +97,15 @@ impl_resolver_singleton!(FMemoryFree, |ctx| async {
Ok(Self(ensure_one(res.into_iter().flatten())?))
});

impl_try_collector! {
#[derive(Debug)]
pub struct ServerModsResolution {
pub set_fstring: FOnlineSessionSettingsSetFString,
pub fill_session_setting: USessionHandlingFSDFillSessionSettting,
pub mods_fname: ModsFName,
}
}

impl_try_collector! {
#[derive(Debug)]
pub struct ServerNameResolution {
Expand Down Expand Up @@ -80,6 +133,7 @@ impl_collector! {
pub fmemory_free: FMemoryFree,
pub disable: Disable,
pub server_name: ServerNameResolution,
pub server_mods: ServerModsResolution,
pub save_game: SaveGameResolution,
}
}
3 changes: 3 additions & 0 deletions mint_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ edition.workspace = true
anyhow.workspace = true
steamlocate.workspace = true
repak.workspace = true
serde.workspace = true
serde_json.workspace = true
itertools.workspace = true
2 changes: 2 additions & 0 deletions mint_lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod mod_info;

use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
Expand Down
141 changes: 141 additions & 0 deletions mint_lib/src/mod_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

/// Tags from mod.io.
#[derive(Debug, Clone)]
pub struct ModioTags {
pub qol: bool,
pub gameplay: bool,
pub audio: bool,
pub visual: bool,
pub framework: bool,
pub versions: BTreeSet<String>,
pub required_status: RequiredStatus,
pub approval_status: ApprovalStatus,
}

#[derive(Debug, Copy, Clone)]
pub enum RequiredStatus {
RequiredByAll,
Optional,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ApprovalStatus {
Verified,
Approved,
Sandbox,
}

/// Whether a mod can be resolved by clients or not
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub enum ResolvableStatus {
Unresolvable(String),
Resolvable,
}

/// Returned from ModStore
#[derive(Debug, Clone)]
pub struct ModInfo {
pub provider: &'static str,
pub name: String,
pub spec: ModSpecification, // unpinned version
pub versions: Vec<ModSpecification>, // pinned versions TODO make this a different type
pub resolution: ModResolution,
pub suggested_require: bool,
pub suggested_dependencies: Vec<ModSpecification>, // ModResponse
pub modio_tags: Option<ModioTags>, // only available for mods from mod.io
pub modio_id: Option<u32>, // only available for mods from mod.io
}

/// Returned from ModProvider
#[derive(Debug, Clone)]
pub enum ModResponse {
Redirect(ModSpecification),
Resolve(ModInfo),
}

/// Points to a mod, optionally a specific version
#[derive(
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct ModSpecification {
pub url: String,
}

impl ModSpecification {
pub fn new(url: String) -> Self {
Self { url }
}
pub fn satisfies_dependency(&self, other: &ModSpecification) -> bool {
// TODO this hack works surprisingly well but is still a complete hack and should be replaced
self.url.starts_with(&other.url) || other.url.starts_with(&self.url)
}
}

/// Points to a specific version of a specific mod
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub struct ModResolution {
pub url: String,
pub status: ResolvableStatus,
}

impl ModResolution {
pub fn resolvable(url: String) -> Self {
Self {
url,
status: ResolvableStatus::Resolvable,
}
}
pub fn unresolvable(url: String, name: String) -> Self {
Self {
url,
status: ResolvableStatus::Unresolvable(name),
}
}
/// Used to get the URL if resolvable or just return the mod name if not
pub fn get_resolvable_url_or_name(&self) -> &str {
match &self.status {
ResolvableStatus::Resolvable => &self.url,
ResolvableStatus::Unresolvable(name) => name,
}
}
}

/// Stripped down mod info stored in the mod pak to be used in game
#[derive(Debug, Serialize, Deserialize)]
pub struct Meta {
pub version: String,
pub mods: Vec<MetaMod>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MetaMod {
pub name: String,
pub approval: ApprovalStatus,
}
impl Meta {
pub fn to_server_list_string(&self) -> String {
use itertools::Itertools;

["mint".into(), self.version.clone()]
.into_iter()
.chain(
self.mods
.iter()
.sorted_by_key(|m| (std::cmp::Reverse(m.approval), &m.name))
.flat_map(|m| {
[
match m.approval {
ApprovalStatus::Verified => 'V',
ApprovalStatus::Approved => 'A',
ApprovalStatus::Sandbox => 'S',
}
.into(),
m.name.replace(';', ""),
]
}),
)
.join(";")
}
}
Loading

0 comments on commit e6d3ebd

Please sign in to comment.