Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 22 additions & 20 deletions core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ use crate::deeplink_bridge::PlaceDeeplinkError;
use super::types::Status;

pub struct FlowError {
pub user_title: String,
pub user_message: String,
}

impl From<&FlowError> for Status {
fn from(err: &FlowError) -> Self {
Self::Error {
title: err.user_title.clone(),
message: err.user_message.clone(),
}
}
Expand Down Expand Up @@ -132,73 +134,73 @@ impl StepError {
}

// migrate to json config for i18n later
pub fn user_message(&self) -> &str {
pub fn user_message(&self) -> (&str, &str) {
#[allow(clippy::match_same_arms)]
match self {
Self::E0000_GENERIC_ERROR {
error: _,
user_message,
} => match &user_message {
Some(m) => m,
Some(m) => ("Generic Error", m),
None => {
"Internal communication error during download. Please restart the launcher and try again."
("Generic Error", "Internal communication error during download. Please restart the launcher and try again.")
}
},
Self::E1001_FILE_NOT_FOUND { .. } => {
"The downloaded file could not be found. Please try downloading again or check your antivirus and disk permissions."
("File Not Found", "The downloaded file could not be found. Please try downloading again or check your antivirus and disk permissions.")
}
Self::E1002_CORRUPTED_ARCHIVE { .. } => {
"The downloaded file appears to be corrupted. Please try downloading it again."
("Corrupted Archive", "The downloaded file appears to be corrupted. Please try downloading it again.")
}
Self::E1003_DECOMPRESS_ACCESS_DENIED { .. } => {
"We couldn’t extract the files. Please run the launcher as administrator or check your folder permissions."
("Access Denied", "We couldn’t extract the files. Please run the launcher as administrator or check your folder permissions.")
}
Self::E1004_DISK_FULL { .. } => {
"There isn’t enough space on your disk to install Decentraland. Please free up some space and try again."
("Disk Full", "There isn’t enough space on your disk to install Decentraland. Please free up some space and try again.")
}
Self::E1005_DECOMPRESS_OUT_OF_MEMORY { .. } => {
"Your system ran out of memory while installing the game. Try closing other programs or restarting your computer."
("Out of Memory", "Your system ran out of memory while installing the game. Try closing other programs or restarting your computer.")
}
Self::E1006_FILE_DELETE_FAILED { .. } => {
"We couldn’t remove a previous download. Please check your permissions or try restarting the launcher."
("Delete Failed", "We couldn’t remove a previous download. Please check your permissions or try restarting the launcher.")
}
Self::E1007_FILE_CREATE_FAILED { .. } => {
"We couldn’t create a file to download. Please check your permissions or try restarting the launcher."
("Create Failed", "We couldn’t create a file to download. Please check your permissions or try restarting the launcher.")
}
Self::E2001_DOWNLOAD_FAILED { .. } => {
"There was an error while downloading Decentraland. Please check your internet connection and try again."
("Download Failed", "There was an error while downloading Decentraland. Please check your internet connection and try again.")
}
Self::E2002_MISSING_CONTENT_LENGTH { .. } => {
"Failed to get the file size from the server. Please try again later or verify the download URL is reachable."
("Missing Content-Length", "Failed to get the file size from the server. Please try again later or verify the download URL is reachable.")
}
Self::E2003_NETWORK_WRITE_ERROR { .. } => {
"There was an error while saving the downloaded file. Please make sure you have enough disk space and permission to write to the folder."
("Network Write Error", "There was an error while saving the downloaded file. Please make sure you have enough disk space and permission to write to the folder.")
}
Self::E2004_DOWNLOAD_FAILED_HTTP_CODE { .. } => {
"There was an error while downloading Decentraland. Please check your internet connection and try again."
("Download Failed", "There was an error while downloading Decentraland. Please check your internet connection and try again.")
}
Self::E2005_DOWNLOAD_FAILED_FILE_INCOMPLETE { .. } => {
"Downloading file is incomplete due an error. Please check your internet connection and try again."
("File Incomplete", "Downloading file is incomplete due an error. Please check your internet connection and try again.")
}
Self::E2006_DOWNLOAD_FAILED_NETWORK_TIMEOUT => {
"Timeout while downloading Decentraland. Please check your internet connection and try again."
("Network Timeout", "Timeout while downloading Decentraland. Please check your internet connection and try again.")
}
Self::E3001_OPEN_DEEPLINK_TIMEOUT => {
"There was an error while opening the deeplink. Please restart client and try again."
("Deeplink Timeout", "There was an error while opening the deeplink. Please restart client and try again.")
}
Self::E3002_PLACE_DEEPLINK_ERROR { .. } => {
"There was an error while passing the deeplink. Please restart client and try again."
("Deeplink Error", "There was an error while passing the deeplink. Please restart client and try again.")
}
Self::E3003_CANT_GET_VERSION => {
"Version data could not be read. Please delete launcher's data folder."
("Can't Get Version", "Version data could not be read. Please delete launcher's data folder.")
}
}
}
}

impl Display for StepError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.user_message())
write!(f, "{}", self.user_message().1)
}
}

