1use serde::{Deserialize, Serialize};
2use anyhow::{anyhow, Result};
3use anyhow::Error;
4use std::{ffi::OsStr, fs, fs::File, io::{Read, Write}, path::{Path, PathBuf}};
5
6const ALLOWED_GAMES: [&str; 3] = ["EM1", "EM2", "EMR"];
7const ALLOWED_PLATFORMS: [&str; 2] = ["WII", "PC"];
8const BANNED_EXTENSIONS: [&str; 6] = ["dll", "so", "exe", "sh", "bat", "scr"]; const BANNED_PAK_FILES: [&str; 5] = [
13 "global.utoc",
14 "global.ucas",
15 "recolored-WindowsNoEditor.pak",
16 "recolored-WindowsNoEditor.ucas",
17 "recolored-WindowsNoEditor.utoc",
18];
19pub fn validate(path: &PathBuf, strict: bool) -> Result<ModInfo, Error> {
20 let mut final_mod_info: ModInfo = ModInfo {
21 name: "".to_string(),
22 game: "".to_string(),
23 platform: "".to_string(),
24 description: "".to_string(),
25 short_description: "".to_string(),
26 dependencies: Vec::new(),
27 custom_textures_path: "".to_string(),
28 custom_game_files_path: "".to_string(),
29 scripts_path: "".to_string(),
30 icon_path: "".to_string(),
31 auto_generated_tags: Vec::new(),
32 };
33 println!("{}", &path.display());
34 let mut mod_info_path = path.clone();
35 mod_info_path.push("mod.json");
36
37 let mut mod_description_path = path.clone();
38 mod_description_path.push("description.md");
39
40 if !mod_info_path.exists() {
41 return Err(anyhow!("mod.json does not exist."));
42 }
43 let mut mod_info_file = File::open(mod_info_path)?;
44 let mut mod_info_buffer = String::new();
45 mod_info_file.read_to_string(&mut mod_info_buffer)?;
46 let mod_info: serde_json::Map<String, serde_json::Value> =
47 serde_json::from_str(&mod_info_buffer)?;
48
49 let name = mod_info.get("name").unwrap().as_str().unwrap();
50 println!("{}", name);
51 if name.trim().is_empty() {
52 return Err(anyhow!("mod name is empty."));
53 }
54
55 final_mod_info.name = name.to_string();
56
57 let short_description_value = mod_info.get("shortdescription");
58 let mut no_short_description = false;
59
60 match short_description_value {
61 Some(x) => {
62 let short_description = x.as_str().unwrap().trim().to_string();
63 final_mod_info.short_description = short_description;
64 }
65 None => no_short_description = true,
66 }
67
68 if mod_description_path.exists() {
69 let mut mod_description_file = File::open(mod_description_path)?;
70 let mut mod_description = String::new();
71
72 mod_description_file.read_to_string(&mut mod_description)?;
73
74 if mod_description.trim().is_empty() {
75 return Err(anyhow!("mod description is empty."));
76 }
77
78 final_mod_info.description = mod_description.trim().to_string();
79
80 if no_short_description {
81 final_mod_info.short_description = "clone".to_string();
82 }
83 }
84
85 let mut game = match mod_info.get("game") {
86 Some(x) => x.as_str().unwrap().to_string().to_uppercase(),
87 None => {
88 "".to_string()
89 }
90 };
91 let mut platform = match mod_info.get("platform") {
92 Some(x) => x.as_str().unwrap().to_string().to_uppercase(),
93 None => {
94 "".to_string()
95 }
96 };
97
98 if platform.trim().is_empty() {
99 if strict {
100 return Err(anyhow!("mod platform is empty."));
101 }
102 else {
103 platform = "WII".to_string();
104 }
105 }
106
107 if game.trim().is_empty() {
108 return Err(anyhow!("mod game type is empty."));
109 }
110
111 println!("{}", platform);
112
113 final_mod_info.game = game.to_string();
114 final_mod_info.platform = platform.to_string();
115
116 println!("{}", game);
117
118 if !ALLOWED_GAMES.contains(&game.as_str()) {
119 return Err(anyhow!("could not recognize defined game."));
120 }
121
122 if !ALLOWED_PLATFORMS.contains(&platform.as_str()) {
123 return Err(anyhow!("could not recognize defined platform."));
124 }
125
126 if game.to_string() == "EMR" && platform.to_string() == "WII" {
127 return Err(anyhow!("impossible combination (emr/wii)"));
128 }
129
130 if game.to_string() == "EM1" && platform.to_string() == "PC" {
131 return Err(anyhow!("impossible combination (em1/pc)"));
132 }
133
134 let mut no_custom_textures = false;
135 let mut no_custom_files = false;
136 let mut no_scripts = false;
137
138 let custom_textures_path = match mod_info.get("custom_textures_path") {
139 Some(x) => x.as_str().unwrap().to_string(),
140 None => {
141 no_custom_textures = true;
142 "".to_string()
143 }
144 };
145
146 let custom_game_files_path = match mod_info.get("custom_game_files_path") {
147 Some(x) => x.as_str().unwrap().to_string(),
148 None => {
149 no_custom_files = true;
150 "".to_string()
151 }
152 };
153
154 let scripts_path = match mod_info.get("scripts_path") {
155 Some(x) => x.as_str().unwrap().to_string(),
156 None => {
157 no_scripts = true;
158 "".to_string()
159 }
160 };
161
162 if strict {
163 if platform == "PC" && !no_custom_textures {
164 return Err(anyhow!("custom textures not allowed on pc."));
165 }
166
167 if (platform != "PC" || game != "EMR") && !no_scripts {
168 return Err(anyhow!("custom scripts only available with EMR"));
169 }
170 }
171 else {
172 if platform == "PC" && !no_custom_textures {
173 no_custom_textures = true
174 }
175
176 if (platform != "PC" || game != "EMR") && !no_scripts {
177 no_scripts = true;
178 }
179 }
180
181 final_mod_info.scripts_path = scripts_path.clone();
182 final_mod_info.custom_textures_path = custom_textures_path.clone();
183 final_mod_info.custom_game_files_path = custom_game_files_path.clone();
184
185 if !no_custom_files {
186 if PathBuf::from(&custom_game_files_path).is_absolute() {
187 return Err(anyhow!("you are not allowed to have absolute paths on custom file path."));
188 }
189
190 if strict {
191 if !PathBuf::from(&path).join(&custom_game_files_path).exists() {
192 return Err(anyhow!("custom game files path does not exist."));
193 }
194 if custom_game_files_path.trim().is_empty() {
195 return Err(anyhow!("custom game files path is empty."));
196 }
197 }
198
199 let pak_path = PathBuf::from(&path).join(custom_game_files_path).join("Paks");
200
201 if platform == "PC" && game == "EMR" && pak_path.exists() {
202 for pak in BANNED_PAK_FILES {
203 let path = pak_path.clone().join(pak);
204 if path.exists() {
205 return Err(anyhow!(format!("you are not allowed to modify any existing/forbidden PAK files. (global.utoc,global.ucas,recolored-WindowsNoEditor.pak,recolored-WindowsNoEditor.ucas,recolored-WindowsNoEditor.utoc) (VIOLATINGFILE={})", path.display())));
206 }
207 }
208 }
209
210 final_mod_info
211 .auto_generated_tags
212 .push("gamefile-mod".to_string())
213 }
214
215 if !no_custom_textures {
216 if PathBuf::from(&custom_textures_path).is_absolute() {
217 return Err(
218 anyhow!("you are not allowed to have absolute paths on custom textures path."),
219 );
220 }
221
222 if strict {
223 if custom_textures_path.trim().is_empty() {
224 return Err(anyhow!("custom textures path is empty."));
225 }
226 if !PathBuf::from(&path).join(&custom_textures_path).exists() {
227 return Err(anyhow!("custom textures path does not exist."));
228 }
229 }
230
231 final_mod_info
232 .auto_generated_tags
233 .push("texture-mod".to_string())
234 }
235
236 if !no_scripts {
237 if scripts_path.trim().is_empty() {
238 return Err(anyhow!("scripts path is empty."));
239 }
240 if PathBuf::from(&scripts_path).is_absolute() {
241 return Err(anyhow!("you are not allowed to have absolute paths on custom script path."));
242 }
243 if !PathBuf::from(&path).join(&scripts_path).exists() {
244 return Err(anyhow!("custom script path does not exist."));
245 }
246
247 final_mod_info
248 .auto_generated_tags
249 .push("script-mod".to_string())
250 }
251 let icon_path = mod_info.get("icon_path").unwrap().as_str().unwrap();
252
253 final_mod_info.icon_path = icon_path.trim().to_string();
254
255 if icon_path.trim().is_empty() {
256 return Err(anyhow!("mod icon path is empty."));
257 }
258
259 if PathBuf::from(&icon_path).is_absolute() {
260 return Err(anyhow!("you are not allowed to have absolute paths on mod icon."));
261 }
262
263 if PathBuf::from(&icon_path).exists() {
264 return Err(anyhow!("mod icon does not exist."));
265 }
266
267 match mod_info.get("dependencies") {
268 Some(x) => {
269 let array = x.as_array().unwrap();
270 for element in array {
271 let dependency = element.as_str().unwrap().to_string();
272 for char in dependency.trim().chars() {
273 if !char.is_alphanumeric() {
274 return Err(anyhow!("only alphanumerics are allowed in dependency list."));
275 }
276 }
277
278 final_mod_info.dependencies.push(dependency.to_string());
279 }
280 }
281 None => {}
282 };
283
284 for entry in walkdir::WalkDir::new(path).into_iter() {
285 let res = entry?;
286 if res.path().is_dir() {
287 continue;
288 }
289
290 let extension = res.path().extension().unwrap_or_else(|| OsStr::new(""));
291
292 if !extension.is_empty() {
293 let formatted_extension = extension.to_str().unwrap().to_string().to_lowercase();
294 if BANNED_EXTENSIONS.contains(&formatted_extension.as_str()) {
295 return Err(anyhow!(format!("mod contains illegal file ({})", formatted_extension)));
296 }
297 }
298 }
299
300 Ok(final_mod_info)
301}
302
303pub fn generate_project(_game: String, _platform: String, name: String, description: String, path: String) -> Result<()> {
304 println!("Generating Mod");
305 let full_path = PathBuf::from(path);
306
307 let mut mod_info: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
308 let mut meta_file = File::create(Path::new(&full_path).join("mod.json"))?;
309
310 let game = _game.to_uppercase();
311 let platform = _platform.to_uppercase();
312
313 if game == "EM1" && platform == "PC" {
314 return Err(anyhow!(
315 "impossible combination (EM1/PC)",
316 ));
317 }
318
319 if game == "EMR" && platform == "WII" {
320 return Err(anyhow!(
321 "impossible combination (EMR/WII)",
322 ));
323 }
324
325 mod_info.insert("name".to_string(), serde_json::Value::String(name.clone()));
326 mod_info.insert("short_description".to_string(), serde_json::Value::String("Generated with EML-Validate".to_string()));
327 mod_info.insert("game".to_string(), serde_json::Value::String(game.clone()));
328 mod_info.insert("platform".to_string(), serde_json::Value::String(platform.clone()));
329 mod_info.insert("custom_game_files".to_string(), serde_json::Value::String("files".to_string()));
330 mod_info.insert("icon_path".to_string(), serde_json::Value::String("icon.png".to_string()));
331
332 fs::create_dir_all(Path::new(&full_path).join("files"))?;
333 File::create(&full_path.clone().join("description.md"))?.write_all(description.as_bytes())?;
334
335 if platform == "WII" {
336 mod_info.insert("custom_textures_path".to_string(), serde_json::Value::String("textures".to_string()));
337 fs::create_dir_all(Path::new(&full_path).join("textures"))?;
338 }
339
340 if game == "EMR" {
341 mod_info.insert("scripts_path".to_string(), serde_json::Value::String("scripts".to_string()));
342 fs::create_dir_all(Path::new(&full_path).join("scripts"))?;
343 }
344
345 meta_file.write_all(serde_json::to_string(&mod_info)?.as_bytes())?;
346 println!("Finished generating mod");
347 Ok(())
348}
349
350#[derive(Serialize, Deserialize)]
351pub struct ModInfo {
352 pub name: String,
353 pub game: String,
354 pub platform: String,
355 pub description: String,
356 pub short_description: String,
357 pub dependencies: Vec<String>,
358 pub custom_textures_path: String,
359 pub custom_game_files_path: String,
360 pub scripts_path: String,
361 pub icon_path: String,
362 pub auto_generated_tags: Vec<String>,
363}
364
365impl ModInfo {
366 pub fn new() -> ModInfo {
367 ModInfo {
368 name: "".to_string(),
369 game: "".to_string(),
370 platform: "".to_string(),
371 scripts_path: "".to_string(),
372 custom_game_files_path: "".to_string(),
373 custom_textures_path: "".to_string(),
374 description: "".to_string(),
375 short_description: "".to_string(),
376 dependencies: Vec::new(),
377 icon_path: "".to_string(),
378 auto_generated_tags: Vec::new(),
379 }
380 }
381}