37 Commits

Author SHA1 Message Date
3bd05ba8dd Bump to version 4.0.0
Switch from druid to egui

Support deletion of builds

Auto-updater!

Add a test
2023-01-28 23:29:20 -08:00
ff311475dc Add blessed build text coloring 2023-01-15 14:31:55 -08:00
8e61c8a2f2 Merge branch 'hotfix-2.0.0' 2022-11-28 19:37:10 -08:00
97cdf4829a push to 2.3 2022-11-28 19:23:30 -08:00
7244c24fe4 Fix sorting 2022-11-28 19:20:05 -08:00
fbc4c747a5 Merge branch 'hotfix-2.0.0' 2022-11-14 18:37:53 -08:00
7f1a6c6bc1 Bump to 2.2.0 2022-11-14 18:20:32 -08:00
856cffd740 No timeouts while downloading 2022-11-14 18:15:24 -08:00
9468194718 Remove windows-only components
Attempt to divide initialization
2022-10-11 21:03:33 -07:00
69f67d303e Merge branch 'master' of http://git.dormedas.com/dormedas/town-of-us-updater 2022-09-26 19:08:26 -07:00
6d05440d98 Merge branch 'hotfix-2.0.0' 2022-09-26 19:07:54 -07:00
686d24f7b5 Hotfix: Fix initialization of mod if Among Us path is known 2022-09-26 19:02:17 -07:00
972c310673 Update 'README.md' 2022-09-26 14:47:01 -07:00
90b703ee30 Bump to 3.0.0 2022-09-26 08:15:17 -07:00
38c975b44c Massive GUI code improvements!
Almost all data initialization and processing happens through druid

Tokio removed for now, web requests block again
2022-09-25 23:50:07 -07:00
762d96baee Added a semver class
Auto updater improvements, not yet ready

Registry and Epic Games auto-detection started
2022-09-03 18:56:03 -07:00
81e8eaad0f Merge branch 'feature-cleanup' 2022-08-31 12:53:29 -07:00
84d206a87b Merge branch 'master' of http://git.dormedas.com/dormedas/town-of-us-updater 2022-08-31 12:52:52 -07:00
e59c25af25 Cleaned up dead code comments
Locating the among us folder and launching the app are one in the same, need GUI work to allow it to update after location

More and changed log messages
2022-08-31 12:51:56 -07:00
cf75410860 Clippy fixes 2022-08-29 23:30:29 -07:00
35828e4b5b Clippy fixes 2022-08-29 22:59:53 -07:00
bac49bc766 Fix clippy compile error 2022-08-29 22:59:19 -07:00
9564c70a33 Remove comments, simplify 2022-08-29 19:17:47 -07:00
c9e9cbc66d Merge branch 'master' into feature-cleanup 2022-08-29 18:43:53 -07:00
88f8ca7031 Bump to 2.0.0 2022-08-29 18:43:40 -07:00
721e08cfc6 Fix warning 2022-08-29 18:43:22 -07:00
d6e3a7133b Move AmongUsVersion to its own file. Concatenate and reduce imports. 2022-08-29 18:41:03 -07:00
9e90a18c6a Update 'README.md' 2022-08-29 14:31:59 -07:00
9132afe917 Add .gitignore 2022-08-29 11:32:40 -07:00
ebad7cb893 Fix blocking reqwest crash 2022-08-29 11:31:21 -07:00
b4c22cdc48 Migrated to druid gui 2022-08-21 10:50:48 -07:00
54b2b8b117 Updated log message 2022-08-16 08:55:26 -07:00
91579d333e Fully implemented tokio runtime. Added build script. Executable has an icon. 2022-08-16 08:06:06 -07:00
0b940aea70 Updated version check logic. Still un-implemented 2022-07-26 22:12:30 -07:00
86d1e176d4 More improved messaging, added folder detection caching 2022-07-26 20:37:00 -07:00
b6221643e2 Improve messaging around the app 2022-07-26 19:43:50 -07:00
3150019b76 Bumped version to 1.0.0, started work on update infrastructure 2022-07-20 09:26:22 -07:00
10 changed files with 750 additions and 266 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
Cargo.lock

View File

@@ -1,7 +1,8 @@
[package] [package]
name = "town-of-us-updater" name = "town-of-us-updater"
version = "0.1.0" version = "4.0.0"
edition = "2021" edition = "2021"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,4 +12,13 @@ regex = "1.5"
fs_extra = "1.2.0" fs_extra = "1.2.0"
dirs = "4.0.0" dirs = "4.0.0"
reqwest = {version = "0.11.11", features = ["blocking"]} reqwest = {version = "0.11.11", features = ["blocking"]}
iui = "0.3.0" serde_json = "1.0"
md-5 = "0.10.5"
tokio = {version="1", features=["full"]}
registry = "1"
egui = "0.20.1"
eframe = "0.20.1"
rfd = "0.10"
[build-dependencies]
embed-resource = "1.6"

