Compare commits
29 Commits
loader
...
feature/ex
Author | SHA1 | Date | |
---|---|---|---|
bd94520ef4 | |||
ae19448ee8 | |||
4907362c71 | |||
43b756bf8e | |||
360e76177b | |||
f17a1a86a9 | |||
590062f3c3 | |||
cc4e80db8a | |||
8c0f46007f | |||
48d3fca895 | |||
e833d4c032 | |||
8bd0765290 | |||
894584f855 | |||
0e1b9494c2 | |||
7b1b381064 | |||
91f452a94b | |||
eeac376500 | |||
d1b52b4367 | |||
ae5a451159 | |||
02310c5653 | |||
f55f9c1d55 | |||
b561ffdfc6 | |||
6606d3be51 | |||
f639047980 | |||
9e30f5dca7 | |||
8b7f034f50 | |||
7c1ecbf6da | |||
540f5253d6 | |||
e8c73bbd95 |
@ -1,2 +0,0 @@
|
||||
[registries.strafesnet]
|
||||
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
2720
Cargo.lock
generated
2720
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "map-tool"
|
||||
version = "1.6.0"
|
||||
version = "1.3.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -9,25 +9,18 @@ edition = "2021"
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
flate2 = "1.0.27"
|
||||
futures = "0.3.31"
|
||||
image = "0.25.2"
|
||||
image_dds = "0.7.1"
|
||||
image = "0.24.7"
|
||||
image_dds = "0.1.1"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_asset = { version = "0.2.5", registry = "strafesnet" }
|
||||
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
|
||||
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
|
||||
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
|
||||
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
|
||||
strafesnet_bsp_loader = { version = "0.2.1", registry = "strafesnet" }
|
||||
strafesnet_deferred_loader = { version = "0.4.0", features = ["legacy"], registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { version = "0.5.1", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.2.0", registry = "strafesnet" }
|
||||
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
vbsp = "0.6.0"
|
||||
vmdl = "0.2.0"
|
||||
vmt-parser = "0.2.0"
|
||||
rbx_binary = "0.7.1"
|
||||
rbx_dom_weak = "2.5.0"
|
||||
rbx_reflection_database = "0.2.7"
|
||||
rbx_xml = "0.13.1"
|
||||
vbsp = "0.5.0"
|
||||
vmdl = "0.1.1"
|
||||
vmt-parser = "0.1.1"
|
||||
vpk = "0.2.0"
|
||||
vtf = "0.3.0"
|
||||
vtf = "0.2.1"
|
||||
|
||||
#[profile.release]
|
||||
#lto = true
|
||||
|
28
LICENSE
28
LICENSE
@ -1,23 +1,9 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
MIT License
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
Copyright (c) 2023 StrafesNET Map Tool Developers
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
@ -1,75 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::io::{Read,Seek};
|
||||
use anyhow::Result as AResult;
|
||||
|
||||
fn load_image<R:Read+Seek+std::io::BufRead>(input:&mut R)->AResult<image::DynamicImage>{
|
||||
let mut fourcc=[0u8;4];
|
||||
input.read_exact(&mut fourcc)?;
|
||||
input.rewind()?;
|
||||
match &fourcc{
|
||||
b"\x89PNG"=>Ok(image::load(input,image::ImageFormat::Png)?),
|
||||
b"\xFF\xD8\xFF\xE0"=>Ok(image::load(input,image::ImageFormat::Jpeg)?),//JFIF
|
||||
b"<rob"=>Err(anyhow::Error::msg("Roblox xml garbage is not supported yet")),
|
||||
other=>Err(anyhow::Error::msg(format!("Unknown texture format {:?}",other))),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(file_thing:std::fs::DirEntry) -> AResult<()>{
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
|
||||
let image=load_image(&mut input)?.to_rgba8();//this sets a=255, arcane is actually supposed to look like that
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
//write dds
|
||||
let mut dest=PathBuf::from("textures");
|
||||
dest.push(file_thing.file_name());
|
||||
dest.set_extension("dds");
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
dds.write(&mut writer)?;
|
||||
|
||||
//move file to processed
|
||||
let mut dest=PathBuf::from("textures/processed");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn convert_textures() -> AResult<()>{
|
||||
std::fs::create_dir_all("textures/unprocessed")?;
|
||||
std::fs::create_dir_all("textures/processed")?;
|
||||
let start = std::time::Instant::now();
|
||||
let mut threads=Vec::new();
|
||||
for entry in std::fs::read_dir("textures/unprocessed")? {
|
||||
let file_thing=entry?;
|
||||
threads.push(std::thread::spawn(move ||{
|
||||
let file_name=format!("{:?}",file_thing);
|
||||
let result=convert(file_thing);
|
||||
if let Err(e)=result{
|
||||
println!("error processing file:{:?} error message:{:?}",file_name,e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
let mut i=0;
|
||||
let n_threads=threads.len();
|
||||
for thread in threads{
|
||||
i+=1;
|
||||
if let Err(e)=thread.join(){
|
||||
println!("thread error: {:?}",e);
|
||||
}else{
|
||||
println!("{}/{}",i,n_threads);
|
||||
}
|
||||
}
|
||||
println!("{:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
1273
src/main.rs
1273
src/main.rs
File diff suppressed because it is too large
Load Diff
448
src/roblox.rs
448
src/roblox.rs
@ -1,448 +0,0 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use std::io::{Cursor,Read,Seek};
|
||||
use std::collections::HashSet;
|
||||
use clap::{Args,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
use rbx_dom_weak::Instance;
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
use rbxassetid::RobloxAssetId;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
const DOWNLOAD_LIMIT:usize=16;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands{
|
||||
RobloxToSNF(RobloxToSNFSubcommand),
|
||||
DownloadAssets(DownloadAssetsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct RobloxToSNFSubcommand {
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct DownloadAssetsSubcommand{
|
||||
#[arg(required=true)]
|
||||
roblox_files:Vec<PathBuf>,
|
||||
// #[arg(long)]
|
||||
// cookie_file:Option<String>,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
|
||||
Commands::DownloadAssets(subcommand)=>download_assets(
|
||||
subcommand.roblox_files,
|
||||
rbx_asset::cookie::Cookie::new("".to_string()),
|
||||
).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum LoadDomError{
|
||||
IO(std::io::Error),
|
||||
Binary(rbx_binary::DecodeError),
|
||||
Xml(rbx_xml::DecodeError),
|
||||
UnknownFormat,
|
||||
}
|
||||
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
|
||||
let mut first_8=[0u8;8];
|
||||
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
|
||||
input.rewind().map_err(LoadDomError::IO)?;
|
||||
match &first_8{
|
||||
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
|
||||
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
|
||||
_=>Err(LoadDomError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/* The ones I'm interested in:
|
||||
Beam.Texture
|
||||
Decal.Texture
|
||||
FileMesh.MeshId
|
||||
FileMesh.TextureId
|
||||
MaterialVariant.ColorMap
|
||||
MaterialVariant.MetalnessMap
|
||||
MaterialVariant.NormalMap
|
||||
MaterialVariant.RoughnessMap
|
||||
MeshPart.MeshId
|
||||
MeshPart.TextureID
|
||||
ParticleEmitter.Texture
|
||||
Sky.MoonTextureId
|
||||
Sky.SkyboxBk
|
||||
Sky.SkyboxDn
|
||||
Sky.SkyboxFt
|
||||
Sky.SkyboxLf
|
||||
Sky.SkyboxRt
|
||||
Sky.SkyboxUp
|
||||
Sky.SunTextureId
|
||||
SurfaceAppearance.ColorMap
|
||||
SurfaceAppearance.MetalnessMap
|
||||
SurfaceAppearance.NormalMap
|
||||
SurfaceAppearance.RoughnessMap
|
||||
SurfaceAppearance.TexturePack
|
||||
*/
|
||||
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
|
||||
if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(property){
|
||||
let url:&str=content.as_ref();
|
||||
if let Ok(asset_id)=url.parse(){
|
||||
content_list.insert(asset_id);
|
||||
}else{
|
||||
println!("Content failed to parse into AssetID: {:?}",content);
|
||||
}
|
||||
}else{
|
||||
println!("property={} does not exist for class={}",property,object.class.as_str());
|
||||
}
|
||||
}
|
||||
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
|
||||
let mut file=tokio::fs::File::open(path).await?;
|
||||
let mut data=Vec::new();
|
||||
file.read_to_end(&mut data).await?;
|
||||
Ok(Cursor::new(data))
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct UniqueAssets{
|
||||
meshes:HashSet<RobloxAssetId>,
|
||||
unions:HashSet<RobloxAssetId>,
|
||||
textures:HashSet<RobloxAssetId>,
|
||||
}
|
||||
impl UniqueAssets{
|
||||
fn collect(&mut self,object:&Instance){
|
||||
match object.class.as_str(){
|
||||
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
|
||||
"MeshPart"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"TextureID");
|
||||
accumulate_content_id(&mut self.meshes,object,"MeshId");
|
||||
},
|
||||
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
|
||||
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
|
||||
"Sky"=>{
|
||||
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
|
||||
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
|
||||
accumulate_content_id(&mut self.textures,object,"SunTextureId");
|
||||
},
|
||||
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
|
||||
_=>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum UniqueAssetError{
|
||||
IO(std::io::Error),
|
||||
LoadDom(LoadDomError),
|
||||
}
|
||||
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
|
||||
// read entire file
|
||||
let mut assets=UniqueAssets::default();
|
||||
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
|
||||
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
|
||||
for object in dom.into_raw().1.into_values(){
|
||||
assets.collect(&object);
|
||||
}
|
||||
Ok(assets)
|
||||
}
|
||||
enum DownloadType{
|
||||
Texture(RobloxAssetId),
|
||||
Mesh(RobloxAssetId),
|
||||
Union(RobloxAssetId),
|
||||
}
|
||||
impl DownloadType{
|
||||
fn path(&self)->PathBuf{
|
||||
match self{
|
||||
DownloadType::Texture(asset_id)=>format!("downloaded_textures/{}",asset_id.0.to_string()).into(),
|
||||
DownloadType::Mesh(asset_id)=>format!("meshes/{}",asset_id.0.to_string()).into(),
|
||||
DownloadType::Union(asset_id)=>format!("unions/{}",asset_id.0.to_string()).into(),
|
||||
}
|
||||
}
|
||||
fn asset_id(&self)->u64{
|
||||
match self{
|
||||
DownloadType::Texture(asset_id)=>asset_id.0,
|
||||
DownloadType::Mesh(asset_id)=>asset_id.0,
|
||||
DownloadType::Union(asset_id)=>asset_id.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
enum DownloadResult{
|
||||
Cached(PathBuf),
|
||||
Data(Vec<u8>),
|
||||
Failed,
|
||||
}
|
||||
#[derive(Default,Debug)]
|
||||
struct Stats{
|
||||
total_assets:u32,
|
||||
cached_assets:u32,
|
||||
downloaded_assets:u32,
|
||||
failed_downloads:u32,
|
||||
timed_out_downloads:u32,
|
||||
}
|
||||
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(DownloadResult::Cached(path));
|
||||
}
|
||||
let asset_id=download_instruction.asset_id();
|
||||
// if not, download file
|
||||
let mut retry=0;
|
||||
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
|
||||
let mut backoff=1000f32;
|
||||
loop{
|
||||
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id,
|
||||
version:None,
|
||||
}).await;
|
||||
match asset_result{
|
||||
Ok(asset_result)=>{
|
||||
stats.downloaded_assets+=1;
|
||||
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 Ok(DownloadResult::Failed);
|
||||
}
|
||||
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
|
||||
backoff*=BACKOFF_MUL;
|
||||
retry+=1;
|
||||
}else{
|
||||
stats.failed_downloads+=1;
|
||||
println!("weird scuwab error: {scwuab:?}");
|
||||
break Ok(DownloadResult::Failed);
|
||||
}
|
||||
},
|
||||
Err(e)=>{
|
||||
stats.failed_downloads+=1;
|
||||
println!("sadly error: {e}");
|
||||
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(()),
|
||||
};
|
||||
// image::ImageFormat::Png
|
||||
// image::ImageFormat::Jpeg
|
||||
let image=image::load_from_memory(&data)?.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_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
|
||||
tokio::spawn(async move{
|
||||
// move send so it gets dropped when all maps have been decoded
|
||||
// closing the channel
|
||||
let mut it=paths.into_iter();
|
||||
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_assets.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=unique_assets(path.as_path()).await;
|
||||
_=send.send(result).await;
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
});
|
||||
// download manager
|
||||
// insert into global unique assets guy
|
||||
// add to download queue if the asset is globally unique and does not already exist on disk
|
||||
let mut stats=Stats::default();
|
||||
let context=rbx_asset::cookie::CookieContext::new(cookie);
|
||||
let mut globally_unique_assets=UniqueAssets::default();
|
||||
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
|
||||
// SLOW MODE:
|
||||
// acquire all permits
|
||||
// drop all permits
|
||||
// pop one job
|
||||
// if it succeeds go into fast mode
|
||||
// FAST MODE:
|
||||
// acquire one permit
|
||||
// pop a job
|
||||
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();
|
||||
});
|
||||
}
|
||||
download_thread.await??;
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
RobloxRead(strafesnet_rbx_loader::ReadError),
|
||||
RobloxLoad(strafesnet_rbx_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
|
||||
let entire_file=tokio::fs::read(path).await?;
|
||||
|
||||
let model=strafesnet_rbx_loader::read(
|
||||
std::io::Cursor::new(entire_file)
|
||||
).map_err(ConvertError::RobloxRead)?;
|
||||
|
||||
let mut place=model.into_place();
|
||||
place.run_scripts();
|
||||
|
||||
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
let mut it=paths.into_iter();
|
||||
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 output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=convert_to_snf(path.as_path(),output_folder).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
332
src/source.rs
332
src/source.rs
@ -1,332 +0,0 @@
|
||||
use std::path::{Path,PathBuf};
|
||||
use clap::{Args,Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands{
|
||||
SourceToSNF(SourceToSNFSubcommand),
|
||||
ExtractTextures(ExtractTexturesSubcommand),
|
||||
VPKContents(VPKContentsSubcommand),
|
||||
BSPContents(BSPContentsSubcommand),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct SourceToSNFSubcommand {
|
||||
#[arg(long)]
|
||||
output_folder:PathBuf,
|
||||
#[arg(required=true)]
|
||||
input_files:Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct ExtractTexturesSubcommand {
|
||||
#[arg(long)]
|
||||
bsp_file:PathBuf,
|
||||
#[arg(long)]
|
||||
vpk_dir_files:Vec<PathBuf>
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct VPKContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
#[derive(Args)]
|
||||
pub struct BSPContentsSubcommand {
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub async fn run(self)->AResult<()>{
|
||||
match self{
|
||||
Commands::ExtractTextures(subcommand)=>extract_textures(vec![subcommand.bsp_file],subcommand.vpk_dir_files),
|
||||
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder).await,
|
||||
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
|
||||
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum VMTContent{
|
||||
VMT(String),
|
||||
VTF(String),
|
||||
Patch(vmt_parser::material::PatchMaterial),
|
||||
Unsupported,//don't want to deal with whatever vmt variant
|
||||
Unresolved,//could not locate a texture because of vmt content
|
||||
}
|
||||
impl VMTContent{
|
||||
fn vtf(opt:Option<String>)->Self{
|
||||
match opt{
|
||||
Some(s)=>Self::VTF(s),
|
||||
None=>Self::Unresolved,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_some_texture(material:vmt_parser::material::Material)->AResult<VMTContent>{
|
||||
//just grab some texture from somewhere for now
|
||||
Ok(match material{
|
||||
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
|
||||
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
|
||||
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
|
||||
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
|
||||
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
|
||||
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
|
||||
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
|
||||
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
|
||||
_=>return Err(anyhow::Error::msg("vmt failed to parse")),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_vmt<F:Fn(String)->AResult<Option<Vec<u8>>>>(find_stuff:&F,search_name:String)->AResult<vmt_parser::material::Material>{
|
||||
if let Some(stuff)=find_stuff(search_name)?{
|
||||
//decode vmt and then write
|
||||
let stuff=String::from_utf8(stuff)?;
|
||||
let material=vmt_parser::from_str(stuff.as_str())?;
|
||||
println!("vmt material={:?}",material);
|
||||
return Ok(material);
|
||||
}
|
||||
Err(anyhow::Error::msg("vmt not found"))
|
||||
}
|
||||
|
||||
fn recursive_vmt_loader<F:Fn(String)->AResult<Option<Vec<u8>>>>(find_stuff:&F,material:vmt_parser::material::Material)->AResult<Option<Vec<u8>>>{
|
||||
match get_some_texture(material)?{
|
||||
VMTContent::VMT(s)=>recursive_vmt_loader(find_stuff,get_vmt(find_stuff,s)?),
|
||||
VMTContent::VTF(s)=>{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
texture_file_name.push(s);
|
||||
texture_file_name.set_extension("vtf");
|
||||
find_stuff(texture_file_name.into_os_string().into_string().unwrap())
|
||||
},
|
||||
VMTContent::Patch(mat)=>recursive_vmt_loader(find_stuff,
|
||||
mat.resolve(|search_name|{
|
||||
match find_stuff(search_name.to_string())?{
|
||||
Some(bytes)=>Ok(String::from_utf8(bytes)?),
|
||||
None=>Err(anyhow::Error::msg("could not find vmt")),
|
||||
}
|
||||
})?
|
||||
),
|
||||
VMTContent::Unsupported=>{println!("Unsupported vmt");Ok(None)},//print and move on
|
||||
VMTContent::Unresolved=>{println!("Unresolved vmt");Ok(None)},
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
|
||||
std::fs::create_dir_all("textures")?;
|
||||
let vpk_list:Vec<vpk::VPK>=vpk_paths.into_iter().map(|vpk_path|vpk::VPK::read(&vpk_path).expect("vpk file does not exist")).collect();
|
||||
for path in paths{
|
||||
let mut deduplicate=std::collections::HashSet::new();
|
||||
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
|
||||
for texture in bsp.textures(){
|
||||
deduplicate.insert(PathBuf::from(texture.name()));
|
||||
}
|
||||
//dedupe prop models
|
||||
let mut model_dedupe=std::collections::HashSet::new();
|
||||
for prop in bsp.static_props(){
|
||||
model_dedupe.insert(prop.model());
|
||||
}
|
||||
|
||||
//grab texture names from props
|
||||
for model_name in model_dedupe{
|
||||
//.mdl, .vvd, .dx90.vtx
|
||||
let mut path=PathBuf::from(model_name);
|
||||
let file_name=PathBuf::from(path.file_stem().unwrap());
|
||||
path.pop();
|
||||
path.push(file_name);
|
||||
let mut vvd_path=path.clone();
|
||||
let mut vtx_path=path.clone();
|
||||
vvd_path.set_extension("vvd");
|
||||
vtx_path.set_extension("dx90.vtx");
|
||||
match (bsp.pack.get(model_name),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
|
||||
(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
|
||||
match (vmdl::mdl::Mdl::read(mdl_file.as_ref()),vmdl::vvd::Vvd::read(vvd_file.as_ref()),vmdl::vtx::Vtx::read(vtx_file.as_ref())){
|
||||
(Ok(mdl),Ok(vvd),Ok(vtx))=>{
|
||||
let model=vmdl::Model::from_parts(mdl,vtx,vvd);
|
||||
for texture in model.textures(){
|
||||
for search_path in &texture.search_paths{
|
||||
let mut path=PathBuf::from(search_path.as_str());
|
||||
path.push(texture.name.as_str());
|
||||
deduplicate.insert(path);
|
||||
}
|
||||
}
|
||||
},
|
||||
_=>println!("model_name={} error",model_name),
|
||||
}
|
||||
},
|
||||
_=>println!("no model name={}",model_name),
|
||||
}
|
||||
}
|
||||
|
||||
let pack=&bsp.pack;
|
||||
let vpk_list=&vpk_list;
|
||||
std::thread::scope(move|s|{
|
||||
let mut thread_handles=Vec::new();
|
||||
for texture_name in deduplicate{
|
||||
let mut found_texture=false;
|
||||
//LMAO imagine having to write type names
|
||||
let write_image=|mut stuff,write_file_name|{
|
||||
let image=vtf::from_bytes(&mut stuff)?.highres_image.decode(0)?.to_rgba8();
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
//write dds
|
||||
let mut dest=PathBuf::from("textures");
|
||||
dest.push(write_file_name);
|
||||
dest.set_extension("dds");
|
||||
std::fs::create_dir_all(dest.parent().unwrap())?;
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
dds.write(&mut writer)?;
|
||||
Ok::<(),anyhow::Error>(())
|
||||
};
|
||||
let find_stuff=|search_file_name:String|{
|
||||
println!("search_file_name={}",search_file_name);
|
||||
match pack.get(search_file_name.as_str())?{
|
||||
Some(file)=>return Ok(Some(file)),
|
||||
_=>(),
|
||||
}
|
||||
//search pak list
|
||||
for vpk_index in vpk_list{
|
||||
if let Some(vpk_entry)=vpk_index.tree.get(search_file_name.as_str()){
|
||||
return Ok(Some(match vpk_entry.get()?{
|
||||
std::borrow::Cow::Borrowed(bytes)=>bytes.to_vec(),
|
||||
std::borrow::Cow::Owned(bytes)=>bytes,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok::<Option<Vec<u8>>,anyhow::Error>(None)
|
||||
};
|
||||
let loader=|texture_name:String|{
|
||||
let mut texture_file_name=PathBuf::from("materials");
|
||||
//lower case
|
||||
let texture_file_name_lowercase=texture_name.to_lowercase();
|
||||
texture_file_name.push(texture_file_name_lowercase.clone());
|
||||
//remove stem and search for both vtf and vmt files
|
||||
let stem=PathBuf::from(texture_file_name.file_stem().unwrap());
|
||||
texture_file_name.pop();
|
||||
texture_file_name.push(stem);
|
||||
//somehow search for both files
|
||||
let mut texture_file_name_vmt=texture_file_name.clone();
|
||||
texture_file_name.set_extension("vtf");
|
||||
texture_file_name_vmt.set_extension("vmt");
|
||||
if let Some(stuff)=find_stuff(texture_file_name.to_string_lossy().to_string())?{
|
||||
return Ok(Some(stuff))
|
||||
}
|
||||
recursive_vmt_loader(&find_stuff,get_vmt(&find_stuff,texture_file_name_vmt.to_string_lossy().to_string())?)
|
||||
};
|
||||
if let Some(stuff)=loader(texture_name.to_string_lossy().to_string())?{
|
||||
found_texture=true;
|
||||
let texture_name=texture_name.clone();
|
||||
thread_handles.push(s.spawn(move||write_image(stuff,texture_name)));
|
||||
}
|
||||
if !found_texture{
|
||||
println!("no data");
|
||||
}
|
||||
}
|
||||
for thread in thread_handles{
|
||||
match thread.join(){
|
||||
Ok(Err(e))=>println!("write error: {:?}",e),
|
||||
Err(e)=>println!("thread error: {:?}",e),
|
||||
Ok(_)=>(),
|
||||
}
|
||||
}
|
||||
Ok::<(),anyhow::Error>(())
|
||||
})?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
|
||||
let vpk_index=vpk::VPK::read(&vpk_path)?;
|
||||
for (label,entry) in vpk_index.tree.into_iter(){
|
||||
println!("vpk label={} entry={:?}",label,entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bsp_contents(path:PathBuf)->AResult<()>{
|
||||
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
|
||||
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
|
||||
println!("file_name={:?}",file_name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
BspRead(strafesnet_bsp_loader::ReadError),
|
||||
BspLoad(strafesnet_bsp_loader::LoadError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
|
||||
let entire_file=tokio::fs::read(path).await?;
|
||||
|
||||
let bsp=strafesnet_bsp_loader::read(
|
||||
std::io::Cursor::new(entire_file)
|
||||
).map_err(ConvertError::BspRead)?;
|
||||
|
||||
let map=bsp.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::BspLoad)?;
|
||||
|
||||
let mut dest=output_folder;
|
||||
dest.push(path.file_stem().unwrap());
|
||||
dest.set_extension("snfm");
|
||||
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
|
||||
|
||||
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
|
||||
let start=std::time::Instant::now();
|
||||
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
let mut it=paths.into_iter();
|
||||
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 output_folder=output_folder.clone();
|
||||
tokio::spawn(async move{
|
||||
let result=convert_to_snf(path.as_path(),output_folder).await;
|
||||
drop(permit);
|
||||
match result{
|
||||
Ok(())=>(),
|
||||
Err(e)=>println!("Convert error: {e:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
|
||||
|
||||
println!("elapsed={:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user