Initial commit

This commit is contained in:
xavo95 2025-04-15 21:59:17 +07:00
commit 6540a3755a
Signed by: xavo95
GPG key ID: CBF8ADED6DEBB783
18 changed files with 2523 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
/test_game_dl
/test_launcher_dl
/.idea

1630
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

36
Cargo.toml Normal file
View file

@ -0,0 +1,36 @@
[workspace]
resolver = "2"
members = [
"wicked-waifus-archiver/wicked-waifus-archiver-cli",
"wicked-waifus-downloader/wicked-waifus-downloader-cli",
"wicked-waifus-downloader/wicked-waifus-downloader-domain",
"wicked-waifus-downloader/wicked-waifus-downloader-lib"
]
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
clap = { version = "4.5.36", features = ["derive"] }
colog = "1.3.0"
constant_time_eq = "0.4.2"
hex = "0.4.3"
indicatif = "0.17.11"
indicatif-log-bridge = "0.2.3"
log = "0.4.27"
md-5 = "0.10.6"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
thiserror = "2.0.12"
ureq = { version = "3.0.10", features = ["charset", "cookies", "gzip", "json", "platform-verifier", "rustls"] }
url = "2.5.4"
wicked-waifus-downloader-domain = { path = "wicked-waifus-downloader/wicked-waifus-downloader-domain" }
wicked-waifus-downloader-lib = { path = "wicked-waifus-downloader/wicked-waifus-downloader-lib" }
[profile.release]
strip = true # Automatically strip symbols from the binary.
lto = true # Link-time optimization.
opt-level = 3 # Optimization level 3.
codegen-units = 1 # Maximum size reduction optimizations.

View file

@ -0,0 +1,6 @@
[package]
name = "wicked-waifus-archiver-cli"
version.workspace = true
edition.workspace = true
[dependencies]

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,19 @@
[package]
name = "wicked-waifus-downloader-cli"
version.workspace = true
edition.workspace = true
[dependencies]
clap.workspace = true
colog.workspace = true
constant_time_eq.workspace = true
hex.workspace = true
indicatif.workspace = true
indicatif-log-bridge.workspace = true
log.workspace = true
md-5.workspace = true
thiserror.workspace = true
url.workspace = true
wicked-waifus-downloader-domain.workspace = true
wicked-waifus-downloader-lib.workspace = true

View file

