29 Commits

Author SHA1 Message Date
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
11 changed files with 687 additions and 262 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 = "3.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
@@ -12,3 +13,10 @@ 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" iui = "0.3.0"
serde_json = "1.0"
tokio = {version="1", features=["full"]}
druid = "0.7.0"
registry = "1"
[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"

View File

@@ -0,0 +1,186 @@
use crate::AppData;
use druid::{
widget::*, BoxConstraints, Env, Event, EventCtx, FileDialogOptions, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, Selector, Size, UpdateCtx, WidgetExt, WidgetPod,
};
use std::{fs, io, path::PathBuf};
pub const ATTEMPT_INSTALL: Selector = Selector::new("town-of-us-updater.attempt_install");
pub const DETERMINE_AMONG_US_VERSION: Selector =
Selector::new("town-of-us-updater.determine_among_us_version");
pub const COPY_FOLDER: Selector = Selector::new("town-of-us-updater.copy_folder");
pub struct AmongUsLauncherWidget {
pub root: WidgetPod<AppData, Flex<AppData>>,
}
impl AmongUsLauncherWidget {
pub fn build_widget(&mut self, data: &AppData) {
let mut flex: Flex<AppData> = Flex::column();
match &data.app_state {
crate::GlobalAppState::Initializing(_a) => {
let label = Label::new("Initializing...").with_text_size(24.0).center();
flex.add_flex_child(label, 1.0);
self.root = WidgetPod::new(flex);
return;
}
_ => {}
}
// Find existing installs, make buttons for them
let install_iter = fs::read_dir(data.installs_path.clone());
if let Ok(iter) = install_iter {
let mut collection: Vec<Result<fs::DirEntry, io::Error>> = iter.collect();
collection.reverse();
if collection.is_empty() {
if data.among_us_path.is_empty() {
// We did not find an install path, let's add a button to prompt the user
// to find their install path
let open_button = Button::new("Locate Among Us")
.fix_height(45.0)
.on_click(move |ctx, _: &mut AppData, _| {
ctx.submit_command(
druid::commands::SHOW_OPEN_PANEL.with(FileDialogOptions::new()),
);
})
.center();
flex.add_flex_child(open_button, 1.0);
} else {
let label: druid::widget::Label<AppData> =
druid::widget::Label::new("We found Among Us but you have no installs");
flex.add_flex_child(label, 1.0);
}
} else {
// Iterate first to find labels:
let mut flexbox_array: Vec<druid::widget::Flex<AppData>> = Vec::new();
let mut auv_array: Vec<String> = Vec::new();
for i in &collection {
let existing_ver_smash = i.as_ref().unwrap().file_name();
let mut ver_smash_split = existing_ver_smash.to_str().unwrap().split('-');
let auv = ver_smash_split.next().unwrap();
if !auv_array.contains(&auv.to_string()) {
let label_text = format!("Among Us {}", auv);
let flex: druid::widget::Flex<AppData> = druid::widget::Flex::column()
.with_flex_child(
druid::widget::Label::new(label_text.as_str()).with_text_size(24.0),
1.0,
)
.with_default_spacer();
flexbox_array.push(flex);
auv_array.push(auv.to_string());
}
}
println!("Installs list:");
for i in collection {
let existing_ver_smash = i.unwrap().file_name();
let mut ver_smash_split = existing_ver_smash.to_str().unwrap().split('-');
let auv = ver_smash_split.next().unwrap();
let button_string: String =
format!("Town of Us {}", ver_smash_split.next().unwrap());
for (index, j) in auv_array.iter().enumerate() {
if j == auv {
let mut clone: PathBuf = PathBuf::from(data.installs_path.clone());
clone.push(existing_ver_smash.clone());
let mut button_row: Flex<AppData> = Flex::row();
let fybutton = druid::widget::Button::new(button_string.as_str())
.fix_height(45.0)
.center()
.on_click(move |_, _: &mut AppData, _| {
crate::attempt_run_among_us(&clone);
// crate::launch_better_crewlink().unwrap();
});
button_row.add_flex_child(fybutton, 1.0);
// TODO: Uncomment
// let delete_button = druid::widget::Button::new("Delete")
// .fix_height(45.0)
// .on_click(move |_, _: &mut AppData, _| {});
// button_row.add_flex_child(delete_button, 1.0);
flexbox_array
.get_mut(index)
.unwrap()
.add_flex_child(button_row, 1.0);
println!("- {}", existing_ver_smash.clone().to_str().unwrap());
}
}
}
for i in flexbox_array {
flex.add_flex_child(i, 1.0);
}
}
}
self.root = WidgetPod::new(flex);
}
}
impl Widget<AppData> for AmongUsLauncherWidget {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut AppData, env: &Env) {
// println!("{:?}", event);
// println!("{:?}", data);
match event {
Event::MouseDown(_a) => {
// println!("Mouse Down!");
self.root.event(ctx, event, data, env);
}
Event::WindowConnected => {
self.build_widget(data);
ctx.children_changed();
}
Event::KeyDown(evt) => {
if evt.code == druid::Code::F5 {
ctx.submit_command(ATTEMPT_INSTALL);
}
}
_ => {
self.root.event(ctx, event, data, env);
}
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &AppData, env: &Env) {
self.root.lifecycle(ctx, event, data, env);
// println!("Lifecycle!")
}
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &AppData, data: &AppData, env: &Env) {
self.root.update(ctx, data, env);
println!("{:?}", data);
if old_data.among_us_path.is_empty() && !data.among_us_path.is_empty() {
ctx.submit_command(DETERMINE_AMONG_US_VERSION);
// println!("Detect Stuff");
} else if old_data.among_us_version != data.among_us_version {
ctx.submit_command(ATTEMPT_INSTALL);
}
// println!("Update!");
self.build_widget(data);
ctx.children_changed();
}
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &AppData,
env: &Env,
) -> Size {
self.root.layout(ctx, bc, data, env);
// println!("Layout!");
bc.constrain((400.0, 400.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &AppData, env: &Env) {
self.root.paint(ctx, data, env);
// println!("Paint!");
}
}

50
src/among_us_version.rs Normal file
View File

@@ -0,0 +1,50 @@
use druid::Data;
use std::fmt;
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Data)]
pub struct AmongUsVersion {
year: i32,
month: i32,
day: i32,
}
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,
}
}
}

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,79 @@
use std::path::{Path, PathBuf}; // Modules
use std::str; mod among_us_launcher_widget;
use std::*; 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 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, use druid::{
month: i32, commands, widget::*, AppDelegate, AppLauncher, Data, DelegateCtx, Env, Handled, Target,
day: i32, WidgetPod, WindowDesc, WindowId,
};
#[derive(PartialEq, Eq, Data, Clone, Debug)]
pub enum InitializingState {
StartingGUI,
DeterminingVersion,
CopyingAmongUs,
UnmoddingAmongUs,
DownloadingZip,
Unzipping,
CopyingZipIn,
} }
impl fmt::Display for AmongUsVersion { #[derive(PartialEq, Eq, Data, 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,
} }
impl From<&str> for AmongUsVersion { struct Delegate;
fn from(s: &str) -> AmongUsVersion {
let v: Vec<&str> = s.split(".").collect(); #[derive(Data, Clone, Debug)]
AmongUsVersion { pub struct AppData {
year: i32::from_str_radix(v[0], 10).unwrap(), pub among_us_path: String,
month: i32::from_str_radix(v[1], 10).unwrap(), pub installs_path: String,
day: i32::from_str_radix(v[2], 10).unwrap(), pub version: SemVer,
} pub initialized: bool,
} pub among_us_version: AmongUsVersion,
pub data_path: String,
pub app_state: GlobalAppState,
} }
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 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 +83,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 +93,108 @@ 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 AppDelegate<AppData> for Delegate {
determine_among_us_version(String::from(among_us_folder.to_str().unwrap())).unwrap(); 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(DETERMINE_AMONG_US_VERSION);
}
}
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::DETERMINE_AMONG_US_VERSION) {
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);
}
} else if let Some(_a) = cmd.get(among_us_launcher_widget::ATTEMPT_INSTALL) {
data.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(data.among_us_version.to_string().clone()).unwrap();
let version_smash = format!("{}-{}", among_us_version.clone(), ver_url.0.clone()); let version_smash = format!("{}-{}", data.among_us_version, ver_url.0.clone());
let new_installed_path: path::PathBuf = let new_installed_path: PathBuf = [data.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(data.among_us_path.clone(), data.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 =
.iter() [data.installs_path.as_str(), "Among Us"].iter().collect();
.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 +204,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,13 +214,16 @@ 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 = [data.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!(
"Downloading Town of Us... Please be patient! [{}]",
ver_url.1.clone()
);
let zip = reqwest::blocking::get(ver_url.1.clone()) let zip = reqwest::blocking::get(ver_url.1.clone())
.unwrap() .unwrap()
.bytes() .bytes()
@@ -158,18 +231,20 @@ fn main() {
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(data.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 =
[data_path.to_str().unwrap(), root_folder_path.as_str(), "."] [data.data_path.as_str(), root_folder_path.as_str(), "."]
.iter() .iter()
.collect(); .collect();
println!("{}", extracted_path.to_str().unwrap()); println!("{}", extracted_path.to_str().unwrap());
@@ -177,57 +252,122 @@ fn main() {
extracted_path.to_str().unwrap(), extracted_path.to_str().unwrap(),
new_installed_path.to_str().unwrap(), new_installed_path.to_str().unwrap(),
); );
}
data.initialized = !data.initialized;
data.app_state = GlobalAppState::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 { } else {
println!("Modded install already found"); let folder_opt: Option<String> = detect_among_us_folder();
if folder_opt.is_some() {
among_us_folder.push(folder_opt.unwrap());
} }
// Find existing installs, make buttons for them if among_us_folder.exists() {
let install_iter = fs::read_dir(installs_path.clone()); fs::write(existing_file_path, among_us_folder.to_str().unwrap()).unwrap();
match install_iter {
Ok(iter) => {
let mut collection: Vec<Result<fs::DirEntry, io::Error>> = iter.collect();
collection.reverse();
for i in collection {
// for i in iter {
let existing_ver_smash = i.unwrap().file_name();
let mut button = Button::new(&ui, existing_ver_smash.clone().to_str().unwrap());
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...");
}
});
vbox.append(&ui, button, LayoutStrategy::Stretchy);
println!("{}", existing_ver_smash.clone().to_str().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()),
app_state: GlobalAppState::Initializing(InitializingState::StartingGUI),
};
let mut root_column: druid::widget::Flex<AppData> = 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");
}
} }
win.set_child(&ui, vbox); // println!("Checking for updater updates...");
win.show(&ui); // let vals: (String, String) = version_check_thread_handle.join().unwrap();
ui.main();
// let mut install_folder_path = installs_path.clone();
// install_folder_path.push(version_smash);
// fs::create_dir(install_folder_path.clone()).unwrap_or(());
// Copy Among Us Out // TODO: Auto launch latest sanctioned if the user has a setting like that
// Create destination path // Auto launch latest experimental as well
// let executable_path: path::PathBuf = [new_installed_path.to_str().unwrap(), "Among Us.exe"] println!("Launching main window...");
// .iter()
// .collect();
// std::process::Command::new(executable_path.to_str().unwrap()) let _main_menu: druid::MenuDesc<AppData> =
// .spawn() druid::MenuDesc::empty().append(druid::MenuItem::new(
// .unwrap(); 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) // Returns (Version, URL)
@@ -238,33 +378,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 +397,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 +405,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 +437,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 //println!("{:?}", file_bytes);
} }
fn copy_folder_to_target<T: AsRef<path::Path>>(source: T, dest: T) { 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 +457,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 +465,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 +503,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(druid::Data, 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(),
}
}
}