initial commit

This commit is contained in:
ALEZ-DEV 2024-04-17 22:51:00 +02:00
commit f7b2755343
20 changed files with 4513 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
set_github_env.sh

1
README.md Normal file
View File

@ -0,0 +1 @@
# babylonia-terminal-launcher

1
babylonia-terminal-cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1960
babylonia-terminal-cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
[package]
name = "babylonia-terminal-cli"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
babylonia-terminal-sdk = { path = "./../babylonia-terminal-sdk" }
downloader = { git = "https://github.com/ALEZ-DEV/downloader" } # version = "0.2.7",
indicatif = "0.17.8"
log = "0.4.21"
simple_logger = "4.3.3"
tokio = { version = "1.37.0", features = ["full"] }
wincompatlib = "0.7.4"

Binary file not shown.

View File

@ -0,0 +1,76 @@
use babylonia_terminal_sdk::{game_manager::GameManager, game_state::GameState};
use log::{info, LevelFilter};
use simple_logger::SimpleLogger;
use wincompatlib::prelude::*;
pub mod reporter;
use crate::reporter::DownloadReporter;
#[tokio::main]
async fn main() {
SimpleLogger::new()
.with_module_level("hyper", LevelFilter::Off)
.with_module_level("hyper_util", LevelFilter::Off)
.with_module_level("tracing", LevelFilter::Off)
.with_module_level("rustls", LevelFilter::Off)
.with_module_level("minreq", LevelFilter::Off)
.init()
.unwrap();
let mut wine: Option<Wine> = None;
loop {
let state = GameState::get_current_state().await;
if state != GameState::WineNotInstalled && wine.is_none() {
wine = Some(GameManager::init_wine());
}
match state {
GameState::WineNotInstalled => {
info!("Wine not installed, installing it...");
GameManager::install_wine(
GameState::get_config_directory(),
Some(DownloadReporter::create()),
)
.await
.expect("Failed to install Wine");
info!("Wine installed");
}
GameState::DXVKNotInstalled => {
info!("DXVK not installed, installing it...");
GameManager::install_dxvk(
&wine.clone().unwrap(),
GameState::get_config_directory(),
Some(DownloadReporter::create()),
)
.await
.expect("Failed to installed DXVK");
info!("DXVK installed");
}
GameState::FontNotInstalled => {
info!("Fonts not installed, installing it...");
GameManager::install_font(&wine.clone().unwrap(), DownloadReporter::create())
.await
.expect("Failed to install fonts");
info!("Fonts installed");
}
GameState::DependecieNotInstalled => {
info!("Dependecies not installed, installing it...");
GameManager::install_dependecies(&wine.clone().unwrap())
.await
.expect("Failed to install dependecies");
info!("Dependecies installed");
}
_ => {}
}
if GameState::get_current_state().await == GameState::GameInstalled {
break;
}
}
info!("Starting game...");
GameManager::start_game(&wine.unwrap()).await;
}

View File

@ -0,0 +1,63 @@
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use std::fmt::Write;
struct DownloadReporterPrivate {
last_update: std::time::Instant,
max_progress: Option<u64>,
message: String,
pb: ProgressBar,
}
pub struct DownloadReporter {
private: std::sync::Mutex<Option<DownloadReporterPrivate>>,
}
impl DownloadReporter {
pub fn create() -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
private: std::sync::Mutex::new(None),
})
}
}
impl downloader::progress::Reporter for DownloadReporter {
fn setup(&self, max_progress: Option<u64>, message: &str) {
let pb = ProgressBar::new(max_progress.unwrap());
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.white}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap())
.progress_chars("=>-"));
let private = DownloadReporterPrivate {
last_update: std::time::Instant::now(),
max_progress,
message: message.to_owned(),
pb: pb,
};
println!("{}", private.message);
let mut guard = self.private.lock().unwrap();
*guard = Some(private);
}
fn progress(&self, current: u64) {
if let Some(p) = self.private.lock().unwrap().as_mut() {
if p.last_update.elapsed().as_millis() >= 1000 {
p.pb.set_position(current);
p.last_update = std::time::Instant::now();
} else if current == p.max_progress.unwrap() {
p.pb.set_position(current);
p.last_update = std::time::Instant::now();
}
}
}
fn set_message(&self, message: &str) {
println!("File state: {}", message);
}
fn done(&self) {
let mut guard = self.private.lock().unwrap();
*guard = None;
}
}