@ -0,0 +1,23 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub(crate) enum Error {
#[error("Hex FromHex Error {0}")]
HexFromHex(#[from] hex::FromHexError),
#[error("Url Parse error Error {0}")]
UrlParse(#[from] url::ParseError),
#[error("Io Error {0}")]
Io(#[from] std::io::Error),
#[error("Wicked Waifus Downloader Lib Error {0}")]
WickedWaifusDownloaderLib(#[from] wicked_waifus_downloader_lib::Error),
#[error("No more CDN available")]
CdnListExhausted,
#[error("CRC Mismatch, expected: {0}, got: {1}")]
CrcMismatch(String, String),
#[error("Length mismatch, expected: {0}, got: {1}")]
LengthMismatch(u64, u64),
#[error("Preload selected but no preload switch exist")]
PreloadSwitchMissing,
#[error("Preload selected but no preload data exist")]
PreloadDataMissing,
}

View file

@ -0,0 +1,170 @@
use crate::error::Error;
use crate::utils;
use crate::utils::verify_download;
use log::info;
use url::Url;
use wicked_waifus_downloader_domain::game::{
GameConfig, GameDefaultConfig, GameDefaultConfigV2Config, ResourcesData,
};
use wicked_waifus_downloader_domain::CdnInfo;
use wicked_waifus_downloader_lib::{
download_cdn_asset_in_mem, request_chunk_resources, request_resources,
};
pub(crate) fn download_game(
use_predownload: bool,
use_chunked: bool,
full: bool,
game_config: GameConfig,
proxy: Option<&str>,
path: &str,
) -> Result<(), Error> {
if use_predownload {
game_config
.predownload_switch
.ok_or(Error::PreloadSwitchMissing)?;
let _predownload = game_config.predownload.ok_or(Error::PreloadDataMissing)?;
// TODO: Implement logic for download predownload
unreachable!("Implement logic for download predownload");
}
match game_config.default {
GameDefaultConfig::GameDefaultConfigV2(mut config) => {
utils::sort_cdn_list(&mut config.cdn_list);
let chunk_enabled = game_config.chunk_download_switch.unwrap_or(0) == 1;
if full && use_chunked && chunk_enabled {
download_full_game_chunked(&config.cdn_list, config.config, proxy, path)
} else if full {
download_full_game(&config.cdn_list, config.resources_data, proxy, path)
} else {
download_upgrade_game(&config.cdn_list, config.config, proxy, path)
}
}
GameDefaultConfig::GameDefaultConfigV1(mut config) => {
utils::sort_cdn_list(&mut config.cdn_list);
download_full_game(&config.cdn_list, config.resources_data, proxy, path)
}
}
}
fn try_fetch_from_cdn<T>(
cdn_list: &[CdnInfo],
build_url: impl Fn(&str) -> Option<Url>,
fetch_fn: impl Fn(&str) -> Result<T, Error>,
) -> Result<T, Error> {
cdn_list
.iter()
.find_map(|cdn| {
let url = build_url(&cdn.url)?;
fetch_fn(url.as_str()).ok()
})
.ok_or(Error::CdnListExhausted)
}
fn download_full_game(
cdn_list: &[CdnInfo],
resources_data: ResourcesData,
proxy: Option<&str>,
path: &str,
) -> Result<(), Error> {
let resources = try_fetch_from_cdn(
cdn_list,
|base| Url::parse(base).ok()?.join(&resources_data.resources).ok(),
|url| Ok(request_resources(url, proxy)?),
)?;
let base_url = format!(
"{}/",
resources_data.resources_base_path.trim_end_matches('/')
);
let total_elements = resources.resource.len();
for (i, resource) in resources.resource.iter().enumerate() {
let expected_md5 = hex::decode(&resource.md5)?;
let data = download_resource_file(
cdn_list,
&base_url,
&resource.dest,
resource.size,
&expected_md5,
i,
total_elements,
proxy,
)?;
utils::save_file_safe(path, &resource.dest, &data)?;
}
Ok(())
}
fn download_full_game_chunked(
cdn_list: &[CdnInfo],
resources_data: GameDefaultConfigV2Config,
proxy: Option<&str>,
path: &str,
) -> Result<(), Error> {
let resources = try_fetch_from_cdn(
cdn_list,
|base| Url::parse(base).ok()?.join(&resources_data.index_file).ok(),
|url| Ok(request_chunk_resources(url, proxy)?),
)?;
let base_url = format!("{}/", resources_data.base_url.trim_end_matches('/'));
let total_elements = resources.resource.len();
for (i, resource) in resources.resource.iter().enumerate() {
let expected_md5 = hex::decode(&resource.md5)?;
let data = if let Some(chunk_info) = &resource.chunk_infos {
for _chunk in chunk_info {}
unreachable!("Implement logic for chunk download");
} else {
download_resource_file(
cdn_list,
&base_url,
&resource.dest,
resource.size,
&expected_md5,
i,
total_elements,
proxy,
)?
};
utils::save_file_safe(path, &resource.dest, &data)?;
}
Ok(())
}
fn download_upgrade_game(
_cdn_list: &[CdnInfo],
_resources_data: GameDefaultConfigV2Config,
_proxy: Option<&str>,
_path: &str,
) -> Result<(), Error> {
unreachable!("implement upgrade/patch download");
}
fn download_resource_file(
cdn_list: &[CdnInfo],
base_url: &str,
dest: &str,
expected_length: u64,
expected_md5: &[u8],
cnt: usize,
total_elements: usize,
proxy: Option<&str>,
) -> Result<Vec<u8>, Error> {
try_fetch_from_cdn(
cdn_list,
|base| Url::parse(base).ok()?.join(base_url).ok()?.join(dest).ok(),
|url| {
info!("[{}/{}] Download started: {}", cnt + 1, total_elements, url);
let data = download_cdn_asset_in_mem(url, proxy)?;
verify_download(&data, expected_length, expected_md5)?;
Ok(data)
},
)
}

View file

@ -0,0 +1,46 @@
use crate::error::Error;
use crate::utils;
use crate::utils::verify_download;
use log::{info, warn};
use url::Url;
use wicked_waifus_downloader_domain::launcher::LauncherConfig;
use wicked_waifus_downloader_lib::{download_cdn_asset_in_mem, InstallerInfo};
pub(crate) fn download_launcher(
launcher_config: LauncherConfig,
proxy: Option<&str>,
path: &str,
) -> Result<(), Error> {
let mut info = InstallerInfo::from(launcher_config);
let expected_md5 = hex::decode(&info.expected_md5)?;
utils::sort_cdn_list(&mut info.cdn_list);
info.cdn_list.iter().find_map(|cdn| {
let Ok(url) = Url::parse(&cdn.url).and_then(|base| base.join(&info.installer)) else {
warn!("Invalid URL in CDN list: {}", cdn.url);
return None;
};
info!("Download started: {}", url);
let data = match download_cdn_asset_in_mem(url.as_str(), proxy) {
Ok(data) => data,
Err(err) => {
warn!("Download failed from {}: {:?}", url, err);
return None;
}
};
if let Err(e) = verify_download(&data, info.expected_length, &expected_md5) {
warn!("Verification failed for {}: {:?}", url, e);
return None;
}
if let Err(e) = utils::save_file_safe(path, "installer.zip", &data) {
warn!("Failed to save file: {:?}", e);
return None;
}
Some(())
}).ok_or(Error::CdnListExhausted)
}

View file

@ -0,0 +1,51 @@
mod error;
mod game;
mod launcher;
mod utils;
use crate::game::download_game;
use crate::launcher::download_launcher;
use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper;
use log::info;
fn main() {
let logger = colog::default_builder()
.filter_level(log::LevelFilter::Info)
.build();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
wicked_waifus_downloader_lib::enable_progress_bar(multi);
test_launcher_download();
test_game_download(false, false, true);
}
fn test_launcher_download() {
let launcher_config = wicked_waifus_downloader_lib::request_launcher(
"https://prod-cn-alicdn-gamestarter.kurogame.com/launcher/launcher/10008_Pa0Q0EMFxukjEqX33pF9Uyvdc8MaGPSz/G152/index.json",
None,
).unwrap();
info!("{:#?}", launcher_config);
download_launcher(launcher_config, None, "test_launcher_dl").unwrap();
}
fn test_game_download(use_predownload: bool, use_chunked: bool, full: bool) {
let game_config = wicked_waifus_downloader_lib::request_game(
"https://prod-cn-alicdn-gamestarter.kurogame.com/launcher/game/G152/10008_Pa0Q0EMFxukjEqX33pF9Uyvdc8MaGPSz/index.json",
None,
).unwrap();
info!("{:#?}", game_config);
download_game(
use_predownload,
use_chunked,
full,
game_config,
None,
"test_game_dl",
)
.unwrap();
}

View file

@ -0,0 +1,48 @@
use crate::error::Error;
use constant_time_eq::constant_time_eq;
use md5::{Digest, Md5};
use std::io::Write;
use wicked_waifus_downloader_domain::CdnInfo;
pub(crate) fn save_file_safe(prefix: &str, filename: &str, data: &[u8]) -> Result<String, Error> {
let root = std::path::Path::new(prefix);
let location = root.join(filename);
let parent = location.parent().unwrap();
std::fs::create_dir_all(parent)?;
let mut f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&location)?;
f.write_all(data)?;
f.flush()?;
Ok(location.to_string_lossy().to_string())
}
pub(crate) fn sort_cdn_list(cdn_list: &mut Vec<CdnInfo>) {
// Highest priority first
// TODO: Implement K1 and K2 sorting
cdn_list.sort_by(|a, b| b.p.cmp(&a.p));
}
pub(crate) fn verify_download(
data: &[u8],
expected_length: u64,
expected_md5: &[u8],
) -> Result<(), Error> {
let actual_length = data.len() as u64;
if actual_length != expected_length {
return Err(Error::LengthMismatch(expected_length, actual_length));
}
let actual_md5 = Md5::digest(data);
let actual_md5_bytes = actual_md5.as_slice();
match constant_time_eq(expected_md5, actual_md5_bytes) {
true => Ok(()),
false => Err(Error::CrcMismatch(
hex::encode(expected_md5),
hex::encode(actual_md5_bytes),
)),
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "wicked-waifus-downloader-domain"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true

View file

@ -0,0 +1,122 @@
use crate::{CdnInfo, Experiment, LanguageMap};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameConfig {
pub chunk_download_switch: Option<u32>,
pub default: GameDefaultConfig,
pub key_file_check_switch: Option<u32>,
pub predownload_switch: Option<u32>,
pub predownload: Option<ResourcesData>, // TODO: Might need changes
#[serde(rename = "RHIOptionSwitch")]
pub rhioption_switch: Option<u32>,
#[serde(rename = "RHIOptionList")]
pub rhioption_list: Option<Vec<RHIOption>>,
pub resources_login: Option<ResourcesLogin>,
pub experiment: Option<Experiment>,
pub check_exe_is_running: Option<u32>,
pub hash_cache_check_acc_switch: u32,
pub key_file_check_list: Option<Vec<String>>,
pub fingerprints: Option<Vec<String>>,
}
/// The order V2 before V1, is because they share a common structure and only V2 has extra fields.
/// If V1 it's first will be included in V2 too
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum GameDefaultConfig {
GameDefaultConfigV2(GameDefaultConfigV2),
GameDefaultConfigV1(GameDefaultConfigV1),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDefaultConfigV1 {
pub cdn_list: Vec<CdnInfo>,
#[serde(flatten)]
pub resources_data: ResourcesData,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDefaultConfigV2 {
pub cdn_list: Vec<CdnInfo>,
#[serde(flatten)]
pub resources_data: ResourcesData,
pub config: GameDefaultConfigV2Config,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesData {
pub changelog: LanguageMap,
pub resources: String,
pub resources_base_path: String,
pub resources_diff: ResourcesDiff,
pub resources_exclude_path: Vec<String>,
pub resources_exclude_path_need_update: Vec<String>,
pub sample_hash_switch: u32,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDefaultConfigV2Config {
pub index_file_md5: String,
pub un_compress_size: u64,
pub base_url: String,
pub size: u64,
pub patch_type: String, // TODO: Enum
pub index_file: String,
pub version: String,
pub patch_config: Vec<GameDefaultConfigV2PatchConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDefaultConfigV2PatchConfig {
pub index_file_md5: String,
pub un_compress_size: u64,
pub ext: GameDefaultConfigV2PatchConfigExt,
pub base_url: String,
pub size: u64,
pub index_file: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDefaultConfigV2PatchConfigExt {
pub max_file_size: u64,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesDiff {
pub current_game_info: GameInfo,
pub previous_game_info: Option<GameInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameInfo {
pub file_name: String,
pub md5: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RHIOption {
pub cmd_option: String,
pub is_show: u32,
pub text: LanguageMap,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesLogin {
pub host: String,
pub login_switch: u32,
}

View file

@ -0,0 +1,92 @@
use crate::{CdnInfo, Experiment, LanguageMap};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum LauncherConfig {
LauncherConfigV1(LauncherConfigV1),
LauncherConfigV2(LauncherConfigV2),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherConfigV1 {
pub default: LauncherDefaultConfigV1,
pub crash_init_switch: u32,
pub animate_bg_switch: u32,
pub animate_background: Option<AnimateBackground>,
pub animate_background_language: Option<HashMap<String, AnimateBackground>>,
pub navigation_bar_switch: Option<u32>,
pub navigation_bar_language: Option<LanguageMap>,
pub resources_gray: Option<ResourcesGray>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherDefaultConfigCommon {
pub cdn_list: Vec<CdnInfo>,
pub changelog: LanguageMap,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherDefaultConfigV1 {
#[serde(flatten)]
pub common: LauncherDefaultConfigCommon,
pub installer: String,
#[serde(rename = "installerMD5")]
pub installer_md5: String,
pub installer_size: u64,
pub version: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AnimateBackground {
pub url: String,
pub md5: String,
pub frame_rate: u32,
pub duration_in_second: u32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherConfigV2 {
pub default: LauncherDefaultConfigV2,
pub crash_init_switch: u32,
pub navigation_bar_switch: Option<u32>,
pub navigation_bar_language: Option<LanguageMap>,
pub resources_gray: Option<ResourcesGray>,
pub function_code: Option<FunctionCode>,
pub experiment: Option<Experiment>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherDefaultConfigV2 {
#[serde(flatten)]
pub common: LauncherDefaultConfigCommon,
pub resource: LauncherDefaultConfigResource,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LauncherDefaultConfigResource {
pub file_check_list_md5: String,
pub md5: String,
pub path: String,
pub size: u64,
pub version: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesGray {
pub gray_url: Option<String>,
pub gray_switch: u32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FunctionCode {}

View file

@ -0,0 +1,42 @@
pub mod launcher;
pub mod game;
pub mod resources;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub type LanguageMap = HashMap<String, Option<String>>;
#[derive(Debug, Serialize, Deserialize)]
pub struct CdnInfo {
#[serde(rename = "K1")]
pub k1: u32,
#[serde(rename = "K2")]
pub k2: u32,
#[serde(rename = "P")]
pub p: u32,
pub url: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Experiment {
pub download: Option<ExperimentDownload>,
#[serde(rename = "res_check")]
pub res_check: Option<ExperimentResCheck>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExperimentDownload {
pub download_read_block_timeout: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExperimentResCheck {
pub file_chunk_check_switch: Option<String>,
pub file_size_check_switch: Option<String>,
pub res_valid_check_time_out: Option<String>,
pub file_check_white_list_config: Option<String>,
}

View file

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resources {
pub resource: Vec<Resource>,
pub sample_hash_info: SampleHashInfo,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub dest: String,
pub md5: String,
pub sample_hash: String,
pub size: u64,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SampleHashInfo {
pub sample_num: u32,
pub sample_block_max_size: u32,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChunkedResources {
pub resource: Vec<ChunkedResource>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChunkedResource {
pub dest: String,
pub md5: String,
pub size: u64,
pub chunk_infos: Option<Vec<ChunkInfo>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChunkInfo {
pub start: u64,
pub end: u64,
pub md5: String,
}

View file

@ -0,0 +1,12 @@
[package]
name = "wicked-waifus-downloader-lib"
version.workspace = true
edition.workspace = true
[dependencies]
indicatif.workspace = true
log.workspace = true
serde.workspace = true
thiserror.workspace = true
ureq.workspace = true
wicked-waifus-downloader-domain.workspace = true

View file

@ -0,0 +1,165 @@
use indicatif::{MultiProgress, ProgressBar};
use log::{info, warn};
use std::fmt::Write;
use std::sync::OnceLock;
use serde::de::DeserializeOwned;
use thiserror::Error;
use ureq::http::Response;
use ureq::tls::{RootCerts, TlsConfig};
use ureq::{Agent, Body, Proxy};
use wicked_waifus_downloader_domain::game::GameConfig;
use wicked_waifus_downloader_domain::launcher::LauncherConfig;
use wicked_waifus_downloader_domain::resources::{ChunkedResources, Resources};
use wicked_waifus_downloader_domain::CdnInfo;
#[derive(Error, Debug)]
pub enum Error {
#[error("UREQ Error {0}")]
Ureq(#[from] ureq::Error),
#[error("ToStr Error {0}")]
ToStr(#[from] ureq::http::header::ToStrError),
#[error("ParseInt Error {0}")]
ParseInt(#[from] std::num::ParseIntError),
#[error("Io Error {0}")]
Io(#[from] std::io::Error),
#[error("Content-Length header missing")]
ContentLengthHeaderMissing,
}
pub struct InstallerInfo {
pub cdn_list: Vec<CdnInfo>,
pub installer: String,
pub expected_md5: String,
pub expected_length: u64,
}
impl From<LauncherConfig> for InstallerInfo {
fn from(config: LauncherConfig) -> Self {
match config {
LauncherConfig::LauncherConfigV1(cfg) => {
let default = cfg.default;
let common = default.common;
Self {
cdn_list: common.cdn_list,
installer: default.installer,
expected_md5: default.installer_md5,
expected_length: default.installer_size,
}
}
LauncherConfig::LauncherConfigV2(cfg) => {
let default = cfg.default;
let common = default.common;
let resource = default.resource;
Self {
cdn_list: common.cdn_list,
installer: resource.path,
expected_md5: resource.md5,
expected_length: resource.size,
}
}
}
}
}
static MULTI_PROGRESS_BAR: OnceLock<MultiProgress> = OnceLock::new();
pub fn enable_progress_bar(multi: MultiProgress) {
if MULTI_PROGRESS_BAR.set(multi).is_err() {
warn!("MultiProgress already set, ignoring.");
}
}
macro_rules! define_request_fn {
($name:ident, $ty:ty) => {
pub fn $name(url: &str, proxy: Option<&str>) -> Result<$ty, Error> {
request_json::<$ty>(url, proxy)
}
};
}
define_request_fn!(request_launcher, LauncherConfig);
define_request_fn!(request_game, GameConfig);
define_request_fn!(request_resources, Resources);
define_request_fn!(request_chunk_resources, ChunkedResources);
pub fn download_cdn_asset_in_mem(url: &str, proxy: Option<&str>) -> Result<Vec<u8>, Error> {
let (mut res, len) = prepare_download(url, proxy)?;
let mut bytes = Vec::with_capacity(len);
download_internal(url, &mut res, len as u64, &mut bytes)?;
Ok(bytes)
}
pub fn download_cdn_asset_writer<W: std::io::Write>(
url: &str,
proxy: Option<&str>,
writer: &mut W,
) -> Result<(), Error> {
let (mut res, len) = prepare_download(url, proxy)?;
download_internal(url, &mut res, len as u64, writer)
}
fn get_agent(proxy: Option<&str>) -> Result<Agent, Error> {
Ok(Agent::config_builder()
.tls_config(
TlsConfig::builder()
.root_certs(RootCerts::PlatformVerifier)
.build(),
)
.proxy(proxy.map(Proxy::new).transpose()?)
.build()
.new_agent())
}
#[inline(always)]
fn request_json<T: DeserializeOwned>(url: &str, proxy: Option<&str>) -> Result<T, Error> {
Ok(get_agent(proxy)?
.get(url)
.call()?
.body_mut()
.read_json::<T>()?)
}
fn prepare_download(url: &str, proxy: Option<&str>) -> Result<(Response<Body>, usize), Error> {
let res = get_agent(proxy)?.get(url).call()?;
let content_length = res
.headers()
.get("Content-Length")
.ok_or(Error::ContentLengthHeaderMissing)?
.to_str()?
.parse::<usize>()?;
Ok((res, content_length))
}
fn create_progress_bar(len: u64, multi: &MultiProgress) -> ProgressBar {
let pb = multi.add(ProgressBar::new(len));
pb.set_style(
indicatif::ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})"
)
.unwrap()
.with_key("eta", |state: &indicatif::ProgressState, w: &mut dyn Write| {
write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap();
})
.progress_chars("#>-")
);
pb
}
fn download_internal<W: std::io::Write>(
url: &str,
res: &mut Response<Body>,
len: u64,
writer: &mut W,
) -> Result<(), Error> {
if let Some(multi) = MULTI_PROGRESS_BAR.get() {
let pb = create_progress_bar(len, multi);
std::io::copy(&mut pb.wrap_read(res.body_mut().as_reader()), writer)?;
pb.finish();
multi.remove(&pb);
} else {
std::io::copy(&mut res.body_mut().as_reader(), writer)?;
}
info!("Download completed: {}", url);
Ok(())
}