Expand Down
8 changes: 6 additions & 2 deletions core/src/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ impl LaunchFlow {
}

if let Some(e) = last_error {
let user_message = e.error.user_message();
let error = FlowError {
user_message: e.error.user_message().to_owned(),
user_title: user_message.0.to_owned(),
user_message: user_message.1.to_owned(),
};
std::result::Result::Err(error)
} else {
Expand Down Expand Up @@ -255,7 +257,9 @@ impl WorkflowStep<LaunchFlowState, ()> for DownloadStep {
let status = Status::State {
step: Step::Downloading {
progress: 0,
build_type: mode,
bytes_per_second: 0.0,
time_remaining: 0.0,
build_type: mode
},
};
Ok(status)
Expand Down
3 changes: 2 additions & 1 deletion core/src/installs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use std::time::Duration;

pub mod compression;
pub mod downloads;
pub mod download_speed_estimator;

const APP_NAME: &str = "DecentralandLauncherLight";
const EXPLORER_DOWNLOADED_FILENAME: &str = "decentraland.zip";
Expand Down Expand Up @@ -155,7 +156,7 @@ fn get_latest_version(version_data: &Map<String, Value>) -> Result<&str> {
version_data
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!(StepError::E3003_CANT_GET_VERSION.user_message()))
.ok_or_else(|| anyhow!(StepError::E3003_CANT_GET_VERSION.user_message().1))
}

fn get_explorer_launch_path(version: Option<&str>) -> Result<PathBuf> {
Expand Down
60 changes: 60 additions & 0 deletions core/src/installs/download_speed_estimator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Generated by Claude.

use core::f64;
use std::time::Duration;

pub struct DownloadSpeedEstimator {
/// Smoothed bytes per second (exponential moving average).
bytes_per_second: f64,
/// EMA smoothing factor in 0..=1. Lower values = smoother.
alpha: f64,
}

impl DownloadSpeedEstimator {
/// Create a new estimator.
/// `alpha` controls how quickly the estimate reacts to new samples.
/// A value around 0.2–0.3 works well for download progress bars.
pub const fn new(alpha: f64) -> Self {
Self {
bytes_per_second: 0.0,
alpha: alpha.clamp(0.0, 1.0),
}
}

/// Feed a sample of (`bytes_downloaded`, `time_passed`) for the most recent interval.
/// `time_passed` is in seconds.
pub fn update(&mut self, bytes_downloaded: usize, time_passed: Duration) {
if time_passed <= Duration::ZERO {
return;
}

#[allow(clippy::cast_precision_loss)]
let bytes_downloaded = bytes_downloaded as f64;

let sample_bps = bytes_downloaded / time_passed.as_secs_f64();
if self.bytes_per_second == 0.0 {
// First sample — seed the estimate directly.
self.bytes_per_second = sample_bps;
} else {
self.bytes_per_second =
sample_bps.mul_add(self.alpha, self.bytes_per_second * (1.0 - self.alpha));
}
}

/// Current smoothed bytes-per-second estimate.
pub const fn bytes_per_second(&self) -> f64 {
self.bytes_per_second
}

/// Estimated milliseconds remaining to download `bytes_remaining`.ß
/// Returns `f64::INFINITY` if no speed data is available yet.
pub fn time_remaining(&self, bytes_remaining: u64) -> f64 {
if self.bytes_per_second <= 0.0 {
return f64::INFINITY;
}

#[allow(clippy::cast_precision_loss)]
let seconds_remaining = bytes_remaining as f64 / self.bytes_per_second;
seconds_remaining * 1000.0
}
}
10 changes: 10 additions & 0 deletions core/src/installs/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::time::Duration;
use crate::analytics::Analytics;
use crate::analytics::event::Event;
use crate::channel::EventChannel;
use crate::installs::download_speed_estimator::DownloadSpeedEstimator;
use crate::types::{BuildType, Status, Step};
use anyhow::Context;
use std::sync::Arc;
Expand Down Expand Up @@ -113,6 +114,7 @@ pub async fn download_file<T: EventChannel>(
let duration = std::time::Duration::from_millis(500);
let mut tasks = Vec::new();

let mut bytes_per_interval: usize = 0;
let mut downloaded: u64 = 0;
{
let mut file =
Expand All @@ -122,12 +124,15 @@ pub async fn download_file<T: EventChannel>(
})?;
let mut stream = res.bytes_stream();

let mut estimator = DownloadSpeedEstimator::new(0.1);

loop {
match timeout(Duration::from_secs(15), stream.next()).await {
Ok(Some(item)) => {
let chunk = item?;
file.write_all(&chunk)?;

bytes_per_interval = bytes_per_interval.saturating_add(chunk.len());
let new = min(downloaded.saturating_add(chunk.len() as u64), total_size);
downloaded = new;

Expand All @@ -145,6 +150,8 @@ pub async fn download_file<T: EventChannel>(
total_size,
));
tasks.push(task);
estimator.update(bytes_per_interval, duration);
bytes_per_interval = 0;
}

#[allow(
Expand All @@ -158,6 +165,9 @@ pub async fn download_file<T: EventChannel>(
let event: Status = Status::State {
step: Step::Downloading {
progress,
bytes_per_second: estimator.bytes_per_second(),
time_remaining: estimator
.time_remaining(total_size.saturating_sub(downloaded)),
build_type: build_type.clone(),
},
};
Expand Down
17 changes: 14 additions & 3 deletions core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub enum Status {
#[serde(rename_all = "camelCase")]
State { step: Step },
#[serde(rename_all = "camelCase")]
Error { message: String },
Error { title: String, message: String },
}

#[derive(Clone, Serialize)]
Expand All @@ -19,7 +19,13 @@ pub enum Step {
#[serde(rename_all = "camelCase")]
Fetching,
#[serde(rename_all = "camelCase")]
Downloading { progress: u8, build_type: BuildType },
Downloading {
progress: u8,
bytes_per_second: f64,
/// In milliseconds because that's what JavaScript's `Date` uses.
time_remaining: f64,
build_type: BuildType
},
#[serde(rename_all = "camelCase")]
Installing { build_type: BuildType },
#[serde(rename_all = "camelCase")]
Expand All @@ -30,7 +36,12 @@ pub enum Step {
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
pub enum LauncherUpdate {
CheckingForUpdate,
Downloading { progress: Option<u8> },
Downloading {
progress: Option<u8>,
bytes_per_second: f64,
/// In milliseconds because that's what JavaScript's `Date` uses.
time_remaining: Option<f64>
},
DownloadFinished,
InstallingUpdate,
RestartingApp,
Expand Down
Loading
Loading