1pub mod collision;
5pub mod manifest;
6pub mod redmod;
7pub mod saves;
8pub mod scanner;
9
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13
14use smallvec::SmallVec;
15
16use crate::policies::{BareLayoutPolicy, ContentPolicy, DllOverridePolicy, StagingDllSearch};
17use crate::traits::{ContentCategory, GamePlugin, ModSafety};
18
19pub struct Cyberpunk2077;
21
22pub static CYBERPUNK2077: Cyberpunk2077 = Cyberpunk2077;
23
24const CYBERPUNK_SAVE_BREAKING_EXT: &[&str] = &["reds", "lua", "tweak", "xl", "yaml", "yls"];
26
27const CYBERPUNK_SAVE_BREAKING_DIRS: &[&str] = &[
29 "r6/scripts",
30 "r6/tweaks",
31 "bin/x64/plugins/cyber_engine_tweaks/mods",
32];
33
34const CYBERPUNK_COSMETIC_EXT: &[&str] = &["archive", "xl", "png", "jpg", "dds", "tga", "ini"];
36
37const CYBERPUNK_CONTENT_CATEGORIES: &[(&str, ContentCategory)] = &[
38 ("archive", ContentCategory::Archive),
39 ("dll", ContentCategory::Binary),
40 ("so", ContentCategory::Binary),
41 ("reds", ContentCategory::Script),
42 ("lua", ContentCategory::Script),
43 ("tweak", ContentCategory::Script),
44 ("xl", ContentCategory::Script),
45 ("yaml", ContentCategory::Config),
46 ("yls", ContentCategory::Config),
47 ("yml", ContentCategory::Config),
48 ("ini", ContentCategory::Config),
49 ("json", ContentCategory::Config),
50 ("toml", ContentCategory::Config),
51 ("xml", ContentCategory::Config),
52 ("dds", ContentCategory::Texture),
53 ("png", ContentCategory::Texture),
54 ("tga", ContentCategory::Texture),
55 ("jpg", ContentCategory::Texture),
56];
57
58const CYBERPUNK_CONTENT_POLICY: ContentPolicy = ContentPolicy {
59 save_breaking_ext: CYBERPUNK_SAVE_BREAKING_EXT,
60 cosmetic_ext: CYBERPUNK_COSMETIC_EXT,
61 save_breaking_dirs: CYBERPUNK_SAVE_BREAKING_DIRS,
62 categories: CYBERPUNK_CONTENT_CATEGORIES,
63};
64
65const KNOWN_PROXY_DLLS: &[&str] = &[
69 "version", "winmm", "dinput8", "d3d11", "dxgi", "winhttp", "xinput1_3", ];
77
78const CYBERPUNK_DLL_POLICY: DllOverridePolicy = DllOverridePolicy {
79 proxy_dlls: KNOWN_PROXY_DLLS,
80 staging_search: StagingDllSearch::NestedModsBinX64,
81};
82
83const CYBERPUNK_BARE_LAYOUT_POLICY: BareLayoutPolicy = BareLayoutPolicy {
84 root_dirs: &[
85 "r6", "archive", "archives", "bin", "engine", "mods", "red4ext",
86 ],
87 root_file_exts: &[],
88 case_insensitive_dirs: false,
89};
90
91impl GamePlugin for Cyberpunk2077 {
92 fn game_id(&self) -> &'static str {
93 "cyberpunk2077"
94 }
95
96 fn display_name(&self) -> &'static str {
97 "Cyberpunk 2077"
98 }
99
100 fn mod_directory(&self, install: &Path) -> PathBuf {
101 install.join("mods")
102 }
103
104 fn deploy(&self, staging: &Path, target: &Path) -> Result<()> {
105 if !target.exists() {
106 std::fs::create_dir_all(target)
107 .with_context(|| format!("failed to create {}", target.display()))?;
108 }
109 for entry in std::fs::read_dir(staging)
111 .with_context(|| format!("failed to read directory: {}", staging.display()))?
112 {
113 let entry = entry?;
114 let dst = target.join(entry.file_name());
115 if dst.exists() || dst.symlink_metadata().is_ok() {
116 if dst.is_dir() {
117 std::fs::remove_dir_all(&dst)
118 .with_context(|| format!("failed to remove {}", dst.display()))?;
119 } else {
120 std::fs::remove_file(&dst)
121 .with_context(|| format!("failed to remove {}", dst.display()))?;
122 }
123 }
124 modde_core::fs::symlink(&entry.path(), &dst)?;
125 }
126 Ok(())
127 }
128
129 fn post_deploy(&self, install: &Path) -> Result<()> {
130 redmod::deploy_if_available(install)
132 }
133
134 fn save_directory(&self) -> Option<PathBuf> {
135 let save_suffix = "pfx/drive_c/users/steamuser/Saved Games/CD Projekt Red/Cyberpunk 2077";
136
137 let heroic_prefixes =
139 modde_core::paths::home_dir().join("Games/Heroic/Prefixes/default/Cyberpunk 2077");
140 let heroic_path = heroic_prefixes.join(save_suffix);
141 if heroic_path.exists() {
142 return Some(heroic_path);
143 }
144
145 let compat = modde_core::paths::steam_common()
147 .parent()? .join("compatdata/1091500")
149 .join(save_suffix);
150 if compat.exists() {
151 return Some(compat);
152 }
153 None
154 }
155
156 fn supports_save_profiles(&self) -> bool {
157 true
158 }
159
160 fn classify_mod(&self, mod_dir: &Path) -> ModSafety {
161 CYBERPUNK_CONTENT_POLICY.classify_mod(mod_dir)
162 }
163
164 fn classify_extension(&self, ext: &str) -> ContentCategory {
165 CYBERPUNK_CONTENT_POLICY.classify_extension(ext)
166 }
167
168 fn wine_dll_overrides(&self, game_dir: &Path) -> SmallVec<[String; 4]> {
169 CYBERPUNK_DLL_POLICY.from_executable_dir(&self.executable_dir(game_dir))
170 }
171
172 fn wine_dll_overrides_from_staging(&self, staging: &Path) -> SmallVec<[String; 4]> {
173 CYBERPUNK_DLL_POLICY.from_staging(staging)
174 }
175
176 fn executable_dir(&self, install: &Path) -> PathBuf {
177 install.join("bin").join("x64")
178 }
179
180 fn archive_extensions(&self) -> &[&str] {
181 &["archive"]
182 }
183
184 fn steam_app_id_u32(&self) -> Option<u32> {
185 Some(1091500)
186 }
187
188 fn nexus_game_domain(&self) -> Option<&str> {
189 Some("cyberpunk2077")
190 }
191
192 fn nexus_game_id_u32(&self) -> Option<u32> {
193 Some(3333)
196 }
197
198 fn analyze_mod_archive(
199 &self,
200 extracted_dir: &Path,
201 ) -> Option<modde_core::installer::InstallMethod> {
202 let info_json = extracted_dir.join("info.json");
206 if !info_json.is_file() {
207 return None;
208 }
209 let archives = extracted_dir.join("archives");
210 let archive = extracted_dir.join("archive");
211 if archives.is_dir() || archive.is_dir() {
212 return Some(modde_core::installer::InstallMethod::REDmod {
213 manifest: PathBuf::from("info.json"),
214 });
215 }
216 None
217 }
218
219 fn recognizes_bare_layout(&self, extracted_dir: &Path) -> bool {
220 CYBERPUNK_BARE_LAYOUT_POLICY.recognizes(extracted_dir)
225 }
226}