From 2917fded43e9b45b5f7398367a8a115b1e0e0332 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Fri, 31 Jan 2025 12:03:57 -0800 Subject: [PATCH] roblox: convert textures during download --- src/roblox.rs | 141 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 32 deletions(-) diff --git a/src/roblox.rs b/src/roblox.rs index 0753cd5..d297a1a 100644 --- a/src/roblox.rs +++ b/src/roblox.rs @@ -178,6 +178,11 @@ impl DownloadType{ } } } +enum DownloadResult{ + Cached(PathBuf), + Data(Vec<u8>), + Failed, +} #[derive(Default,Debug)] struct Stats{ total_assets:u32, @@ -186,14 +191,14 @@ struct Stats{ failed_downloads:u32, timed_out_downloads:u32, } -async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<(),std::io::Error>{ +async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{ stats.total_assets+=1; let download_instruction=download_instruction; // check if file exists on disk let path=download_instruction.path(); if tokio::fs::try_exists(path.as_path()).await?{ stats.cached_assets+=1; - return Ok(()); + return Ok(DownloadResult::Cached(path)); } let asset_id=download_instruction.asset_id(); // if not, download file @@ -208,15 +213,15 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieConte match asset_result{ Ok(asset_result)=>{ stats.downloaded_assets+=1; - tokio::fs::write(path,asset_result).await?; - break; + tokio::fs::write(path,&asset_result).await?; + break Ok(DownloadResult::Data(asset_result)); }, Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{ if scwuab.status_code.as_u16()==429{ if retry==12{ println!("Giving up asset download {asset_id}"); stats.timed_out_downloads+=1; - break; + break Ok(DownloadResult::Failed); } println!("Hit roblox rate limit, waiting {:.0}ms...",backoff); tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await; @@ -225,27 +230,85 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieConte }else{ stats.failed_downloads+=1; println!("weird scuwab error: {scwuab:?}"); - break; + break Ok(DownloadResult::Failed); } }, Err(e)=>{ stats.failed_downloads+=1; println!("sadly error: {e}"); - break; + break Ok(DownloadResult::Failed); }, } } +} +#[allow(unused)] +#[derive(Debug)] +enum ConvertTextureError{ + Io(std::io::Error), + Image(image::ImageError), + DDS(image_dds::CreateDdsError), + DDSWrite(image_dds::ddsfile::Error), +} +impl From<std::io::Error> for ConvertTextureError{ + fn from(value:std::io::Error)->Self{ + Self::Io(value) + } +} +impl From<image::ImageError> for ConvertTextureError{ + fn from(value:image::ImageError)->Self{ + Self::Image(value) + } +} +impl From<image_dds::CreateDdsError> for ConvertTextureError{ + fn from(value:image_dds::CreateDdsError)->Self{ + Self::DDS(value) + } +} +impl From<image_dds::ddsfile::Error> for ConvertTextureError{ + fn from(value:image_dds::ddsfile::Error)->Self{ + Self::DDSWrite(value) + } +} +async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{ + let data=match download_result{ + DownloadResult::Cached(path)=>tokio::fs::read(path).await?, + DownloadResult::Data(data)=>data, + DownloadResult::Failed=>return Ok(()), + }; + + let image=image::ImageReader::new(std::io::Cursor::new(data)).decode()?.to_rgba8(); + + // pick format + let format=if image.width()%4!=0||image.height()%4!=0{ + image_dds::ImageFormat::Rgba8UnormSrgb + }else{ + image_dds::ImageFormat::BC7RgbaUnormSrgb + }; + + //this fails if the image dimensions are not a multiple of 4 + let dds=image_dds::dds_from_image( + &image, + format, + image_dds::Quality::Slow, + image_dds::Mipmaps::GeneratedAutomatic, + )?; + + let file_name=format!("textures/{}.dds",asset_id.0); + let mut file=std::fs::File::create(file_name)?; + dds.write(&mut file)?; Ok(()) } async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{ tokio::try_join!( tokio::fs::create_dir_all("downloaded_textures"), + tokio::fs::create_dir_all("textures"), tokio::fs::create_dir_all("meshes"), tokio::fs::create_dir_all("unions"), )?; // use mpsc let thread_limit=std::thread::available_parallelism()?.get(); - let (send,mut recv)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT); + let (send_assets,mut recv_assets)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT); + let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit); // map decode dispatcher // read files multithreaded // produce UniqueAssetsResult per file @@ -256,7 +319,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0); SEM.add_permits(thread_limit); while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){ - let send=send.clone(); + let send=send_assets.clone(); tokio::spawn(async move{ let result=unique_assets(path.as_path()).await; _=send.send(result).await; @@ -279,32 +342,46 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A // FAST MODE: // acquire one permit // pop a job - while let Some(result)=recv.recv().await{ - let unique_assets=match result{ - Ok(unique_assets)=>unique_assets, - Err(e)=>{ - println!("error: {e:?}"); - continue; - }, - }; - for texture_id in unique_assets.textures{ - if globally_unique_assets.textures.insert(RobloxAssetId(texture_id.0)){ - download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?; - } - } - for mesh_id in unique_assets.meshes{ - if globally_unique_assets.meshes.insert(RobloxAssetId(mesh_id.0)){ - download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?; - } - } - for union_id in unique_assets.unions{ - if globally_unique_assets.unions.insert(RobloxAssetId(union_id.0)){ - download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?; + let download_thread=tokio::spawn(async move{ + while let Some(result)=recv_assets.recv().await{ + let unique_assets=match result{ + Ok(unique_assets)=>unique_assets, + Err(e)=>{ + println!("error: {e:?}"); + continue; + }, + }; + for texture_id in unique_assets.textures{ + if globally_unique_assets.textures.insert(texture_id){ + let data=download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?; + send_texture.send((texture_id,data)).await?; + } + } + for mesh_id in unique_assets.meshes{ + if globally_unique_assets.meshes.insert(mesh_id){ + download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?; + } + } + for union_id in unique_assets.unions{ + if globally_unique_assets.unions.insert(union_id){ + download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?; + } } } + dbg!(stats); + Ok::<(),anyhow::Error>(()) + }); + static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0); + SEM.add_permits(thread_limit); + while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){ + tokio::spawn(async move{ + let result=convert_texture(asset_id,download_result).await; + drop(permit); + result.unwrap(); + }); } - - dbg!(stats); + download_thread.await??; + _=SEM.acquire_many(thread_limit as u32).await.unwrap(); Ok(()) }