roblox: convert textures during download

This commit is contained in:
Quaternions 2025-01-31 12:03:57 -08:00
parent 5c8a35fb20
commit 2917fded43

@ -178,6 +178,11 @@ impl DownloadType{
} }
} }
} }
enum DownloadResult{
Cached(PathBuf),
Data(Vec<u8>),
Failed,
}
#[derive(Default,Debug)] #[derive(Default,Debug)]
struct Stats{ struct Stats{
total_assets:u32, total_assets:u32,
@ -186,14 +191,14 @@ struct Stats{
failed_downloads:u32, failed_downloads:u32,
timed_out_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; stats.total_assets+=1;
let download_instruction=download_instruction; let download_instruction=download_instruction;
// check if file exists on disk // check if file exists on disk
let path=download_instruction.path(); let path=download_instruction.path();
if tokio::fs::try_exists(path.as_path()).await?{ if tokio::fs::try_exists(path.as_path()).await?{
stats.cached_assets+=1; stats.cached_assets+=1;
return Ok(()); return Ok(DownloadResult::Cached(path));
} }
let asset_id=download_instruction.asset_id(); let asset_id=download_instruction.asset_id();
// if not, download file // if not, download file
@ -208,15 +213,15 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieConte
match asset_result{ match asset_result{
Ok(asset_result)=>{ Ok(asset_result)=>{
stats.downloaded_assets+=1; stats.downloaded_assets+=1;
tokio::fs::write(path,asset_result).await?; tokio::fs::write(path,&asset_result).await?;
break; break Ok(DownloadResult::Data(asset_result));
}, },
Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{ Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
if scwuab.status_code.as_u16()==429{ if scwuab.status_code.as_u16()==429{
if retry==12{ if retry==12{
println!("Giving up asset download {asset_id}"); println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1; stats.timed_out_downloads+=1;
break; break Ok(DownloadResult::Failed);
} }
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff); println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await; 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{ }else{
stats.failed_downloads+=1; stats.failed_downloads+=1;
println!("weird scuwab error: {scwuab:?}"); println!("weird scuwab error: {scwuab:?}");
break; break Ok(DownloadResult::Failed);
} }
}, },
Err(e)=>{ Err(e)=>{
stats.failed_downloads+=1; stats.failed_downloads+=1;
println!("sadly error: {e}"); 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(()) Ok(())
} }
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
tokio::try_join!( tokio::try_join!(
tokio::fs::create_dir_all("downloaded_textures"), tokio::fs::create_dir_all("downloaded_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"), tokio::fs::create_dir_all("meshes"),
tokio::fs::create_dir_all("unions"), tokio::fs::create_dir_all("unions"),
)?; )?;
// use mpsc // use mpsc
let thread_limit=std::thread::available_parallelism()?.get(); 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 // map decode dispatcher
// read files multithreaded // read files multithreaded
// produce UniqueAssetsResult per file // 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); static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit); SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){ while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send.clone(); let send=send_assets.clone();
tokio::spawn(async move{ tokio::spawn(async move{
let result=unique_assets(path.as_path()).await; let result=unique_assets(path.as_path()).await;
_=send.send(result).await; _=send.send(result).await;
@ -279,32 +342,46 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
// FAST MODE: // FAST MODE:
// acquire one permit // acquire one permit
// pop a job // pop a job
while let Some(result)=recv.recv().await{ let download_thread=tokio::spawn(async move{
let unique_assets=match result{ while let Some(result)=recv_assets.recv().await{
Ok(unique_assets)=>unique_assets, let unique_assets=match result{
Err(e)=>{ Ok(unique_assets)=>unique_assets,
println!("error: {e:?}"); Err(e)=>{
continue; println!("error: {e:?}");
}, continue;
}; },
for texture_id in unique_assets.textures{ };
if globally_unique_assets.textures.insert(RobloxAssetId(texture_id.0)){ for texture_id in unique_assets.textures{
download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?; 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(RobloxAssetId(mesh_id.0)){ }
download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).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(RobloxAssetId(union_id.0)){ }
download_retry(&mut stats,&context,DownloadType::Union(union_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();
});
} }
download_thread.await??;
dbg!(stats); _=SEM.acquire_many(thread_limit as u32).await.unwrap();
Ok(()) Ok(())
} }