View File

@@ -1,2 +1,21 @@
# town-of-us-updater # Town of Us Updater
A tool to automatically install the **Town of Us R** mod for **Among Us**.
## Features
- Caches old builds to allow you to continue playing the mod in case of a breaking Among Us update
- GUI to select which build to play
- Auto-detection of Among Us install directory
- Auto-launch of BetterCrewLink if installed
# Contributing
Help. I have no standards. Just look at the code.
## How to Build
- [Install Rust](https://www.rust-lang.org/learn/get-started)
- Download repository
- Navigate to repository
- `cargo build`

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

1
assets/main.rc Normal file
View File

@@ -0,0 +1 @@
1 ICON "icon.ico"

91
src/among_us_version.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::fmt;
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
pub struct AmongUsVersion {
year: i32,
month: i32,
day: i32,
}
impl AmongUsVersion {
pub fn as_american_string(&self) -> String {
format!("{}-{}-{}", self.month, self.day, self.year)
}
}
impl fmt::Display for AmongUsVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.year, self.month, self.day)
}
}
impl Default for AmongUsVersion {
fn default() -> Self {
AmongUsVersion {
year: 0,
month: 0,
day: 0,
}
}
}
impl From<&str> for AmongUsVersion {
fn from(s: &str) -> AmongUsVersion {
// Ignore a prepending "v"
let tmp_str = s.replace("v", "");
let v: Vec<&str> = tmp_str.split('.').collect();
AmongUsVersion {
year: v[0].parse().unwrap(),
month: v[1].parse().unwrap(),
day: v[2].parse().unwrap(),
}
}
}
impl From<(i32, i32, i32)> for AmongUsVersion {
fn from(s: (i32, i32, i32)) -> AmongUsVersion {
AmongUsVersion {
year: s.0,
month: s.1,
day: s.2,
}
}
}
#[test]
fn ordering_test() {
let mut vec = Vec::new();
vec.push(AmongUsVersion {
year: 2019,
month: 1,
day: 25,
});
vec.push(AmongUsVersion {
year: 2022,
month: 12,
day: 25,
});
vec.push(AmongUsVersion {
year: 2022,
month: 12,
day: 14,
});
vec.push(AmongUsVersion {
year: 2022,
month: 12,
day: 8,
});
vec.sort();
assert_eq!(vec[0].year, 2019);
assert_eq!(vec[1].year, 2022);
assert_eq!(vec[1].day, 8);
assert_eq!(vec[2].year, 2022);
assert_eq!(vec[2].day, 14);
assert_eq!(vec[3].year, 2022);
assert_eq!(vec[3].day, 25);
}

5
src/build.rs Normal file
View File

@@ -0,0 +1,5 @@
extern crate embed_resource;
fn main() {
embed_resource::compile("assets/main.rc");
}

View File

