nitai/projects
noxstrap / src / crates / nox-roblox / tests / mods_and_tweaks.rs
190 lines · 7.3 KB Raw
1use nox_roblox::mods;
2use nox_roblox::tweaks;
3use nox_roblox::tweaks::Choices;
4
5fn tmp(name: &str) -> std::path::PathBuf {
6 let p = std::env::temp_dir().join(format!("nox-mods-{name}"));
7 let _ = std::fs::remove_dir_all(&p);
8 std::fs::create_dir_all(&p).unwrap();
9 p
10}
11
12#[test]
13fn scan_walks_nested_folders() {
14 let dir = tmp("scan");
15 std::fs::create_dir_all(dir.join("content/textures/Cursors")).unwrap();
16 std::fs::create_dir_all(dir.join("content/sounds")).unwrap();
17 std::fs::write(dir.join("content/textures/Cursors/a.png"), b"12345").unwrap();
18 std::fs::write(dir.join("content/sounds/b.ogg"), b"xy").unwrap();
19 std::fs::write(dir.join("top.txt"), b"").unwrap();
20
21 let files = mods::scan(&dir);
22 assert_eq!(files.len(), 3);
23 let names: Vec<String> = files.iter().map(|f| f.display()).collect();
24 assert!(names.contains(&"content/textures/Cursors/a.png".to_string()));
25 assert!(names.contains(&"content/sounds/b.ogg".to_string()));
26 assert!(names.contains(&"top.txt".to_string()));
27 assert_eq!(mods::total_bytes(&files), 7);
28 // Sorted, so the list does not reshuffle between scans.
29 let again: Vec<String> = mods::scan(&dir).iter().map(|f| f.display()).collect();
30 assert_eq!(names, again);
31}
32
33#[test]
34fn scan_of_a_missing_folder_is_empty_not_an_error() {
35 let files = mods::scan(std::path::Path::new("/definitely/not/here"));
36 assert!(files.is_empty());
37}
38
39#[test]
40fn apply_copies_over_the_client_preserving_layout() {
41 let src = tmp("apply-src");
42 let dst = tmp("apply-dst");
43 std::fs::create_dir_all(src.join("content/textures")).unwrap();
44 std::fs::write(src.join("content/textures/cursor.png"), b"NEW").unwrap();
45 std::fs::write(src.join("loose.dll"), b"DLL").unwrap();
46
47 // One target already exists and must be overwritten, the other is new.
48 std::fs::create_dir_all(dst.join("content/textures")).unwrap();
49 std::fs::write(dst.join("content/textures/cursor.png"), b"ORIGINAL-LONGER").unwrap();
50
51 let applied = mods::apply(&src, &dst);
52 assert_eq!(applied.copied, 2);
53 assert!(applied.failed.is_empty(), "{:?}", applied.failed);
54 assert_eq!(std::fs::read(dst.join("content/textures/cursor.png")).unwrap(), b"NEW");
55 assert_eq!(std::fs::read(dst.join("loose.dll")).unwrap(), b"DLL");
56}
57
58#[test]
59fn scaffold_creates_the_suggested_layout() {
60 let dir = tmp("scaffold");
61 mods::scaffold(&dir).unwrap();
62 for s in mods::SUGGESTED {
63 assert!(dir.join(s).is_dir(), "{s} not created");
64 }
65 // Empty folders are not files, so nothing is reported as a mod yet.
66 assert!(mods::scan(&dir).is_empty());
67}
68
69#[test]
70fn tweaks_expand_into_flags() {
71 let flags = tweaks::merge(&["uncap_fps".into()], "Automatic", Choices::default(), &[]);
72 assert_eq!(flags, vec![("DFIntTaskSchedulerTargetFps".to_string(), "9999".to_string())]);
73
74 let none = tweaks::merge(&[], "Automatic", Choices::default(), &[]);
75 assert!(none.is_empty(), "nothing enabled should write nothing");
76
77 let unknown = tweaks::merge(&["not-a-tweak".into()], "Automatic", Choices::default(), &[]);
78 assert!(unknown.is_empty(), "unknown ids are ignored, not fatal");
79}
80
81#[test]
82fn render_modes_and_quality_contribute() {
83 let vk = tweaks::merge(&[], "Vulkan", Choices::default(), &[]);
84 assert!(vk.iter().any(|(k, v)| k == "FFlagDebugGraphicsPreferVulkan" && v == "true"));
85 let q = tweaks::merge(&[], "Automatic", Choices { quality: 7, ..Default::default() }, &[]);
86 assert!(q.iter().any(|(k, v)| k == "DFIntDebugFRMQualityLevelOverride" && v == "7"));
87 let auto = tweaks::merge(&[], "Automatic", Choices::default(), &[]);
88 assert!(auto.is_empty());
89 // An unrecognised renderer name falls back to automatic rather than breaking.
90 assert!(tweaks::merge(&[], "Glide", Choices::default(), &[]).is_empty());
91}
92
93#[test]
94fn custom_flags_win_over_tweaks() {
95 let flags = tweaks::merge(
96 &["uncap_fps".into()],
97 "Automatic",
98 Choices::default(),
99 &[("DFIntTaskSchedulerTargetFps".into(), "144".into())],
100 );
101 assert_eq!(flags.len(), 1, "the key must not be duplicated");
102 assert_eq!(flags[0].1, "144", "the custom value must win");
103}
104
105#[test]
106fn every_tweak_is_well_formed() {
107 let mut seen = std::collections::HashSet::new();
108 for t in tweaks::TWEAKS {
109 assert!(seen.insert(t.id), "duplicate tweak id {}", t.id);
110 assert!(!t.label.is_empty() && !t.detail.is_empty(), "{} lacks text", t.id);
111 assert!(!t.flags.is_empty(), "{} sets no flags", t.id);
112 for (k, _) in t.flags {
113 assert!(
114 k.starts_with("FFlag") || k.starts_with("DFFlag") || k.starts_with("FInt")
115 || k.starts_with("DFInt") || k.starts_with("FString") || k.starts_with("DFString"),
116 "{k} does not look like an engine flag"
117 );
118 }
119 assert!(tweaks::find(t.id).is_some());
120 }
121 assert!(seen.len() >= 8);
122}
123
124#[test]
125fn msaa_and_texture_choices_expand_correctly() {
126 // Only the sample counts the engine understands produce a flag.
127 for bad in [0u8, 3, 5, 8, 255] {
128 assert!(tweaks::msaa_flags(bad).is_empty(), "{bad} should not set a flag");
129 }
130 for good in [1u8, 2, 4] {
131 let f = tweaks::msaa_flags(good);
132 assert_eq!(f.len(), 1);
133 assert_eq!(f[0].0, "FIntDebugForceMSAASamples");
134 assert_eq!(f[0].1, good.to_string());
135 }
136
137 // The texture override needs switching on as well as given a level.
138 assert!(tweaks::texture_flags(-1).is_empty());
139 assert!(tweaks::texture_flags(4).is_empty());
140 for level in 0..=3i8 {
141 let f = tweaks::texture_flags(level);
142 assert_eq!(f.len(), 2, "level {level} needs the enable flag too");
143 assert!(f.iter().any(|(k, v)| k == "DFFlagTextureQualityOverrideEnabled" && v == "true"));
144 assert!(f.iter().any(|(k, v)| k == "DFIntTextureQualityOverride" && *v == level.to_string()));
145 }
146}
147
148#[test]
149fn choices_reach_the_merged_output() {
150 let flags = tweaks::merge(
151 &[],
152 "Automatic",
153 Choices { quality: 5, msaa: 4, texture: 2 },
154 &[],
155 );
156 assert!(flags.iter().any(|(k, v)| k == "DFIntDebugFRMQualityLevelOverride" && v == "5"));
157 assert!(flags.iter().any(|(k, v)| k == "FIntDebugForceMSAASamples" && v == "4"));
158 assert!(flags.iter().any(|(k, v)| k == "DFIntTextureQualityOverride" && v == "2"));
159 assert_eq!(flags.len(), 4, "quality + msaa + two texture flags");
160}
161
162#[test]
163fn every_tweak_belongs_to_a_declared_category() {
164 for t in tweaks::TWEAKS {
165 assert!(
166 tweaks::CATEGORIES.contains(&t.category),
167 "{} has category {:?}, which is not in CATEGORIES",
168 t.id,
169 t.category
170 );
171 }
172 // Every category should actually be used, or it is dead UI.
173 for c in tweaks::CATEGORIES {
174 assert!(tweaks::TWEAKS.iter().any(|t| t.category == c), "{c} has no tweaks");
175 }
176}
177
178#[test]
179fn default_choices_change_nothing() {
180 // Zero is a valid texture level, so the "off" sentinel has to be negative.
181 // A derived Default would pin every fresh install to the lowest detail.
182 let d = Choices::default();
183 assert!(d.texture < 0, "default texture must mean 'leave it alone'");
184 assert_eq!(d.quality, 0);
185 assert_eq!(d.msaa, 0);
186 assert!(
187 tweaks::merge(&[], "Automatic", d, &[]).is_empty(),
188 "a default configuration must write no flags at all"
189 );
190}