Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ cafebabe = "0.8.1"
chrono = "0.4.40"
config = "0.15.18"
csv = "1.3"
flate2 = "1.1.9"
flume = { version = "0.11.1", features = ["async", "select"] }
font-loader = "0.11.0"
futures = "0.3.31"
Expand Down Expand Up @@ -68,6 +69,7 @@ strum = "0.27.1"
strum_macros = "0.27.1"
sysinfo = "0.36.0"
systemstat = "0.2.3"
tar = "0.4.45"
tauri = { version = "2.9.5", features = ["protocol-asset"] }
tauri-plugin-clipboard-manager = "2.3.2"
tauri-plugin-deep-link = "2.4.6"
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"deep-link:default",
"dialog:default",
"log:default",
"http:default",
{
"identifier": "http:default",
"allow": [{ "url": "http://127.0.0.1:*/*" }]
},
"os:default",
"process:default"
]
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod instance;
mod intelligence;
mod launch;
mod launcher_config;
mod multiplayer;
mod partial;
mod resource;
mod storage;
Expand Down Expand Up @@ -180,6 +181,10 @@ pub async fn run() {
utils::commands::delete_directory,
utils::commands::retrieve_truetype_font_list,
utils::commands::check_service_availability,
multiplayer::commands::check_terracotta,
multiplayer::commands::launch_terracotta,
multiplayer::commands::download_terracotta,
multiplayer::commands::fetch_port,
])
.setup(|app| {
// init APP_DATA_DIR
Expand Down
88 changes: 88 additions & 0 deletions src-tauri/src/multiplayer/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use std::pin::Pin;
use std::time::Duration;
use std::{ffi::OsStr, fs};

use crate::{
error::SJMCLResult,
multiplayer::helpers::terracotta::{build_download_param, decompress},
multiplayer::models::MultiplayerError,
resource::models::ResourceError,
tasks::commands::schedule_progressive_task_group,
tasks::monitor::TaskMonitor,
};
use serde_json::Value;
use tauri::{path::BaseDirectory, AppHandle, Manager};
use tokio::process::Command;

#[tauri::command]
pub async fn check_terracotta(app: AppHandle) -> SJMCLResult<bool> {
let dir = &app.path().resolve("terracotta", BaseDirectory::AppData)?;
Ok(dir.exists() && fs::read_dir(dir)?.next().is_some())
}

#[tauri::command]
pub async fn launch_terracotta(app: AppHandle) -> SJMCLResult<()> {
let dir = &app.path().resolve("terracotta", BaseDirectory::AppData)?;

for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();

if path.is_file()
&& path.extension()
== if cfg!(target_os = "windows") {
Some(OsStr::new("exe"))
} else {
None
}
{
Command::new(path)
.arg("--hmcl")
.arg(
&app
.path()
.resolve("sjmcl-terracotta", BaseDirectory::Temp)?,
)
.spawn()?;
return Ok(());
}
}
Err(MultiplayerError::ExecutableNotFound.into())
}

#[tauri::command]
pub async fn download_terracotta(app: AppHandle) -> SJMCLResult<()> {
let download_param = build_download_param(&app).await?;
if download_param.is_empty() {
return Err(ResourceError::NoDownloadApi.into());
}
let task_group =
schedule_progressive_task_group(app.clone(), "terracotta".to_string(), download_param, false)
.await?;

let monitor = app.state::<Pin<Box<TaskMonitor>>>();
monitor.wait_for_task_group(&task_group.task_group).await?;

log::info!("Terracotta downloaded, starting decompression...");
decompress(&app).await?;
log::info!("Terracotta decompressed successfully.");
Ok(())
}

#[tauri::command]
pub async fn fetch_port(app: AppHandle) -> SJMCLResult<u16> {
let path = &app
.path()
.resolve("sjmcl-terracotta", BaseDirectory::Temp)?;
for _ in 0..6 {
if path.exists() {
let content = tokio::fs::read_to_string(path).await?;
let json: Value = serde_json::from_str(&content)?;
if let Some(port) = json.get("port").and_then(|v| v.as_u64()) {
return Ok(port as u16);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
Err(MultiplayerError::PortFileNotFound.into())
}
1 change: 1 addition & 0 deletions src-tauri/src/multiplayer/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod terracotta;
95 changes: 95 additions & 0 deletions src-tauri/src/multiplayer/helpers/terracotta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use crate::error::SJMCLResult;
use crate::launcher_config::models::LauncherConfig;
use crate::multiplayer::models::MultiplayerError;
use crate::resource::helpers::misc::{get_download_api, get_source_priority_list};
use crate::resource::models::ResourceType;
use crate::tasks::{download::DownloadParam, PTaskParam};
use flate2::read::GzDecoder;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::sync::Mutex;
use tar::Archive;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use tauri_plugin_http::reqwest;

pub async fn build_download_param(app: &AppHandle) -> SJMCLResult<Vec<PTaskParam>> {
let config = app.state::<Mutex<LauncherConfig>>().lock()?.clone();
let client = app.state::<reqwest::Client>();

let mut param = Vec::<PTaskParam>::new();

let platform = match (&*config.basic_info.os_type, &*config.basic_info.arch) {
("windows", "aarch64") => "windows-arm64",
("windows", _) => "windows-x86_64",
("macos", "aarch64") => "macos-arm64",
("macos", _) => "macos-x86_64",
("linux", "aarch64") => "linux-arm64",
("linux", "riscv64") => "linux-riscv64",
_ => "linux-x86_64",
};
let priority_list = get_source_priority_list(&config);

for source_type in priority_list.iter() {
let api_url = get_download_api(*source_type, ResourceType::Terracotta)?;
match client.get(api_url.clone()).send().await {
Ok(_) => {
let filename = format!("terracotta-0.4.2-{platform}-pkg.tar.gz");
let url = api_url.join(&format!("download/v0.4.2/{filename}"))?;
let path = app
.path()
.resolve("terracotta", BaseDirectory::AppData)?
.join(filename);
log::debug!("{}, {}", url, path.to_str().unwrap());
param.push(PTaskParam::Download(DownloadParam {
src: url,
dest: path,
filename: None,
sha1: None,
}));
break;
}
Err(_) => continue,
}
}

Ok(param)
}

pub async fn decompress(app: &AppHandle) -> SJMCLResult<()> {
let dir = app.path().resolve("terracotta", BaseDirectory::AppData)?;
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();

if path.extension().and_then(|s| s.to_str()) == Some("gz") {
log::info!("Found compressed file: {:?}", path);
let file = fs::File::open(&path)?;
let decompressor = GzDecoder::new(file);
let mut archive = Archive::new(decompressor);
archive.unpack(dir).map_err(|e| {
log::error!("Failed to unpack archive: {}", e);
e
})?;
fs::remove_file(path)?;

#[cfg(unix)]
{
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let ext = path.extension().and_then(|s| s.to_str());
if ext != Some("gz") && ext != Some("pkg") {
fs::set_permissions(&path, PermissionsExt::from_mode(0o755))?;
}
}
}
}

return Ok(());
}
}
Err(MultiplayerError::CompressedFileNotFound.into())
}
3 changes: 3 additions & 0 deletions src-tauri/src/multiplayer/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod commands;
pub mod helpers;
pub mod models;
11 changes: 11 additions & 0 deletions src-tauri/src/multiplayer/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use strum_macros::Display;

#[derive(Debug, Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum MultiplayerError {
ExecutableNotFound,
PortFileNotFound,
CompressedFileNotFound,
}