1
babylonia-terminal-sdk/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1741
babylonia-terminal-sdk/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
[package]
name = "babylonia-terminal-sdk"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.81"
dirs = "5.0.1"
dotenv = "0.15.0"
downloader = { git = "https://github.com/ALEZ-DEV/downloader" } # version = "0.2.7",
flate2 = "1.0.28"
log = "0.4.21"
reqwest = "0.12.2"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
tar = "0.4.40"
tokio = { version = "1.37.0", features = ["fs"] }
wincompatlib = { version = "0.7.4", features = [
"dxvk",
"wine-bundles",
"wine-proton",
"wine-fonts",
"winetricks",
] }
xz = "0.1.0"

View File

@ -0,0 +1,15 @@
use std::{path::PathBuf, sync::Arc};
use downloader::progress::Reporter;
pub trait ComponentDownloader {
async fn install<P: Reporter + 'static>(&self, progress: Option<Arc<P>>) -> anyhow::Result<()>;
//the 'static is something to change, I don't very like it, but it's for testing purpose
async fn download<P: Reporter + 'static>(
output_dir: &PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<PathBuf>;
async fn uncompress(file: PathBuf, new_filename: PathBuf) -> anyhow::Result<()>;
}

View File

@ -0,0 +1,2 @@
pub(crate) mod component_downloader;
pub(crate) mod wine_component;

View File

@ -0,0 +1,107 @@
use std::{
fs::{create_dir, remove_file, rename, File},
path::PathBuf,
sync::Arc,
};
use downloader::{progress::Reporter, Downloader};
use tar::Archive;
use wincompatlib::wine::ext::WineBootExt;
use xz::read::XzDecoder;
use super::component_downloader::ComponentDownloader;
use crate::{game_state::GameState, utils::github_requester::GithubRequester};
pub struct WineComponent {
path: PathBuf,
}
impl GithubRequester for WineComponent {}
impl ComponentDownloader for WineComponent {
async fn install<P: Reporter + 'static>(&self, progress: Option<Arc<P>>) -> anyhow::Result<()> {
let file_output = Self::download(
&self
.path
.parent()
.expect("Failed to get the parent directory of Wine")
.to_path_buf(),
progress,
)
.await?;
Self::uncompress(file_output.clone(), self.path.clone()).await?;
Ok(())
}
async fn download<P: Reporter + 'static>(
output_dir: &PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<PathBuf> {
let release = Self::get_latest_github_release("GloriousEggroll", "wine-ge-custom").await?;
let asset = release[0]
.assets
.get(1)
.expect("Asset not found in the github release");
let mut downloader = Downloader::builder()
.download_folder(output_dir)
.parallel_requests(1)
.build()?;
let mut dl = downloader::Download::new(&asset.browser_download_url);
if let Some(p) = progress {
dl = dl.progress(p);
}
let _result = downloader.async_download(&[dl]).await?;
let file_location = output_dir.join(asset.name.clone());
Ok(file_location)
}
async fn uncompress(file: PathBuf, new_directory_name: PathBuf) -> anyhow::Result<()> {
tokio::task::spawn_blocking(move || {
let tar_xz = File::open(file.clone()).unwrap();
let tar = XzDecoder::new(tar_xz);
let mut archive = Archive::new(tar);
archive
.unpack(new_directory_name.parent().unwrap())
.unwrap();
remove_file(file.clone()).unwrap();
rename(
file.to_str()
.unwrap()
.replace("wine-", "")
.strip_suffix(".tar.xz")
.unwrap(),
new_directory_name,
)
.unwrap();
})
.await
.unwrap();
Ok(())
}
}
impl WineComponent {
pub fn new(path: PathBuf) -> Self {
WineComponent { path }
}
pub fn init_wine(&self) -> wincompatlib::prelude::Wine {
let wine_path = self.path.parent().unwrap().join("bin/wine");
let wine_prefix = self.path.parent().unwrap().join("data");
if !wine_prefix.exists() {
create_dir(wine_prefix.clone()).unwrap()
}
let wine = wincompatlib::prelude::Wine::from_binary(wine_path);
wine.update_prefix(Some(wine_prefix)).unwrap();
wine
}
}

