Skip to content

Commit

Permalink
Update and merge main to testing (#11)
Browse files Browse the repository at this point in the history
* Run cargo check instead of cargo test since we don't have tests & BloxstrapRPC added, install script improvements, bug fixes, code cleanup

Co-authored-By: Daniel Conley <[email protected]>

* BloxstrapRPC added, install script improvements, bug fixes, code cleanup

* Fix RPC stuck on "elapsed"

* generate_help in launch.rs

* Replace "client" with "player" for the outdated client notification

* Error handling for

---------

Co-authored-by: Daniel Conley <[email protected]>
  • Loading branch information
WaviestBalloon and danii authored Oct 27, 2023
1 parent 0c97da9 commit 3a143b5
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 104 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
name = "applejuice_cli"
version = "0.1.0"
edition = "2021"

[profile.release]
opt-level = "s"

[dependencies]
discord-rich-presence = "0.2.3"
inotify = "0.10.2"
notify = "6.1.1"
progress_bar = "1.0.5"
reqwest = { version = "0.11.18", features = ["blocking"] }
sdl2 = "0.35.2"
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ApplejuiceCLI
# <img src="assets/crudejuice.png" width=85px> ApplejuiceCLI
ApplejuiceCLI is the backbone and bootstrapper of Applejuice, you can either use the interface or if you're big brain, use the CLI instead. (**Less bloat! Wow!**)

*Applejuice is a manager to get Roblox to run on Linux using Valve's Proton.*
Expand All @@ -11,13 +11,12 @@ ApplejuiceCLI is the backbone and bootstrapper of Applejuice, you can either use
## Installation

### Lazy™
> [!IMPORTANT]
> If compile fails, you might be missing a dependency with SDL. So far, Ubuntu seems to be the one that has issues with compiling, make sure you run `sudo apt-get -y install libsdl2-dev` before installing and it should successfully compile.
1. Run this command:
```bash
git clone https://github.com/WaviestBalloon/ApplejuiceCLI.git ; cd ApplejuiceCLI ; chmod +x ./install.sh ; bash ./install.sh
```
2. Run `applejuicecli --install player` to install the Roblox Player!
```bash
git clone https://github.com/WaviestBalloon/ApplejuiceCLI.git ; cd ApplejuiceCLI ; chmod +x ./install.sh ; bash ./install.sh
```

### Using the install script

Expand Down
5 changes: 5 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ echo "---------------------------"
echo "Installing the Applejuice CLI to /usr/local/bin..."
sudo cp ./target/release/applejuice_cli /usr/local/bin/applejuicecli || doas cp ./target/release/applejuice_cli /usr/local/bin/applejuicecli

echo "Copying asset files..."
mkdir -p ~/.local/share/applejuice
mkdir -p ~/.local/share/applejuice/assets
cp -r ./assets/* ~/.local/share/applejuice/assets

echo "Initialising Applejuice..."
echo "---------------------------"
applejuicecli --init
Expand Down
124 changes: 48 additions & 76 deletions src/args/launch.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
use crate::utils::{setup, terminal::*, argparse, installation, notification::create_notification};
use crate::args::{install};
use crate::configuration;
use std::{process, thread, time};
use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
use inotify::{Inotify, WatchMask, EventMask};
use crate::utils::{argparse, installation, notification::create_notification, setup, terminal::*, rpc};
use std::process;

const HELP_TEXT: &str = "No help information is currently documented\nUsage: ?";
static ACCEPTED_PARAMS: [(&str, &str); 7] = [
("binary", "The binary type to launch, either Player or Studio"),
("channel", "The deployment channel to launch"),
("hash", "The version hash to launch"),
("args", "The protocol arguments to launch with, usually given by a protocol"),
("skipupdatecheck", "Skip checking for updates from clientsettings.roblox.com"),
("debug", "Enable debug notifications"),
("bootstrap", "Bootstrap the latest version (NOT IMPLEMENTED)"),
];

pub fn main(raw_args: &[(String, String)]) {
if argparse::get_param_value_new(&raw_args, "help").is_some() {
help!("Accepted parameters:\n{}", argparse::generate_help(ACCEPTED_PARAMS.to_vec()));
return;
}
let dir_location = setup::get_applejuice_dir();
let binary_type = argparse::get_param_value_new(&raw_args, "binary").unwrap();
let channel = argparse::get_param_value_new(&raw_args, "channel").unwrap();
let version_hash = argparse::get_param_value_new(&raw_args, "hash").unwrap();
let protocol_arguments = argparse::get_param_value_new(&raw_args, "args").unwrap();

let skip_update_check = argparse::get_param_value_new(&raw_args, "skipupdatecheck"); // Optional
let debug_notifications = argparse::get_param_value_new(&raw_args, "debug"); // Optional
let shall_we_bootstrap = argparse::get_param_value_new(&raw_args, "bootstrap"); // Optional
Expand All @@ -28,89 +38,54 @@ pub fn main(raw_args: &[(String, String)]) {
if shall_we_bootstrap.is_some() {
status!("Downloading and installing latest version...");
create_notification(&format!("{}/assets/crudejuice.png", dir_location), "5000", &format!("Updating Roblox {}...", binary_type), &format!("Updating to deployment {latest_version}"));

//args::install()
} else {
let formatted_install_command = format!("--install {} {}", if binary_type == "Player" { "client" } else { "studio" }, if channel == "LIVE" { "" } else { channel });
let formatted_install_command = format!("--install {} {}",
if binary_type == "Player" { "player" } else { "studio" },
if channel == "LIVE" { "" } else { channel }
);
create_notification("dialog-warning", "5000", "Version outdated!", &format!("You are on {} and the latest version for {} is {}\nConsider running \"{}\"", version_hash.replace("version-", ""), channel, latest_version.replace("version-", ""), formatted_install_command));
}
}
}
status!("Protocol parameter(s): {}", protocol_arguments);
if debug_notifications.is_some() { create_notification("dialog-info", "15000", "Debug protocol parameters", protocol_arguments); }
if debug_notifications.is_some() {
create_notification("dialog-info", "15000", "Debug protocol parameters", protocol_arguments);
}

status!("Detecting Proton...");
let installation_configuration = configuration::get_config(version_hash);
let installed_deployment_location = installation_configuration["install_path"].as_str().unwrap();

status!("Starting RPC...");
let client = match DiscordIpcClient::new("1145934604444897410").and_then(|mut client| {
client.connect()?;

let state = format!("Using Roblox {} on Linux!", binary_type.clone());
let payload = activity::Activity::new()
.state(&state)
.details("With Applejuice")
.assets(activity::Assets::new()
.large_image("holy_fuck_juice")
.large_text("Bitdancer Approved"))
.timestamps(activity::Timestamps::new()
.start(time::SystemTime::now()
.duration_since(time::SystemTime::UNIX_EPOCH).unwrap().as_millis() as i64));

client.set_activity(payload)?;
success!("RPC instance started");
if debug_notifications.is_some() { create_notification("dialog-info", "15000", "Debug RPC", "Rich presence connected"); }

// TODO: Get latest log file and tail stdout it for BloxstrapRPC
thread::spawn(|| {
let mut inotify = Inotify::init().expect("Failed to initialise inotify");
let log_directory = format!("{}/prefixdata/pfx/drive_c/users/steamuser/AppData/Local/Roblox/logs/", setup::get_applejuice_dir());
let mut buffer = [0; 1024];

inotify.watches().add(log_directory.clone(), WatchMask::CREATE).expect("Error adding watch");
status!("Waiting for log file on separate thread...");

let mut event = inotify.read_events_blocking(&mut buffer).expect("Failed to read_events");
let file = loop {
match event.next() {
Some(x) => {
let filename = x.name.unwrap().to_string_lossy();
if filename.contains("last.log") {
inotify.watches().remove(x.wd).expect("Error removing watch");
break Some(filename)
}
},
None => break None
}
};
if let Some(file) = file {
success!("Log found: {file} was created - {log_directory}{file}");
inotify.watches().add(format!("{log_directory}{file}"), WatchMask::ACCESS).expect("Error adding watch to tail log file");

let mut event = inotify.into_event_stream(&mut buffer).expect("Failed to into_event_stream");
} else {
warning!("A file was created, but it is not a log file");
}
});

Ok(client)
}) {
Ok(client) => Some(client),
Err(errmsg) => {
warning!("Failed to start RPC instance");
if debug_notifications.is_some() { create_notification("dialog-info", "15000", "Debug RPC", &format!("Rich presence failed to start!\n{}", errmsg)); }
None
}
};
status!("Starting RPC...");
rpc::init_rpc(binary_type.to_owned(), debug_notifications);

status!("Launching Roblox...");
create_notification(&format!("{}/assets/crudejuice.png", dir_location), "5000", &format!("Roblox {} is starting!", binary_type), "");
create_notification(
&format!("{}/assets/crudejuice.png", dir_location),
"5000",
&format!("Roblox {} is starting!", binary_type),
"",
);
let output = process::Command::new(dbg!(format!("{}/proton", installation_configuration["preferred_proton"].as_str().unwrap())))
.env("STEAM_COMPAT_DATA_PATH", format!("{}/prefixdata", dir_location))
.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", format!("{}/not-steam", dir_location))
.env(
"STEAM_COMPAT_DATA_PATH",
format!("{}/prefixdata", dir_location),
)
.env(
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
format!("{}/not-steam", dir_location),
)
.arg("waitforexitandrun")
.arg(format!("{}/{}", installed_deployment_location, if binary_type == "Player" { "RobloxPlayerBeta.exe".to_string() } else { "RobloxStudioBeta.exe".to_string() }))
.arg(format!(
"{}/{}",
installed_deployment_location,
if binary_type == "Player" {
"RobloxPlayerBeta.exe".to_string()
} else {
"RobloxStudioBeta.exe".to_string()
}
))
.arg(protocol_arguments)
.spawn()
.expect("Failed to launch Roblox Player using Proton")
Expand All @@ -119,7 +94,4 @@ pub fn main(raw_args: &[(String, String)]) {

status!("Roblox has exited with code {}", output.code().unwrap_or(0));
create_notification(&format!("{}/assets/crudejuice.png", dir_location), "5000", &format!("Roblox {} has closed", binary_type), &format!("Exit code: {}", output.code().unwrap_or(0)));

status!("Dropping RPC...");
drop(client);
}
1 change: 0 additions & 1 deletion src/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ pub mod help;
pub mod install;
pub mod purge;
pub mod opendata;
pub mod play;
pub mod launch;
3 changes: 1 addition & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ compile_error!("Since you are compiling for Windows, consider using Bloxstrap: h

fn main() {
let args: Vec<String> = env::args().collect();
if !setup::confirm_applejuice_data_folder_existence() && args[1] != "init" { // Initialisation warning
if !setup::confirm_applejuice_data_folder_existence() && args[1] != "--init" { // Initialisation warning
warning!("Applejuice has not been initialised yet! Attempting to initialise...");
args::initialise::main();
status!("Continuing with task...");
Expand All @@ -33,7 +33,6 @@ fn main() {
"install" => args::install::main(&arguments),
"purge" => args::purge::main(arguments.into_iter().map(|item| vec![item]).collect()),
"opendata" => args::opendata::main(),
"play" => args::play::main(),
// TODO: fix this in above code
"launch" => args::launch::main(&arguments),
_ => {
Expand Down
8 changes: 8 additions & 0 deletions src/utils/argparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ pub fn parse_arguments(args: &[String]) -> Vec<(String, String)> {

arguments
}

pub fn generate_help(accepted_params: Vec<(&str, &str)>) -> String {
let mut help_string = String::new();
for (index, (param, description)) in accepted_params.iter().enumerate() {
help_string = format!("{help_string}\t\x1b[1m{param}\x1b[0m - {description}{}", if index == accepted_params.len() - 1 { "" } else { "\n" });
}
help_string
}
43 changes: 34 additions & 9 deletions src/utils/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ impl<'a> Version<'a> {
}
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
client_version_upload: String
}

/* Response error handling for fetch_latest_version fn */
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseErrorMeat {
code: i32,
message: String
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseError {
errors: Vec<ResponseErrorMeat>
}

const PLAYER_EXTRACT_BINDINGS: [(&str, &str); 20] = [
("RobloxApp.zip", ""),
("shaders.zip", "shaders/"),
Expand Down Expand Up @@ -121,12 +140,6 @@ pub fn get_latest_version_hash(binary: &str, channel: &str) -> String {
}

pub fn fetch_latest_version(version: LatestVersion) -> ExactVersion {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
client_version_upload: String
}

status!("Fetching latest version hash...");
let LatestVersion {channel, binary} = version;

Expand All @@ -144,7 +157,19 @@ pub fn fetch_latest_version(version: LatestVersion) -> ExactVersion {
.text()
.unwrap();

let Response {client_version_upload: hash} = from_str(&output).unwrap();
let Response {client_version_upload: hash} = match from_str(&output) {
Ok(json_parsed) => json_parsed,
Err(error) => {
let ResponseError {errors} = from_str(&output).expect(&format!("Failed to parse error response from server.\nResponse: {}\nError: {}", output, error));
match errors[0].code {
1 => { error!("Could not find version details for channel {}, make sure you have spelt the deployment channel name correctly.", channel); },
5 => { error!("The deployment channel {} is restricted by Roblox!", channel); },
_ => { error!("Unknown error response.\nResponse: {}\nError: {}", output, error); }
}

exit(1);
}
};
success!("Resolved hash to {}", hash);
ExactVersion {channel, hash: Cow::Owned(hash)}
}
Expand All @@ -162,14 +187,14 @@ pub fn get_binary_type(package_manifest: Vec<&str>) -> &str {
}
}
if binary.is_empty() {
error!("Could not determine binary type for provided package menifest!");
error!("Could not determine binary type for provided package manifest!");
exit(1);
}

binary
}

pub fn write_appsettings_xml(path: String) {
pub fn write_appsettings_xml(path: String) { // spaghetti
fs::write(format!("{}/AppSettings.xml", path), "\
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Settings>
Expand Down
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub mod proton;
pub mod configuration;
pub mod argparse;
pub mod notification;
pub mod rpc;
13 changes: 9 additions & 4 deletions src/utils/notification.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::process::{self, exit};
use std::process;
use crate::utils::terminal::*;

pub fn create_notification(icon: &str, expire_time: &str, title: &str, body: &str) {
let output = process::Command::new("notify-send")
Expand All @@ -12,9 +13,13 @@ pub fn create_notification(icon: &str, expire_time: &str, title: &str, body: &st

match output {
Ok(_) => { },
Err(errmsg) => {
println!("Failed to create notification, raw: '{}'\nError: {}", icon, errmsg);
exit(1);
Err(errmsg) => { // Do not quit or panic here, since it's a non-critical error
warning!("Failed to create notification, raw: '{}'\nError: {}", icon, errmsg);

if errmsg.to_string().contains("No such file or directory (os error 2)") { // Fallback to default/no icon if we detect a missing file error
warning!("Assuming a asset was missing; falling back to no icon...");
create_notification("", expire_time, title, body);
}
}
}
}
Loading

0 comments on commit 3a143b5

Please sign in to comment.