modde_games/bethesda/
diagnostics.rs1use std::path::PathBuf;
6
7use modde_core::diagnostics::{
8 DiagContext, DiagFix, Diagnostic, DiagnosticEngine, DiagnosticRule, Severity,
9};
10
11use super::plugin_header::{self, PluginWarning};
12
13pub struct MissingMasterRule;
15
16impl DiagnosticRule for MissingMasterRule {
17 fn name(&self) -> &'static str {
18 "missing-masters"
19 }
20
21 fn check(&self, ctx: &DiagContext) -> Vec<Diagnostic> {
22 let plugin_dir = ctx.staging_dir;
23
24 let active_plugins: Vec<&str> = collect_active_plugins(ctx);
25 if active_plugins.is_empty() {
26 return Vec::new();
27 }
28
29 let warnings = plugin_header::validate_plugins(plugin_dir, &active_plugins, false);
30
31 warnings
32 .into_iter()
33 .filter_map(|w| match w {
34 PluginWarning::MissingMaster { plugin, master } => Some(Diagnostic {
35 severity: Severity::Error,
36 title: format!("Missing master: {master}"),
37 detail: format!(
38 "Plugin '{plugin}' requires master '{master}' which is not in the load order. \
39 The game will crash on load."
40 ),
41 affected_mod: Some(plugin),
42 affected_file: Some(PathBuf::from(&master)),
43 fix: Some(DiagFix {
44 label: "Install missing master".to_string(),
45 description: format!("Install the mod that provides '{master}' and enable it."),
46 }),
47 }),
48 _ => None,
49 })
50 .collect()
51 }
52}
53
54pub struct Form43Rule;
56
57impl DiagnosticRule for Form43Rule {
58 fn name(&self) -> &'static str {
59 "form-43"
60 }
61
62 fn check(&self, ctx: &DiagContext) -> Vec<Diagnostic> {
63 if ctx.game_id != "skyrim-se" && ctx.game_id != "skyrim-ae" {
65 return Vec::new();
66 }
67
68 let plugin_dir = ctx.staging_dir;
69
70 let active_plugins: Vec<&str> = collect_active_plugins(ctx);
71 if active_plugins.is_empty() {
72 return Vec::new();
73 }
74
75 let warnings = plugin_header::validate_plugins(plugin_dir, &active_plugins, true);
76
77 warnings
78 .into_iter()
79 .filter_map(|w| match w {
80 PluginWarning::Form43 { plugin, version } => Some(Diagnostic {
81 severity: Severity::Warning,
82 title: format!("Form 43 plugin: {plugin}"),
83 detail: format!(
84 "Plugin '{plugin}' uses Form 43 (v{version:.2}), the Oldrim format. \
85 This can cause crashes in Skyrim SE/AE. Resave it in Creation Kit."
86 ),
87 affected_mod: Some(plugin),
88 affected_file: None,
89 fix: Some(DiagFix {
90 label: "Resave in Creation Kit".to_string(),
91 description: "Open the plugin in Creation Kit (SSE) and save it to convert to Form 44.".to_string(),
92 }),
93 }),
94 _ => None,
95 })
96 .collect()
97 }
98}
99
100pub use modde_core::diagnostics::StorePresenceRule as EmptyModRule;
101
102pub struct OrphanedOverridesRule;
104
105impl DiagnosticRule for OrphanedOverridesRule {
106 fn name(&self) -> &'static str {
107 "orphaned-overrides"
108 }
109
110 fn check(&self, ctx: &DiagContext) -> Vec<Diagnostic> {
111 let overrides_dir = &ctx.profile.overrides;
112 if !overrides_dir.exists() {
113 return Vec::new();
114 }
115
116 let has_files = match std::fs::read_dir(overrides_dir) {
117 Ok(mut entries) => entries.next().is_some(),
118 Err(_) => false,
119 };
120
121 if has_files {
122 vec![Diagnostic {
123 severity: Severity::Info,
124 title: "Overrides directory has files".to_string(),
125 detail: format!(
126 "The overrides directory '{}' contains files. \
127 These files take highest priority and override all mods. \
128 Review them to ensure they are intentional.",
129 overrides_dir.display()
130 ),
131 affected_mod: None,
132 affected_file: Some(overrides_dir.clone()),
133 fix: None,
134 }]
135 } else {
136 Vec::new()
137 }
138 }
139}
140
141#[must_use]
143pub fn bethesda_diagnostics() -> DiagnosticEngine {
144 let mut engine = modde_core::diagnostics::base_diagnostics();
145 engine.add_rule(Box::new(MissingMasterRule));
146 engine.add_rule(Box::new(Form43Rule));
147 engine.add_rule(Box::new(OrphanedOverridesRule));
148 engine
149}
150
151fn collect_active_plugins<'a>(ctx: &'a DiagContext<'a>) -> Vec<&'a str> {
155 ctx.active_plugins.iter().map(String::as_str).collect()
156}