View File

@ -0,0 +1,142 @@
use std::{path::PathBuf, sync::Arc};
use downloader::progress::Reporter;
use downloader::Downloader as FileDownloader;
use reqwest::header::{self, USER_AGENT};
use serde::{Deserialize, Serialize};
use tar::Header;
//use tokio::{fs::File, io};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubRelease {
pub url: String,
#[serde(rename = "assets_url")]
pub assets_url: String,
#[serde(rename = "tag_name")]
pub tag_name: String,
#[serde(rename = "target_commitish")]
pub target_commitish: String,
pub name: String,
pub assets: Vec<Asset>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Asset {
pub url: String,
pub id: i64,
#[serde(rename = "node_id")]
pub node_id: String,
pub name: String,
#[serde(rename = "content_type")]
pub content_type: String,
pub state: String,
pub size: i64,
#[serde(rename = "download_count")]
pub download_count: i64,
#[serde(rename = "created_at")]
pub created_at: String,
#[serde(rename = "updated_at")]
pub updated_at: String,
#[serde(rename = "browser_download_url")]
pub browser_download_url: String,
}
pub struct ComponentsDownloader;
static GITHUB_TOKEN: &'static str = "Bearer <GITHUB_TOKEN>"; //this token can only read public repo
impl ComponentsDownloader {
fn get_client() -> reqwest::Client {
let mut headers = header::HeaderMap::new();
headers.insert(
"Authorization",
header::HeaderValue::from_static(GITHUB_TOKEN),
);
reqwest::Client::builder()
.user_agent(USER_AGENT)
.default_headers(headers)
.build()
.expect("Failed to build https client")
}
async fn get_latest_github_release(
user: &str,
repo_name: &str,
) -> anyhow::Result<Vec<GithubRelease>> {
let response = ComponentsDownloader::get_client()
.get(format!(
"https://api.github.com/repos/{}/{}/releases",
user, repo_name
))
.send()
.await?;
let body = response.text().await?;
let releases: Vec<GithubRelease> = serde_json::from_str(&body)?;
Ok(releases)
}
pub async fn download_latest_wine<P>(
output_dir: &PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<PathBuf>
where
P: Reporter + 'static, //the 'static is something to change, I just made it like this for test prupose
{
let releases =
//ComponentsDownloader::get_latest_github_release("Kron4ek", "Wine-Builds").await?;
ComponentsDownloader::get_latest_github_release("GloriousEggroll", "wine-ge-custom").await?;
let asset = releases[0]
.assets
.get(1)
.expect("Asset not found in the github release");
let mut downloader = FileDownloader::builder()
.download_folder(output_dir)
.parallel_requests(1)
.build()?;
let mut dl = downloader::Download::new(&asset.browser_download_url);
if let Some(p) = progress {
dl = dl.progress(p);
}
let _result = downloader.async_download(&[dl]).await?;
let file_location = output_dir.join(asset.name.clone());
Ok(file_location)
}
pub async fn download_latest_dxvk<P>(
output_dir: &PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<PathBuf>
where
P: Reporter + 'static,
{
let releases = ComponentsDownloader::get_latest_github_release("doitsujin", "dxvk").await?;
let asset = releases[0]
.assets
.first()
.expect("Asset not found in the github release");
let mut downloader = FileDownloader::builder()
.download_folder(output_dir)
.parallel_requests(1)
.build()?;
let mut dl = downloader::Download::new(&asset.browser_download_url);
if let Some(p) = progress {
dl = dl.progress(p);
}
let _result = downloader.async_download(&[dl]).await?;
let file_location = output_dir.join(asset.name.clone());
Ok(file_location)
}
}

View File

@ -0,0 +1,168 @@
use std::{
fs::{create_dir, remove_file, rename, File},
path::PathBuf,
sync::Arc,
time::Duration,
};
use downloader::progress::Reporter;
use flate2::read::GzDecoder;
use log::info;
use tar::Archive;
use tokio::{join, time::sleep};
use wincompatlib::{prelude::*, wine::bundle::proton};
use xz::read::XzDecoder;
use crate::{
components::{component_downloader::ComponentDownloader, wine_component::WineComponent},
components_downloader::ComponentsDownloader,
game_state::GameState,
};
pub struct GameManager;
impl GameManager {
pub async fn install_wine<P>(
config_dir: PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<WineComponent>
where
P: Reporter + 'static,
{
let wine_component = WineComponent::new(config_dir.join("wine"));
wine_component.install(progress).await?;
let mut config = GameState::get_config().await?;
config.is_wine_installed = true;
config.wine_path = Some(
GameState::get_config_directory()
.join("wine")
.to_str()
.unwrap()
.to_string(),
);
GameState::save_config(config).await?;
Ok(wine_component)
}
pub async fn install_dxvk<P>(
wine: &Wine,
config_dir: PathBuf,
progress: Option<Arc<P>>,
) -> anyhow::Result<()>
where
P: Reporter + 'static,
{
let file_output = ComponentsDownloader::download_latest_dxvk(&config_dir, progress)
.await
.expect("failed to download dxvk");
let file_to_uncompress = file_output.clone();
tokio::task::spawn_blocking(move || {
let tar_gz = File::open(file_to_uncompress.clone()).unwrap();
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(config_dir).unwrap();
remove_file(file_to_uncompress.clone()).unwrap();
})
.await
.unwrap();
wine.install_dxvk(
file_output
.to_str()
.unwrap()
.strip_suffix(".tar.gz")
.unwrap(),
InstallParams::default(),
)
.expect("failed to installed DXVK");
let mut config = GameState::get_config().await?;
config.is_dxvk_installed = true;
GameState::save_config(config).await?;
Ok(())
}
pub async fn install_font<P>(wine: &Wine, progress: Arc<P>) -> anyhow::Result<()>
where
P: Reporter + 'static,
{
progress.setup(Some(10), "");
progress.progress(0);
wine.install_font(Font::Arial)?;
progress.progress(1);
wine.install_font(Font::Andale)?;
progress.progress(2);
wine.install_font(Font::Courier)?;
progress.progress(3);
wine.install_font(Font::ComicSans)?;
progress.progress(4);
wine.install_font(Font::Georgia)?;
progress.progress(5);
wine.install_font(Font::Impact)?;
progress.progress(6);
wine.install_font(Font::Times)?;
progress.progress(7);
wine.install_font(Font::Trebuchet)?;
progress.progress(8);
wine.install_font(Font::Verdana)?;
progress.progress(9);
wine.install_font(Font::Webdings)?;
progress.progress(10);
let mut config = GameState::get_config().await?;
config.is_font_installed = true;
GameState::save_config(config).await?;
Ok(())
}
pub async fn install_dependecies(wine: &Wine) -> anyhow::Result<()> {
let winetricks = Winetricks::from_wine("winetricks", wine);
winetricks.install("corefonts")?;
winetricks.install("vcrun2022")?;
let mut config = GameState::get_config().await?;
config.is_dependecies_installed = true;
GameState::save_config(config).await?;
Ok(())
}
pub fn init_wine() -> Wine {
let wine_path = GameState::get_config_directory().join("wine/bin/wine");
let wine_prefix = GameState::get_config_directory().join("data");
if !wine_prefix.exists() {
create_dir(wine_prefix.clone()).unwrap()
}
let wine = Wine::from_binary(wine_path);
wine.update_prefix(Some(wine_prefix)).unwrap();
wine
}
pub fn download_game() {}
pub async fn start_game(wine: &Wine) {
wine.run("/home/alez/.steam/steam/steamapps/compatdata/3841903579/pfx/drive_c/Punishing Gray Raven/launcher.exe").unwrap();
loop {
sleep(Duration::from_millis(10000)).await;
}
}
}

View File

@ -0,0 +1,103 @@
use serde::{Deserialize, Serialize};
use std::{
io::{Read, Write},
path::PathBuf,
};
use tokio::{
fs::{read_to_string, File},
io::{AsyncReadExt, AsyncWriteExt},
};
//use wincompatlib::prelude::*;
#[derive(Debug, PartialEq, Eq)]
pub enum GameState {
WineNotInstalled,
DXVKNotInstalled,
FontNotInstalled,
DependecieNotInstalled, // that's just the missing dll to install
LauncherNotInstalled,
GameNotInstalled,
InstallingGame,
GameInstalled,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GameConfig {
pub dedicated_wine: bool,
pub is_wine_installed: bool,
pub wine_path: Option<String>,
pub is_dxvk_installed: bool,
pub is_font_installed: bool,
pub is_dependecies_installed: bool,
}
impl Default for GameConfig {
fn default() -> Self {
GameConfig {
dedicated_wine: true,
is_wine_installed: false,
wine_path: None,
is_dxvk_installed: false,
is_font_installed: false,
is_dependecies_installed: false,
}
}
}
impl GameState {
pub fn get_config_directory() -> PathBuf {
dirs::home_dir().unwrap().join(".babylonia-terminal")
}
fn get_config_file_path() -> PathBuf {
GameState::get_config_directory().join("babylonia-terminal-config")
}
async fn try_get_config_file() -> anyhow::Result<File> {
let _ = tokio::fs::create_dir(GameState::get_config_directory()).await;
Ok(tokio::fs::File::create(GameState::get_config_file_path()).await?)
}
pub async fn save_config(config: GameConfig) -> anyhow::Result<()> {
let mut file = GameState::try_get_config_file().await?;
let content = serde_json::to_string(&config)?;
file.write_all(content.as_bytes()).await?;
Ok(())
}
pub async fn get_config() -> anyhow::Result<GameConfig> {
let content = match read_to_string(GameState::get_config_file_path()).await {
Err(_) => return Ok(GameConfig::default()),
Ok(c) => c,
};
if let Ok(config) = serde_json::from_str::<GameConfig>(&content) {
Ok(config)
} else {
Ok(GameConfig::default())
}
}
pub async fn get_current_state() -> Self {
let config = GameState::get_config().await.unwrap();
if !config.is_wine_installed {
return GameState::WineNotInstalled;
}
if !config.is_dxvk_installed {
return GameState::DXVKNotInstalled;
}
if !config.is_font_installed {
return GameState::FontNotInstalled;
}
if !config.is_dependecies_installed {
return GameState::DependecieNotInstalled;
}
GameState::GameInstalled
}
}

View File

@ -0,0 +1,5 @@
pub mod components;
pub mod components_downloader;
pub mod game_manager;
pub mod game_state;
pub mod utils;

View File

@ -0,0 +1,84 @@
use std::borrow::{Borrow, BorrowMut};
use reqwest::header::{self, USER_AGENT};
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubRelease {
pub url: String,
#[serde(rename = "assets_url")]
pub assets_url: String,
#[serde(rename = "tag_name")]
pub tag_name: String,
#[serde(rename = "target_commitish")]
pub target_commitish: String,
pub name: String,
pub assets: Vec<Asset>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Asset {
pub url: String,
pub id: i64,
#[serde(rename = "node_id")]
pub node_id: String,
pub name: String,
#[serde(rename = "content_type")]
pub content_type: String,
pub state: String,
pub size: i64,
#[serde(rename = "download_count")]
pub download_count: i64,
#[serde(rename = "created_at")]
pub created_at: String,
#[serde(rename = "updated_at")]
pub updated_at: String,
#[serde(rename = "browser_download_url")]
pub browser_download_url: String,
}
pub struct ComponentsDownloader;
pub trait GithubRequester {
fn get_client() -> reqwest::Client {
let mut headers = None;
if let Ok(github_token) = std::env::var("BT_GITHUB_TOKEN") {
headers = Some(header::HeaderMap::new());
headers.as_mut().unwrap().insert(
"Authorization",
header::HeaderValue::from_str(&format!("Bearer {}", github_token)).unwrap(),
);
}
let mut client = reqwest::Client::builder();
if let Some(h) = headers {
client = client.default_headers(h);
}
client
.user_agent(USER_AGENT)
.build()
.expect("Failed to build https client")
}
async fn get_latest_github_release(
user: &str,
repo_name: &str,
) -> anyhow::Result<Vec<GithubRelease>> {
let response = Self::get_client()
.get(format!(
"https://api.github.com/repos/{}/{}/releases",
user, repo_name
))
.send()
.await?;
let body = response.text().await?;
let releases: Vec<GithubRelease> = serde_json::from_str(&body)?;
Ok(releases)
}
}

View File

@ -0,0 +1 @@
pub mod github_requester;