pub mod saves;
pub mod scanner;
use std::path::{Path, PathBuf};
use modde_core::collision::CollisionSeverity;
use modde_core::installer::InstallMethod;
use modde_core::paths;
use smallvec::SmallVec;
use crate::optiscaler::OptiScalerProfile;
use crate::policies::{CollisionPolicy, ContentPolicy, DllOverridePolicy, StagingDllSearch};
use crate::traits::{ContentCategory, DeployTarget, DeployTargetKind, GamePlugin, ModSafety};
pub struct Ue4Game {
game_id: &'static str,
display_name: &'static str,
steam_app_id: &'static str,
project_name: &'static str,
nexus_domain: Option<&'static str>,
save_profiles: bool,
}
impl Ue4Game {
#[must_use]
pub const fn new(
game_id: &'static str,
display_name: &'static str,
steam_app_id: &'static str,
project_name: &'static str,
nexus_domain: Option<&'static str>,
) -> Self {
Self {
game_id,
display_name,
steam_app_id,
project_name,
nexus_domain,
save_profiles: false,
}
}
#[must_use]
pub const fn with_save_profiles(mut self, enabled: bool) -> Self {
self.save_profiles = enabled;
self
}
#[must_use]
pub fn paks_root(&self, install: &Path) -> PathBuf {
install.join(self.project_name).join("Content").join("Paks")
}
#[must_use]
pub fn project_name(&self) -> &'static str {
self.project_name
}
}
const UE4_PROXY_DLLS: &[&str] = &[
"dwmapi", "xinput1_3", "d3d11", "dxgi", "version", "winmm", "dinput8", ];
const UE4_SAVE_BREAKING_EXT: &[&str] = &["pak", "ucas", "utoc", "dll", "lua"];
const UE4_COSMETIC_EXT: &[&str] = &["png", "jpg", "dds", "tga", "ini"];
const UE4_CONTENT_CATEGORIES: &[(&str, ContentCategory)] = &[
("pak", ContentCategory::Archive),
("ucas", ContentCategory::Archive),
("utoc", ContentCategory::Archive),
("dll", ContentCategory::Binary),
("so", ContentCategory::Binary),
("lua", ContentCategory::Script),
("dds", ContentCategory::Texture),
("png", ContentCategory::Texture),
("tga", ContentCategory::Texture),
("jpg", ContentCategory::Texture),
("ini", ContentCategory::Config),
("json", ContentCategory::Config),
("yaml", ContentCategory::Config),
("xml", ContentCategory::Config),
("toml", ContentCategory::Config),
];
const UE4_CONTENT_POLICY: ContentPolicy = ContentPolicy {
save_breaking_ext: UE4_SAVE_BREAKING_EXT,
cosmetic_ext: UE4_COSMETIC_EXT,
save_breaking_dirs: &[],
categories: UE4_CONTENT_CATEGORIES,
};
const UE4_DLL_POLICY: DllOverridePolicy = DllOverridePolicy {
proxy_dlls: UE4_PROXY_DLLS,
staging_search: StagingDllSearch::DirectChildDirs,
};
pub(crate) const UE4_COLLISION_SEVERITIES: &[(&str, CollisionSeverity)] = &[
("pak", CollisionSeverity::Dangerous),
("ucas", CollisionSeverity::Dangerous),
("utoc", CollisionSeverity::Dangerous),
("dll", CollisionSeverity::Dangerous),
("lua", CollisionSeverity::Dangerous),
("ini", CollisionSeverity::Config),
("cfg", CollisionSeverity::Config),
("json", CollisionSeverity::Config),
("toml", CollisionSeverity::Config),
("xml", CollisionSeverity::Config),
("yaml", CollisionSeverity::Config),
("dds", CollisionSeverity::Cosmetic),
("png", CollisionSeverity::Cosmetic),
("jpg", CollisionSeverity::Cosmetic),
("tga", CollisionSeverity::Cosmetic),
];
pub(crate) const UE4_COLLISION_POLICY: CollisionPolicy = CollisionPolicy {
archive_extensions: &["pak", "ucas", "utoc"],
severities: UE4_COLLISION_SEVERITIES,
};
pub const STELLAR_BLADE: Ue4Game = Ue4Game::new(
"stellar-blade",
"Stellar Blade",
"3489700",
"SB",
Some("stellarblade"),
)
.with_save_profiles(true);
pub const SUBNAUTICA2: Ue4Game = Ue4Game::new(
"subnautica2",
"Subnautica 2",
"1962700",
"Subnautica2",
Some("subnautica2"),
)
.with_save_profiles(true);
pub(crate) const STELLAR_BLADE_OPTISCALER_PROFILES: &[OptiScalerProfile] = &[OptiScalerProfile {
id: "community-dxgi",
name: "Community tested dxgi.dll",
source_url: "https://github.com/optiscaler/OptiScaler/wiki/Stellar-Blade",
tested_optiscaler_version: "0.9",
source_mode: Some("github_release"),
goverlay_channel: None,
proxy_dll: "dxgi.dll",
release_tag: Some("official:v0.9.1"),
release_asset: None,
wine_dll_overrides: &[],
copy_companion_files: true,
enable_optipatcher: true,
fsr4_variant: Some("latest_fp8"),
emulate_fp8: true,
spoof_dlss: false,
ini_overrides: &[],
notes: "Use OptiPatcher to unlock DLSS and DLSS-FG inputs without spoofing. The community compatibility notes report that the game may crash on first boot but work afterwards, and that setting the in-game sharpness slider to 0 can fix DLSSG HUD interpolation.",
}];
impl GamePlugin for Ue4Game {
fn game_id(&self) -> &str {
self.game_id
}
fn display_name(&self) -> &str {
self.display_name
}
fn mod_directory(&self, install: &Path) -> PathBuf {
self.paks_root(install).join("~mods")
}
fn save_directory(&self) -> Option<PathBuf> {
let compat = paths::steam_common()
.parent()?
.join("compatdata")
.join(self.steam_app_id)
.join("pfx/drive_c/users/steamuser/AppData/Local")
.join(self.project_name)
.join("Saved/SaveGames");
if compat.exists() {
return Some(compat);
}
None
}
fn supports_save_profiles(&self) -> bool {
self.save_profiles
}
fn deploy_targets(&self) -> &'static [DeployTarget] {
&[DeployTarget {
id: "ue4-saved-config",
label: "UE4 Saved/Config",
kind: DeployTargetKind::UserConfig,
}]
}
fn resolve_deploy_target(&self, id: &str, _install: &Path) -> Option<PathBuf> {
if id != "ue4-saved-config" {
return None;
}
let prefix = paths::steam_common()
.parent()?
.join("compatdata")
.join(self.steam_app_id)
.join("pfx");
if !prefix.exists() {
return None;
}
Some(
prefix
.join("drive_c/users/steamuser/AppData/Local")
.join(self.project_name)
.join("Saved/Config/Windows"),
)
}
fn executable_dir(&self, install: &Path) -> PathBuf {
install
.join(self.project_name)
.join("Binaries")
.join("Win64")
}
fn wine_dll_overrides(&self, game_dir: &Path) -> SmallVec<[String; 4]> {
UE4_DLL_POLICY.from_executable_dir(&self.executable_dir(game_dir))
}
fn wine_dll_overrides_from_staging(&self, staging: &Path) -> SmallVec<[String; 4]> {
UE4_DLL_POLICY.from_staging(staging)
}
fn classify_mod(&self, mod_dir: &Path) -> ModSafety {
UE4_CONTENT_POLICY.classify_mod(mod_dir)
}
fn classify_extension(&self, ext: &str) -> ContentCategory {
UE4_CONTENT_POLICY.classify_extension(ext)
}
fn archive_extensions(&self) -> &[&str] {
&["pak", "ucas", "utoc"]
}
fn steam_app_id_u32(&self) -> Option<u32> {
self.steam_app_id.parse().ok()
}
fn nexus_game_domain(&self) -> Option<&str> {
self.nexus_domain
}
fn analyze_mod_archive(&self, extracted_dir: &Path) -> Option<InstallMethod> {
has_root_file_with_ext(extracted_dir, &["pak", "ucas", "utoc"])
.then_some(InstallMethod::SingleFileSet)
}
}
fn has_root_file_with_ext(dir: &Path, extensions: &[&str]) -> bool {
std::fs::read_dir(dir).is_ok_and(|entries| {
entries.flatten().any(|entry| {
let path = entry.path();
path.is_file()
&& path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
extensions
.iter()
.any(|candidate| ext.eq_ignore_ascii_case(candidate))
})
})
})
}