// Modules mod among_us_launcher_widget; mod among_us_version; mod semver; // Uses use among_us_launcher_widget::*; use among_us_version::*; use semver::*; use std::{ fs, path::{Path, PathBuf}, process, str, }; use fs_extra::{copy_items, dir}; use regex::Regex; use registry::Hive; // GUI stuff use druid::{ commands, widget::*, AppDelegate, AppLauncher, Data, DelegateCtx, Env, Handled, Target, WidgetPod, WindowDesc, WindowId, }; struct Delegate; #[derive(Data, Clone, Debug)] pub struct AppData { pub among_us_path: String, pub installs_path: String, pub version: SemVer, pub initialized: bool, pub among_us_version: AmongUsVersion, pub data_path: String, } static AMONG_US_APPID: &str = "945360"; fn attempt_run_among_us(install_path: &Path) { let executable_path: PathBuf = [install_path.to_str().unwrap(), "Among Us.exe"] .iter() .collect(); process::Command::new(executable_path.to_str().unwrap()) .spawn() .unwrap(); } fn unmod_among_us_folder(folder_path: &Path) { println!("Unmodding Among Us..."); let doorstop_path: PathBuf = [folder_path.to_str().unwrap(), "doorstop_config.ini"] .iter() .collect(); let steam_appid_path: PathBuf = [folder_path.to_str().unwrap(), "steam_appid.txt"] .iter() .collect(); let winhttp_path: PathBuf = [folder_path.to_str().unwrap(), "winhttp.dll"] .iter() .collect(); let bepinex_path: PathBuf = [folder_path.to_str().unwrap(), "BepInEx"].iter().collect(); let mono_path: PathBuf = [folder_path.to_str().unwrap(), "mono"].iter().collect(); fs::remove_file(doorstop_path).unwrap_or_default(); fs::remove_file(steam_appid_path).unwrap_or_default(); fs::remove_file(winhttp_path).unwrap_or_default(); fs::remove_dir_all(bepinex_path).unwrap_or_default(); fs::remove_dir_all(mono_path).unwrap_or_default(); } async fn _get_latest_updater_version() -> Result<(String, String), reqwest::Error> { let version = env!("CARGO_PKG_VERSION"); let local_sem_ver = SemVer::from(version); let body = reqwest::get("https://git.dormedas.com/api/v1/repos/dormedas/town-of-us-updater/releases"); let root: serde_json::Value = serde_json::from_str(&body.await?.text().await?).unwrap(); for i in root.as_array().unwrap() { let tag_name = i["tag_name"].as_str().unwrap(); if let Some(trimmed_str) = tag_name.strip_prefix('v') { let remote_sem_ver = SemVer::from(trimmed_str); if remote_sem_ver > local_sem_ver { for j in i["assets"].as_array().unwrap() { if j["name"].as_str().unwrap().contains(".exe") { // let url = j["browser_download_url"].as_str().unwrap(); // let data = reqwest::get(url).await?.bytes().await?; // let mut exe_dir = std::env::current_dir().unwrap(); // exe_dir.push("town-of-us-updater-new.exe"); // fs::write(exe_dir, data).unwrap(); } } println!("New version ({}) of the updater detected!", remote_sem_ver); } } else { let remote_sem_ver = SemVer::from(tag_name); if remote_sem_ver > local_sem_ver { for j in i["assets"].as_array().unwrap() { if j["name"].as_str().unwrap().contains(".exe") { // let url = j["browser_download_url"].as_str().unwrap(); // let data = reqwest::get(url).await?.bytes().await?; // let mut exe_dir = std::env::current_dir().unwrap(); // exe_dir.push("town-of-us-updater-new.exe"); // fs::write(exe_dir, data).unwrap(); } } println!("New version ({}) of the updater detected!", remote_sem_ver); } } } Ok((String::from("no"), String::from("yes"))) } impl AppDelegate for Delegate { fn window_added( &mut self, _id: WindowId, data: &mut AppData, _env: &Env, ctx: &mut DelegateCtx, ) { if !data.among_us_path.is_empty() { ctx.submit_command(ATTEMPT_INSTALL); } } fn command( &mut self, _ctx: &mut DelegateCtx, _target: Target, cmd: &druid::Command, data: &mut AppData, _env: &Env, ) -> Handled { // println!("Command!"); if let Some(file_info) = cmd.get(commands::OPEN_FILE) { let among_us_folder = file_info.path(); let mut buf = among_us_folder.to_path_buf(); buf.pop(); data.among_us_path = String::from(buf.to_str().unwrap()); // Pop the selected file off the end of the path // among_us_folder.pop(); println!("{}", buf.to_str().unwrap()); // println!("Handled!"); //Application::global().quit(); return Handled::Yes; } else if let Some(_a) = cmd.get(among_us_launcher_widget::ATTEMPT_INSTALL) { if let Some(among_us_version) = determine_among_us_version(String::from(data.among_us_path.clone())) { data.among_us_version = among_us_version.clone(); println!("AmongUsVersion: {}", among_us_version); let ver_url: (String, String, bool) = determine_town_of_us_url(among_us_version.to_string().clone()).unwrap(); let version_smash = format!("{}-{}", among_us_version, ver_url.0.clone()); let new_installed_path: PathBuf = [data.installs_path.as_str(), version_smash.as_str()] .iter() .collect(); if !Path::exists(&new_installed_path) { println!("Copying Among Us to cache location..."); copy_folder_to_target(data.among_us_path.clone(), data.installs_path.clone()); let among_us_path: PathBuf = [data.installs_path.as_str(), "Among Us\\"].iter().collect(); if !among_us_path.to_str().unwrap().contains("Among Us") { process::exit(0); } // Un-mod whatever we found if it's modded unmod_among_us_folder(&among_us_path); println!( "Renaming {} to {}", among_us_path.to_str().unwrap(), new_installed_path.to_str().unwrap() ); let mut perms = fs::metadata(among_us_path.clone()).unwrap().permissions(); perms.set_readonly(false); fs::set_permissions(among_us_path.clone(), perms).unwrap(); fs::rename(among_us_path, new_installed_path.clone()).unwrap(); let mut download_path: PathBuf = [data.data_path.as_str()].iter().collect(); let downloaded_filename: &str = ver_url.1.rsplit('/').next().unwrap(); download_path.push(downloaded_filename); if !download_path.exists() { // println!("{:?}", download_path); println!( "Downloading Town of Us... Please be patient! [{}]", ver_url.1.clone() ); let zip = reqwest::blocking::get(ver_url.1.clone()) .unwrap() .bytes() .unwrap(); fs::write(download_path.clone(), zip).unwrap(); } let opened_zip = fs::File::open(download_path.clone()).unwrap(); println!("Extracting mod zip file..."); let mut archive = zip::ZipArchive::new(opened_zip).unwrap(); archive.extract(data.data_path.clone()).unwrap(); fs::remove_file(download_path).unwrap(); let mut root_folder_path = String::new(); for i in archive.file_names() { root_folder_path = String::from(i.split('/').next().unwrap()); break; } let extracted_path: PathBuf = [data.data_path.as_str(), root_folder_path.as_str(), "."] .iter() .collect(); println!("{}", extracted_path.to_str().unwrap()); copy_folder_contents_to_target( extracted_path.to_str().unwrap(), new_installed_path.to_str().unwrap(), ); } } data.initialized = !data.initialized; return Handled::Yes; } Handled::No } } fn main() { let version = env!("CARGO_PKG_VERSION"); let title_string: String = format!("Town of Us Updater - {}", version); // println!("Updater Version: {}", version); // get_latest_updater_version().await.unwrap(); // CREATE PROGRAM DIRECTORY let mut data_path = dirs::data_dir().unwrap(); data_path.push("town-of-us-updater"); fs::create_dir(data_path.clone()).unwrap_or(()); let mut installs_path = data_path.clone(); installs_path.push("installs"); fs::create_dir(installs_path.clone()).unwrap_or(()); let version = env!("CARGO_PKG_VERSION"); let mut among_us_folder = PathBuf::new(); let mut existing_file_path = data_path.clone(); existing_file_path.push("existing_among_us_dir.txt"); let existing_found_folder = fs::read_to_string(existing_file_path.clone()); if let Ok(existing_folder) = existing_found_folder { among_us_folder.push(existing_folder); } else { let folder_opt: Option = detect_among_us_folder(); if folder_opt.is_some() { among_us_folder.push(folder_opt.unwrap()); } if among_us_folder.exists() { fs::write(existing_file_path, among_us_folder.to_str().unwrap()).unwrap(); } } let app_data: AppData = AppData { among_us_path: String::from(among_us_folder.clone().to_str().unwrap()), installs_path: String::from(installs_path.clone().to_str().unwrap()), version: SemVer::from(version), initialized: false, among_us_version: AmongUsVersion::default(), data_path: String::from(data_path.clone().to_str().unwrap()), }; let mut root_column: druid::widget::Flex = druid::widget::Flex::column(); // DETERMINE AMONG US VERSION if let Some(among_us_folder_str) = among_us_folder.to_str() { if among_us_folder_str.len() > 0 { println!("Among Us Folder: {}", among_us_folder_str); } else { println!("Among Us Folder not automatically determined"); } } // println!("Checking for updater updates..."); // let vals: (String, String) = version_check_thread_handle.join().unwrap(); // TODO: Auto launch latest sanctioned if the user has a setting like that // Auto launch latest experimental as well println!("Launching main window..."); let _main_menu: druid::MenuDesc = druid::MenuDesc::empty().append(druid::MenuItem::new( druid::LocalizedString::new("File"), druid::Command::new(druid::Selector::new("test"), 0, Target::Auto), )); let widget: AmongUsLauncherWidget = AmongUsLauncherWidget { root: WidgetPod::new(Flex::column()), }; root_column.add_flex_child(widget, 1.0); launch_better_crewlink().unwrap_or(()); let main_window = WindowDesc::new(|| root_column) .title(title_string) // .menu(main_menu) .window_size((400.0, 400.0)); let app_launcher = AppLauncher::with_window(main_window).delegate(Delegate {}); let _external_handler = app_launcher.get_external_handle(); app_launcher.launch(app_data).unwrap(); } fn launch_better_crewlink() -> std::result::Result<(), String> { let mut dir = dirs::data_local_dir().unwrap(); dir.push("Programs"); dir.push("bettercrewlink"); dir.push("Better-CrewLink.exe"); println!("Attempting to launch Better Crew Link"); if dir.exists() { process::Command::new(dir.as_path().to_str().unwrap()) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .spawn() .unwrap(); Ok(()) } else { Err("Better Crew Link not found".to_string()) } } // Returns (Version, URL) fn determine_town_of_us_url(among_us_version: String) -> Option<(String, String, bool)> { let markdown = reqwest::blocking::get( "https://raw.githubusercontent.com/eDonnes124/Town-Of-Us-R/master/README.md", ) .unwrap() .text() .unwrap(); let mut line = markdown.find(&among_us_version); let mut line_offset = 0; let mut official_compatibility = false; if line.is_some() { println!("Found sanctioned version!"); official_compatibility = true; } else { println!("Sanctioned version cannot be determined, installing experimental latest..."); line = markdown.find("[Download]"); line_offset = 15; } // 100% scientific "-15" here to get the correct version since we find to [Download] above which is after the version number for that line let splits = markdown.split_at(line.unwrap() - line_offset); // println!("{}", splits.1); let url_regex = Regex::new(r#"\(([\w\d:/\.\-]+)\)\s|\n"#).unwrap(); let captures = url_regex.captures(splits.1).unwrap(); let capture = captures.get(captures.len() - 1).unwrap(); let url = splits.1.get(capture.start()..capture.end()).unwrap(); println!("Mod URL is: {}", url); let ver_regex = Regex::new(r#"\| (v\d\.\d\.\d) \|"#).unwrap(); let ver_captures = ver_regex.captures(splits.1).unwrap(); let ver_capture = ver_captures.get(ver_captures.len() - 1).unwrap(); let ver = splits .1 .get(ver_capture.start()..ver_capture.end()) .unwrap(); println!("Installing Town of Us version: {}", ver); Some((String::from(ver), String::from(url), official_compatibility)) } fn determine_among_us_version(folder_root: String) -> Option { let asset_file = format!("{}\\Among Us_Data\\globalgamemanagers", folder_root); const TARGET_BYTES_LEN: usize = 16; let target_bytes: [u8; TARGET_BYTES_LEN] = [3, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0]; if let Ok(file_bytes) = fs::read(asset_file) { let mut bytes_str = String::new(); let mut target_head = 0; let mut file_index = 0; for i in &file_bytes { if target_head == TARGET_BYTES_LEN { break; } if *i == target_bytes[target_head] { target_head += 1; } else { target_head = 0; } file_index += 1; bytes_str += &format!("{i:x}"); } let offset: usize = usize::from(file_bytes[file_index]); file_index += 4; Some(AmongUsVersion::from( str::from_utf8(file_bytes.get(file_index..file_index + offset).unwrap()).unwrap(), )) } else { None } //println!("{:?}", file_bytes); } fn copy_folder_to_target>(source: T, dest: T) { let mut copy_opts = dir::CopyOptions::new(); copy_opts.overwrite = true; copy_opts.skip_exist = true; copy_opts.copy_inside = true; let mut from_paths = Vec::new(); from_paths.push(source); copy_items(&from_paths, dest, ©_opts).unwrap(); } fn copy_folder_contents_to_target>(source: T, dest: T) { let mut copy_opts = dir::CopyOptions::new(); copy_opts.overwrite = true; copy_opts.skip_exist = true; copy_opts.copy_inside = true; copy_opts.content_only = true; fs_extra::dir::copy(source, dest, ©_opts).unwrap(); } fn _detect_steam() -> Option { None } fn _detect_epic() -> Option { let _default_folder = String::from("C:\\Program Files\\Epic Games\\AmongUs"); None } fn detect_among_us_folder() -> Option { if let Ok(steam_regkey) = Hive::LocalMachine.open( r"SOFTWARE\WOW6432Node\Valve\Steam", registry::Security::Read, ) { if let Ok(steam_folder) = steam_regkey.value("InstallPath") { println!("{:?}", steam_folder); } } let library_folder = fs::read_to_string("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf"); if library_folder.is_ok() { println!("Steam is on C:\\ drive!"); let mut library_folder_string: String = library_folder.unwrap(); let appid_index = library_folder_string.find(AMONG_US_APPID); if appid_index.is_none() { println!("Among Us not found!"); return None; } else { library_folder_string.truncate(appid_index.unwrap()); } let path_regex = Regex::new(r#"path"\s+"([Z-a\w\d\s\\\(\):]+)""#).unwrap(); let caps: regex::CaptureMatches = path_regex.captures_iter(&library_folder_string); let last_path = caps.last().unwrap(); let start = last_path.get(last_path.len() - 1).unwrap(); return Some(format!( "{}\\\\steamapps\\\\common\\\\Among Us\\\\", library_folder_string .get(start.start()..start.end()) .unwrap() )); } None }