@@ -1,48 +1,224 @@
use std::path::{Path, PathBuf}; // Modules
use std::str; mod among_us_version;
use std::*; mod semver;
// Uses
use among_us_version::*;
use md5::{Digest, Md5};
use semver::*;
use std::{
fs, io,
path::{Path, PathBuf},
process, str,
};
use fs_extra::{copy_items, dir}; use fs_extra::{copy_items, dir};
use iui::controls::{Button, Group, Label, VerticalBox};
use iui::prelude::*;
use regex::Regex; use regex::Regex;
static AMONG_US_APPID: &'static str = "945360"; #[cfg(target_os = "windows")]
use registry::Hive;
struct AmongUsVersion { // GUI stuff
year: i32,
month: i32, use eframe::egui;
day: i32,
use reqwest::StatusCode;
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum InitializingState {
StartingGUI,
DeterminingVersion,
CopyingAmongUs,
UnmoddingAmongUs,
DownloadingZip,
Unzipping,
CopyingZipIn,
} }
impl fmt::Display for AmongUsVersion { #[derive(PartialEq, Eq, Clone, Debug)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { pub enum GlobalAppState {
write!(f, "{}.{}.{}", self.year, self.month, self.day) Initializing(InitializingState),
Initialized,
Installing,
Playing,
}
#[derive(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,
pub app_state: GlobalAppState,
pub blessed_version: String,
pub update_installs_list: bool,
pub installs_last_update_time: std::time::Instant,
pub installs_list: Vec<PathBuf>,
}
impl Default for AppData {
fn default() -> AppData {
AppData {
among_us_path: String::new(),
installs_path: String::new(),
version: SemVer {
major: 0,
minor: 0,
patch: 0,
},
initialized: false,
among_us_version: AmongUsVersion::default(),
data_path: String::new(),
app_state: GlobalAppState::Initialized,
blessed_version: String::new(),
update_installs_list: true,
installs_last_update_time: std::time::Instant::now(),
installs_list: Vec::new(),
}
} }
} }
impl From<&str> for AmongUsVersion { impl AppData {
fn from(s: &str) -> AmongUsVersion { fn detect_installs(&mut self) {
let v: Vec<&str> = s.split(".").collect(); let install_iter = fs::read_dir(self.installs_path.clone());
AmongUsVersion { if let Ok(iter) = install_iter {
year: i32::from_str_radix(v[0], 10).unwrap(), self.installs_list = iter
month: i32::from_str_radix(v[1], 10).unwrap(), .map(|elem| PathBuf::from(elem.unwrap().file_name()))
day: i32::from_str_radix(v[2], 10).unwrap(), .collect();
} else {
self.installs_list.clear();
}
self.installs_last_update_time = std::time::Instant::now();
}
fn draw_layout(&mut self, ui: &mut egui::Ui) {
let now = std::time::Instant::now();
if now.duration_since(self.installs_last_update_time) > std::time::Duration::from_secs(10) {
self.detect_installs();
}
let mut collection = self.installs_list.clone();
if collection.is_empty() {
if ui.button("Locate your Among Us Directory").clicked() {
if let Some(mut path) = rfd::FileDialog::new().pick_file() {
path.pop();
self.among_us_path = path.display().to_string();
self.on_path_added();
}
}
return;
}
collection.reverse();
let mut auv_array: Vec<AmongUsVersion> = Vec::new();
for i in &collection {
let existing_ver_smash = i;
let mut ver_smash_split = existing_ver_smash.to_str().unwrap().split('-');
let among_us_version = AmongUsVersion::from(ver_smash_split.next().unwrap());
if !auv_array.contains(&among_us_version) {
auv_array.push(among_us_version);
}
}
auv_array.sort();
auv_array.reverse();
ui.separator();
for i in &auv_array {
ui.label(
egui::RichText::new(format!("Among Us {}", i.as_american_string())).size(35.0),
);
for j in &collection {
let existing_ver_smash = j;
let mut ver_smash_split = existing_ver_smash.to_str().unwrap().split('-');
let mut blessed_split = self.blessed_version.as_str().split('-');
let among_us_version = AmongUsVersion::from(ver_smash_split.next().unwrap());
let tou_version = ver_smash_split.next().unwrap();
let blessed_version = AmongUsVersion::from(blessed_split.next().unwrap());
let blessed_tou_version = blessed_split.next().unwrap();
let button_string: String = format!("Town of Us {}", tou_version);
if i == &among_us_version {
let mut clone: PathBuf = PathBuf::from(self.installs_path.clone());
clone.push(existing_ver_smash.clone());
ui.horizontal(|ui| {
if i == &blessed_version && tou_version == blessed_tou_version {
let button_text = egui::RichText::new(button_string.clone())
.size(25.0)
.color(egui::Color32::GREEN);
if ui.button(button_text).clicked() {
crate::attempt_run_among_us(&clone);
}
} else {
let button_text = egui::RichText::new(button_string.clone()).size(25.0);
if ui.button(button_text).clicked() {
crate::attempt_run_among_us(&clone);
}
}
if !self.initialized {
ui.set_visible(false);
}
let button_text = egui::RichText::new("DELETE")
.size(25.0)
.color(egui::Color32::RED);
if ui.button(button_text).clicked() {
if i != &blessed_version || tou_version != blessed_tou_version {
crate::attempt_delete_among_us(&clone);
self.update_installs_list = true;
}
}
ui.set_visible(true);
});
}
}
ui.separator();
} }
} }
} }
impl eframe::App for AppData {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.update_installs_list {
self.detect_installs();
self.update_installs_list = false;
}
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
ui.checkbox(&mut self.initialized, "DELETE MODE");
});
egui::CentralPanel::default().show(ctx, |ui| {
self.draw_layout(ui);
});
}
}
static AMONG_US_APPID: &str = "945360";
fn attempt_run_among_us(install_path: &Path) { fn attempt_run_among_us(install_path: &Path) {
let executable_path: path::PathBuf = [install_path.to_str().unwrap(), "Among Us.exe"] let executable_path: PathBuf = [install_path.to_str().unwrap(), "Among Us.exe"]
.iter() .iter()
.collect(); .collect();
std::process::Command::new(executable_path.to_str().unwrap()) process::Command::new(executable_path.to_str().unwrap())
.spawn() .spawn()
.unwrap(); .unwrap();
} }
fn attempt_delete_among_us(install_path: &Path) {
fs::remove_dir_all(install_path).unwrap();
}
fn unmod_among_us_folder(folder_path: &Path) { fn unmod_among_us_folder(folder_path: &Path) {
println!("Unmodding Among Us...");
let doorstop_path: PathBuf = [folder_path.to_str().unwrap(), "doorstop_config.ini"] let doorstop_path: PathBuf = [folder_path.to_str().unwrap(), "doorstop_config.ini"]
.iter() .iter()
.collect(); .collect();
@@ -52,6 +228,7 @@ fn unmod_among_us_folder(folder_path: &Path) {
let winhttp_path: PathBuf = [folder_path.to_str().unwrap(), "winhttp.dll"] let winhttp_path: PathBuf = [folder_path.to_str().unwrap(), "winhttp.dll"]
.iter() .iter()
.collect(); .collect();
let bepinex_path: PathBuf = [folder_path.to_str().unwrap(), "BepInEx"].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(); let mono_path: PathBuf = [folder_path.to_str().unwrap(), "mono"].iter().collect();
fs::remove_file(doorstop_path).unwrap_or_default(); fs::remove_file(doorstop_path).unwrap_or_default();
@@ -61,70 +238,74 @@ fn unmod_among_us_folder(folder_path: &Path) {
fs::remove_dir_all(mono_path).unwrap_or_default(); fs::remove_dir_all(mono_path).unwrap_or_default();
} }
fn main() { async fn _get_latest_updater_version() -> Result<(String, String), reqwest::Error> {
// CREATE PROGRAM DIRECTORY let version = env!("CARGO_PKG_VERSION");
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(); let local_sem_ver = SemVer::from(version);
installs_path.push("installs");
fs::create_dir(installs_path.clone()).unwrap_or(());
let ui = UI::init().expect("UI failed to init"); 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();
let mut win = Window::new(&ui, "Town of Us Updater", 600, 400, WindowType::NoMenubar); for i in root.as_array().unwrap() {
let tag_name = i["tag_name"].as_str().unwrap();
let mut vbox = VerticalBox::new(&ui); if let Some(trimmed_str) = tag_name.strip_prefix('v') {
let remote_sem_ver = SemVer::from(trimmed_str);
vbox.set_padded(&ui, true); if remote_sem_ver > local_sem_ver {
for j in i["assets"].as_array().unwrap() {
let mut button = Button::new(&ui, "Button"); if j["name"].as_str().unwrap().contains(".exe") {
button.on_clicked(&ui, { // let url = j["browser_download_url"].as_str().unwrap();
let ui = ui.clone(); // let data = reqwest::get(url).await?.bytes().await?;
move |btn| { // let mut exe_dir = std::env::current_dir().unwrap();
let folder_opt = detect_among_us_folder(); // exe_dir.push("town-of-us-updater-new.exe");
if folder_opt.is_some() { // fs::write(exe_dir, data).unwrap();
btn.set_text(&ui, "Amogus");
ui.quit();
} }
} }
}); println!("New version ({}) of the updater detected!", remote_sem_ver);
}
let folder_opt = detect_among_us_folder();
let mut among_us_folder = path::PathBuf::new();
if folder_opt.is_some() {
among_us_folder.push(folder_opt.unwrap());
} else { } else {
win.modal_msg(&ui, "Find Among Us", "On the following Open File dialog, navigate to your Among Us folder and select Among Us.exe"); let remote_sem_ver = SemVer::from(tag_name);
let open_window = win.open_file(&ui); if remote_sem_ver > local_sem_ver {
// println!("{}", open_window.unwrap().to_str().unwrap()); for j in i["assets"].as_array().unwrap() {
among_us_folder = open_window.unwrap().clone(); if j["name"].as_str().unwrap().contains(".exe") {
among_us_folder.pop(); // 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);
}
}
} }
println!("Among Us Folder: {}", among_us_folder.to_str().unwrap()); Ok((String::from("no"), String::from("yes")))
}
let among_us_version = impl AppData {
determine_among_us_version(String::from(among_us_folder.to_str().unwrap())).unwrap(); fn on_path_added(&mut self) {
if let Some(among_us_version) =
determine_among_us_version(String::from(self.among_us_path.clone()))
{
self.among_us_version = among_us_version.clone();
println!("AmongUsVersion: {}", among_us_version);
}
self.app_state = GlobalAppState::Initializing(InitializingState::DeterminingVersion);
let ver_url: (String, String, bool) = let ver_url: (String, String, bool) =
determine_town_of_us_url(among_us_version.clone()).unwrap(); determine_town_of_us_url(self.among_us_version.to_string().clone()).unwrap();
let version_smash = format!("{}-{}", among_us_version.clone(), ver_url.0.clone()); let version_smash = format!("{}-{}", self.among_us_version, ver_url.0.clone());
let new_installed_path: path::PathBuf = let new_installed_path: PathBuf = [self.installs_path.as_str(), version_smash.as_str()]
[installs_path.to_str().unwrap(), version_smash.as_str()]
.iter() .iter()
.collect(); .collect();
if !path::Path::exists(&new_installed_path) { if !Path::exists(&new_installed_path) {
copy_folder_to_target( println!("Copying Among Us to cache location...");
among_us_folder.to_str().unwrap(), copy_folder_to_target(self.among_us_path.clone(), self.installs_path.clone());
installs_path.to_str().unwrap(),
);
let among_us_path: path::PathBuf = [installs_path.to_str().unwrap(), "Among Us\\"] let among_us_path: PathBuf = [self.installs_path.as_str(), "Among Us"].iter().collect();
.iter()
.collect();
if !among_us_path.to_str().unwrap().contains("Among Us") { if !among_us_path.to_str().unwrap().contains("Among Us") {
process::exit(0); process::exit(0);
@@ -134,7 +315,7 @@ fn main() {
unmod_among_us_folder(&among_us_path); unmod_among_us_folder(&among_us_path);
println!( println!(
"Rename {} to {}", "Renaming {} to {}",
among_us_path.to_str().unwrap(), among_us_path.to_str().unwrap(),
new_installed_path.to_str().unwrap() new_installed_path.to_str().unwrap()
); );
@@ -144,32 +325,38 @@ fn main() {
fs::set_permissions(among_us_path.clone(), perms).unwrap(); fs::set_permissions(among_us_path.clone(), perms).unwrap();
fs::rename(among_us_path, new_installed_path.clone()).unwrap(); fs::rename(among_us_path, new_installed_path.clone()).unwrap();
let mut download_path = data_path.clone(); let mut download_path: PathBuf = [self.data_path.as_str()].iter().collect();
let downloaded_filename = ver_url.1.rsplit("/").nth(0).unwrap(); let downloaded_filename: &str = ver_url.1.rsplit('/').next().unwrap();
download_path.push(downloaded_filename.clone()); download_path.push(downloaded_filename);
if !path::Path::exists(&download_path) { if !download_path.exists() {
println!("{:?}", download_path); // println!("{:?}", download_path);
println!("Downloading Town of Us [{}]", ver_url.1.clone()); println!(
let zip = reqwest::blocking::get(ver_url.1.clone()) "Downloading Town of Us... Please be patient! [{}]",
.unwrap() ver_url.1.clone()
.bytes() );
let client = reqwest::blocking::Client::builder()
.timeout(None)
.build()
.unwrap(); .unwrap();
let zip_request = client.get(ver_url.1.clone()).build().unwrap();
let zip = client.execute(zip_request).unwrap().bytes().unwrap();
fs::write(download_path.clone(), zip).unwrap(); fs::write(download_path.clone(), zip).unwrap();
} }
let opened_zip = fs::File::open(download_path).unwrap(); let opened_zip = fs::File::open(download_path.clone()).unwrap();
println!("Extracting..."); println!("Extracting mod zip file...");
let mut archive = zip::ZipArchive::new(opened_zip).unwrap(); let mut archive = zip::ZipArchive::new(opened_zip).unwrap();
archive.extract(data_path.clone()).unwrap(); archive.extract(self.data_path.clone()).unwrap();
fs::remove_file(download_path).unwrap();
let mut root_folder_path = String::new(); let mut root_folder_path = String::new();
for i in archive.file_names() { for i in archive.file_names() {
root_folder_path = String::from(i.split("/").nth(0).unwrap()); root_folder_path = String::from(i.split('/').next().unwrap());
break; break;
} }
let extracted_path: path::PathBuf = let extracted_path: PathBuf = [self.data_path.as_str(), root_folder_path.as_str(), "."]
[data_path.to_str().unwrap(), root_folder_path.as_str(), "."]
.iter() .iter()
.collect(); .collect();
println!("{}", extracted_path.to_str().unwrap()); println!("{}", extracted_path.to_str().unwrap());
@@ -177,57 +364,212 @@ fn main() {
extracted_path.to_str().unwrap(), extracted_path.to_str().unwrap(),
new_installed_path.to_str().unwrap(), new_installed_path.to_str().unwrap(),
); );
}
self.initialized = !self.initialized;
self.app_state = GlobalAppState::Initialized;
self.update_installs_list = true;
}
}
fn main() {
let version = env!("CARGO_PKG_VERSION");
let title_string: String = format!("Town of Us Updater - {}", version);
let blessed_body = reqwest::blocking::get("https://tou.dormedas.com/blessed");
let response = blessed_body.unwrap();
let blessed_version: String = match response.status() {
StatusCode::OK => response.text().unwrap(),
_ => String::from("0.0.0-v0.0.0"),
};
println!("Blessed Version: {}", blessed_version);
// 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 mut true_env = false;
for argument in std::env::args() {
if argument == "true" {
true_env = true;
}
}
if !true_env {
let mut exe_path = data_path.clone();
if cfg!(windows) {
exe_path.push("town-of-us-updater.exe");
} else if cfg!(unix) {
exe_path.push("town-of-us-updater");
}
let mut updated_exe_hash = String::new();
if let Ok(response) = reqwest::blocking::get("https://tou.dormedas.com/release/hash") {
if let Ok(text) = response.text() {
updated_exe_hash = text;
}
} else { } else {
println!("Modded install already found"); println!("Failed to get hash. Running local version.")
} }
// Find existing installs, make buttons for them // If we heard back, try to update, otherwise run normally
let install_iter = fs::read_dir(installs_path.clone()); if !updated_exe_hash.is_empty() {
let mut our_hash = String::new();
match install_iter { let mut try_continue = false;
Ok(iter) => { match fs::read(&exe_path) {
let mut collection: Vec<Result<fs::DirEntry, io::Error>> = iter.collect(); Ok(bytes) => {
collection.reverse(); let mut hasher = Md5::new();
for i in collection { hasher.update(bytes);
// for i in iter { let finalized = hasher.finalize();
let existing_ver_smash = i.unwrap().file_name(); our_hash = format!("{finalized:x}");
let mut button = Button::new(&ui, existing_ver_smash.clone().to_str().unwrap()); try_continue = true;
button.on_clicked(&ui, {
let ui = ui.clone();
let installs_path = installs_path.clone();
let existing_ver_smash = existing_ver_smash.clone();
move |btn| {
let mut new_path = installs_path.clone();
new_path.push(existing_ver_smash.clone());
println!("{}", new_path.clone().to_str().unwrap());
attempt_run_among_us(&new_path);
btn.set_text(&ui, "Launching...");
} }
}); Err(e) => match e.kind() {
vbox.append(&ui, button, LayoutStrategy::Stretchy); io::ErrorKind::NotFound => try_continue = true,
println!("{}", existing_ver_smash.clone().to_str().unwrap()); _ => {
println!("{}", e.to_string());
} }
} },
_ => (),
} }
win.set_child(&ui, vbox); if !our_hash.is_empty() {
win.show(&ui); println!("{} vs {}", our_hash, updated_exe_hash);
ui.main(); } else {
// let mut install_folder_path = installs_path.clone(); println!("Local executable didn't exist");
// install_folder_path.push(version_smash); }
// fs::create_dir(install_folder_path.clone()).unwrap_or(());
// Copy Among Us Out if try_continue {
// Create destination path if our_hash != updated_exe_hash {
println!("Newer version detected, downloading...");
let client = reqwest::blocking::Client::builder()
.timeout(None)
.build()
.unwrap();
let updated_exe_data_req = client
.get("https://tou.dormedas.com/release")
.build()
.unwrap();
// let executable_path: path::PathBuf = [new_installed_path.to_str().unwrap(), "Among Us.exe"] let updated_exe_data = client.execute(updated_exe_data_req);
// .iter()
// .collect();
// std::process::Command::new(executable_path.to_str().unwrap()) if let Ok(response) = updated_exe_data {
// .spawn() if response.status() == StatusCode::OK {
// .unwrap(); if let Ok(file_bytes) = response.bytes() {
match fs::write(&exe_path, file_bytes) {
Ok(_) => {
println!("Successfully updated executable");
}
_ => {
println!("Failed to update executable");
}
};
}
}
}
}
// We can fail to download a good binary, just continue with our current executable
if exe_path.exists() {
process::Command::new(&exe_path)
.arg("true")
.stdout(process::Stdio::null())
.stderr(process::Stdio::null())
.spawn()
.unwrap();
process::exit(0);
}
}
}
}
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<String> = 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 mut 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()),
app_state: GlobalAppState::Initializing(InitializingState::StartingGUI),
blessed_version,
update_installs_list: true,
installs_last_update_time: std::time::Instant::now(),
installs_list: Vec::new(),
};
// 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");
}
}
// TODO: Auto launch latest sanctioned if the user has a setting like that
// Auto launch latest experimental as well
println!("Launching main window...");
launch_better_crewlink().unwrap_or(());
// let app_launcher = AppLauncher::with_window(main_window).delegate(Delegate {});
// app_launcher.launch(app_data).unwrap();
let mut native_options = eframe::NativeOptions::default();
native_options.min_window_size = Some(egui::vec2(425.0, 400.0));
native_options.initial_window_size = Some(egui::vec2(425.0, 400.0));
app_data.detect_installs(); // Initial check since we only update every 10s
eframe::run_native(
title_string.as_str(),
native_options,
Box::new(|_cc| Box::new(app_data)),
);
}
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) // Returns (Version, URL)
@@ -238,33 +580,17 @@ fn determine_town_of_us_url(among_us_version: String) -> Option<(String, String,
.unwrap() .unwrap()
.text() .text()
.unwrap(); .unwrap();
let mut line = markdown.find(&among_us_version); let mut line = markdown.find(&among_us_version);
let mut line_offset = 0; let mut line_offset = 0;
let mut official_compatibility = false; let mut official_compatibility = false;
if line.is_some() { if line.is_some() {
println!("Found official version!"); println!("Found sanctioned version!");
official_compatibility = true; official_compatibility = true;
} else { } else {
println!("Official version cannot be determined, installing experimental latest..."); println!("Sanctioned version cannot be determined, installing experimental latest...");
line = markdown.find("[Download]"); line = markdown.find("[Download]");
line_offset = 15; line_offset = 15;
// println!("At this point, there are two options:");
// println!(" [1] Revert Among Us to public-previous in Steam THEN select this option.");
// println!(" [2] Attempt to use the latest version of Town of Us anyway");
// let mut input = String::new();
// io::stdin().read_line(&mut input).unwrap();
// match input.trim() {
// "1" => {
// println!("Re-attempting...");
// return None;
// }
// "2" => {
// println!("Continuing...");
// line = markdown.find("[Download]");
// line_offset = 15;
// }
// _ => return None,
// };
} }
// 100% scientific "-15" here to get the correct version since we find to [Download] above which is after the version number for that line // 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); let splits = markdown.split_at(line.unwrap() - line_offset);
@@ -273,7 +599,7 @@ fn determine_town_of_us_url(among_us_version: String) -> Option<(String, String,
let captures = url_regex.captures(splits.1).unwrap(); let captures = url_regex.captures(splits.1).unwrap();
let capture = captures.get(captures.len() - 1).unwrap(); let capture = captures.get(captures.len() - 1).unwrap();
let url = splits.1.get(capture.start()..capture.end()).unwrap(); let url = splits.1.get(capture.start()..capture.end()).unwrap();
println!("Official URL is: {}", url); println!("Mod URL is: {}", url);
let ver_regex = Regex::new(r#"\| (v\d\.\d\.\d) \|"#).unwrap(); let ver_regex = Regex::new(r#"\| (v\d\.\d\.\d) \|"#).unwrap();
let ver_captures = ver_regex.captures(splits.1).unwrap(); let ver_captures = ver_regex.captures(splits.1).unwrap();
let ver_capture = ver_captures.get(ver_captures.len() - 1).unwrap(); let ver_capture = ver_captures.get(ver_captures.len() - 1).unwrap();
@@ -281,20 +607,18 @@ fn determine_town_of_us_url(among_us_version: String) -> Option<(String, String,
.1 .1
.get(ver_capture.start()..ver_capture.end()) .get(ver_capture.start()..ver_capture.end())
.unwrap(); .unwrap();
println!("Matching version is: {}", ver); println!("Installing Town of Us version: {}", ver);
Some((String::from(ver), String::from(url), official_compatibility)) Some((String::from(ver), String::from(url), official_compatibility))
} }
fn determine_among_us_version(folder_root: String) -> Option<String> { fn determine_among_us_version(folder_root: String) -> Option<AmongUsVersion> {
let asset_file = format!("{}\\Among Us_Data\\globalgamemanagers", folder_root); let asset_file = format!("{}/Among Us_Data/globalgamemanagers", folder_root);
const TARGET_BYTES_LEN: usize = 16; 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]; let target_bytes: [u8; TARGET_BYTES_LEN] = [3, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0];
let file_bytes = fs::read(asset_file).unwrap(); if let Ok(file_bytes) = fs::read(asset_file) {
//println!("{:?}", file_bytes);
let mut bytes_str = String::new(); let mut bytes_str = String::new();
let mut target_head = 0; let mut target_head = 0;
let mut file_index = 0; let mut file_index = 0;
@@ -315,32 +639,17 @@ fn determine_among_us_version(folder_root: String) -> Option<String> {
let offset: usize = usize::from(file_bytes[file_index]); let offset: usize = usize::from(file_bytes[file_index]);
file_index += 4; file_index += 4;
// let mut offset = 10; Some(AmongUsVersion::from(
// if file_bytes[file_index + 9] == 0 {
// offset -= 1;
// }
// if file_bytes[file_index + 8] == 0 {
// offset -= 1;
// }
println!(
"|{}|",
str::from_utf8(file_bytes.get(file_index..file_index + offset).unwrap()).unwrap()
);
let ver = AmongUsVersion::from(
str::from_utf8(file_bytes.get(file_index..file_index + offset).unwrap()).unwrap(),
);
println!("AmongUsVersion: {}", ver);
Some(String::from(
str::from_utf8(file_bytes.get(file_index..file_index + offset).unwrap()).unwrap(), str::from_utf8(file_bytes.get(file_index..file_index + offset).unwrap()).unwrap(),
)) ))
//println!("{}", bytes_str); } else {
None
//None
} }
fn copy_folder_to_target<T: AsRef<path::Path>>(source: T, dest: T) { //println!("{:?}", file_bytes);
}
fn copy_folder_to_target<T: AsRef<Path>>(source: T, dest: T) {
let mut copy_opts = dir::CopyOptions::new(); let mut copy_opts = dir::CopyOptions::new();
copy_opts.overwrite = true; copy_opts.overwrite = true;
copy_opts.skip_exist = true; copy_opts.skip_exist = true;
@@ -350,7 +659,7 @@ fn copy_folder_to_target<T: AsRef<path::Path>>(source: T, dest: T) {
copy_items(&from_paths, dest, &copy_opts).unwrap(); copy_items(&from_paths, dest, &copy_opts).unwrap();
} }
fn copy_folder_contents_to_target<T: AsRef<path::Path>>(source: T, dest: T) { fn copy_folder_contents_to_target<T: AsRef<Path>>(source: T, dest: T) {
let mut copy_opts = dir::CopyOptions::new(); let mut copy_opts = dir::CopyOptions::new();
copy_opts.overwrite = true; copy_opts.overwrite = true;
copy_opts.skip_exist = true; copy_opts.skip_exist = true;
@@ -358,12 +667,36 @@ fn copy_folder_contents_to_target<T: AsRef<path::Path>>(source: T, dest: T) {
copy_opts.content_only = true; copy_opts.content_only = true;
fs_extra::dir::copy(source, dest, &copy_opts).unwrap(); fs_extra::dir::copy(source, dest, &copy_opts).unwrap();
} }
fn _detect_steam() -> Option<String> {
None
}
fn _detect_epic() -> Option<String> {
let _default_folder = String::from("C:\\Program Files\\Epic Games\\AmongUs");
None
}
fn detect_among_us_folder() -> Option<String> { fn detect_among_us_folder() -> Option<String> {
if cfg!(windows) {
let mut library_folder_path: PathBuf =
PathBuf::from("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf");
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") {
library_folder_path = PathBuf::from(steam_folder.to_string());
library_folder_path.push("steamapps\\libraryfolders.vdf");
println!("{:?}", steam_folder);
println!("{:?}", library_folder_path.to_str());
}
}
let library_folder = let library_folder =
fs::read_to_string("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf"); fs::read_to_string(library_folder_path.to_str().expect("Path invalid"));
if library_folder.is_ok() { if library_folder.is_ok() {
println!("Steam is on C:\\ drive!");
let mut library_folder_string: String = library_folder.unwrap(); let mut library_folder_string: String = library_folder.unwrap();
let appid_index = library_folder_string.find(AMONG_US_APPID); let appid_index = library_folder_string.find(AMONG_US_APPID);
if appid_index.is_none() { if appid_index.is_none() {
@@ -372,27 +705,20 @@ fn detect_among_us_folder() -> Option<String> {
} else { } else {
library_folder_string.truncate(appid_index.unwrap()); library_folder_string.truncate(appid_index.unwrap());
} }
//println!("{}", library_folder_string);
let path_regex = Regex::new(r#"path"\s+"([Z-a\w\d\s\\\(\):]+)""#).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 caps: regex::CaptureMatches = path_regex.captures_iter(&library_folder_string);
let last_path = caps.last().unwrap(); let last_path = caps.last().unwrap();
let start = last_path.get(last_path.len() - 1).unwrap(); let start = last_path.get(last_path.len() - 1).unwrap();
/*
println!(
"{}",
library_folder_string
.get(start.start()..start.end())
.unwrap()
);
*/
return Some(format!( return Some(format!(
"{}\\\\steamapps\\\\common\\\\Among Us\\\\", r"{}\\steamapps\\common\\Among Us\\",
library_folder_string library_folder_string
.get(start.start()..start.end()) .get(start.start()..start.end())
.unwrap() .unwrap()
)); ));
} }
} else {
}
None None
} }

31
src/semver.rs Normal file
View File

@@ -0,0 +1,31 @@
//! # SemVer
//! A simple Semantic Versioning struct to handle comparisons and ordering
// Uses
use std::fmt;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
pub struct SemVer {
pub major: i32,
pub minor: i32,
pub patch: i32,
}
impl fmt::Display for SemVer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl From<&str> for SemVer {
fn from(s: &str) -> SemVer {
let v: Vec<&str> = s.split('.').collect();
SemVer {
major: v[0].parse().unwrap(),
minor: v[1].parse().unwrap(),
patch: v[2].parse().unwrap(),
}
}
}