impl std::error::Error for MultiplayerError {}
2 changes: 2 additions & 0 deletions src-tauri/src/resource/helpers/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub fn get_download_api(source: SourceType, resource_type: ResourceType) -> SJMC
ResourceType::NeoforgeMaven | ResourceType::NeoforgeInstall => Ok(Url::parse("https://maven.neoforged.net/releases/")?),
ResourceType::QuiltMaven => Ok(Url::parse("https://maven.quiltmc.org/repository/release/")?),
ResourceType::QuiltMeta => Ok(Url::parse("https://meta.quiltmc.org/")?),
ResourceType::Terracotta => Ok(Url::parse("https://github.com/burningtnt/Terracotta/releases/")?),
},
SourceType::BMCLAPIMirror => match resource_type {
ResourceType::VersionManifest => Ok(Url::parse("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json")?),
Expand All @@ -72,6 +73,7 @@ pub fn get_download_api(source: SourceType, resource_type: ResourceType) -> SJMC
ResourceType::OptiFine => Ok(Url::parse("https://bmclapi2.bangbang93.com/optifine/")?),
ResourceType::QuiltMaven => Ok(Url::parse("https://bmclapi2.bangbang93.com/maven/")?),
ResourceType::QuiltMeta => Ok(Url::parse("https://bmclapi2.bangbang93.com/quilt-meta/")?),// seems 'not found'
ResourceType::Terracotta => Ok(Url::parse("https://gitee.com/burningtnt/Terracotta/releases/")?),
},
}
}
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/resource/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum ResourceType {
NeoforgeMaven,
QuiltMaven,
QuiltMeta,
Terracotta,
}

#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug, EnumIter)]
Expand Down
23 changes: 22 additions & 1 deletion src-tauri/src/tasks/monitor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::SJMCLResult;
use crate::error::{SJMCLError, SJMCLResult};
use crate::launcher_config::commands::retrieve_launcher_config;
use crate::tasks::download::DownloadTask;
use crate::tasks::events::{GEvent, GEventStatus, PEvent, TEvent};
Expand Down Expand Up @@ -479,6 +479,27 @@ impl TaskMonitor {
}
}

pub async fn wait_for_task_group(&self, task_group: &str) -> SJMCLResult<()> {
loop {
let status = {
let group_map = self.group_map.read().unwrap();
group_map.get(task_group).map(|g| g.status.clone())
};
match status {
Some(GEventStatus::Completed) => return Ok(()),
Some(GEventStatus::Failed) => {
return Err(SJMCLError(format!("Task group '{task_group}' failed")))
}
Some(GEventStatus::Cancelled) => {
return Err(SJMCLError(format!(
"Task group '{task_group}' was cancelled"
)))
}
_ => tokio::time::sleep(std::time::Duration::from_millis(200)).await,
}
}
}

pub fn state_list(&self) -> Vec<PTaskGroupDesc> {
self
.group_map
Expand Down
Loading
Loading