| 1 | use nox_roblox::mods; |
| 2 | use nox_roblox::tweaks; |
| 3 | use nox_roblox::tweaks::Choices; |
| 4 | |
| 5 | fn 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] |
| 13 | fn 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] |
| 34 | fn 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] |
| 40 | fn 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] |
| 59 | fn 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] |
| 70 | fn 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] |
| 82 | fn 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] |
| 94 | fn 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] |
| 106 | fn 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] |
| 125 | fn 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] |
| 149 | fn 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] |
| 163 | fn 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] |
| 179 | fn 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 | } |