asset-tool/src/main.rs

1419 lines
43 KiB
Rust
Raw Normal View History

2024-03-08 17:48:47 +00:00
use std::{io::Read,path::PathBuf};
2023-12-31 01:59:40 +00:00
use clap::{Args,Parser,Subcommand};
use anyhow::{anyhow,Result as AResult};
2023-12-31 02:00:51 +00:00
use futures::StreamExt;
2024-07-04 00:24:27 +00:00
use rbx_asset::cloud::{ApiKey,CloudContext};
use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,InventoryItem};
2023-12-31 01:59:40 +00:00
type AssetID=u64;
2024-03-08 17:48:47 +00:00
type AssetIDFileMap=Vec<(AssetID,PathBuf)>;
2024-01-12 01:11:44 +00:00
const CONCURRENT_DECODE:usize=8;
2024-01-06 19:48:05 +00:00
const CONCURRENT_REQUESTS:usize=32;
2023-12-31 01:59:40 +00:00
#[derive(Parser)]
#[command(author,version,about,long_about=None)]
#[command(propagate_version = true)]
struct Cli{
#[command(subcommand)]
command:Commands,
}
#[derive(Subcommand)]
enum Commands{
2024-03-08 17:35:10 +00:00
DownloadHistory(DownloadHistorySubcommand),
Download(DownloadSubcommand),
2024-07-02 03:41:52 +00:00
DownloadDecompile(DownloadDecompileSubcommand),
2024-04-26 00:38:06 +00:00
DownloadGroupInventoryJson(DownloadGroupInventoryJsonSubcommand),
2024-07-10 16:08:16 +00:00
CreateAsset(CreateAssetSubcommand),
2024-07-10 15:35:13 +00:00
CreateAssetMedia(CreateAssetMediaSubcommand),
2024-08-16 22:57:54 +00:00
CreateAssetMedias(CreateAssetMediasSubcommand),
2024-07-10 16:08:16 +00:00
UploadAsset(UpdateAssetSubcommand),
2024-07-10 15:35:13 +00:00
UploadAssetMedia(UpdateAssetMediaSubcommand),
2024-07-02 21:26:14 +00:00
UploadPlace(UpdatePlaceSubcommand),
2024-03-08 17:35:10 +00:00
Compile(CompileSubcommand),
2024-07-02 21:26:14 +00:00
CompileUploadAsset(CompileUploadAssetSubcommand),
CompileUploadPlace(CompileUploadPlaceSubcommand),
2024-03-08 17:35:10 +00:00
Decompile(DecompileSubcommand),
DecompileHistoryIntoGit(DecompileHistoryIntoGitSubcommand),
DownloadAndDecompileHistoryIntoGit(DownloadAndDecompileHistoryIntoGitSubcommand),
2023-12-31 01:59:40 +00:00
}
2024-03-08 17:35:10 +00:00
#[derive(Args)]
struct DownloadHistorySubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
asset_id:AssetID,
2024-07-04 00:24:27 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
output_folder:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
continue_from_versions:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
start_version:Option<u64>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
end_version:Option<u64>,
2024-01-12 19:24:03 +00:00
}
2024-01-17 05:50:35 +00:00
#[derive(Args)]
2024-03-08 17:35:10 +00:00
struct DownloadSubcommand{
2024-07-16 18:10:18 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
output_folder:Option<PathBuf>,
2024-04-20 21:43:02 +00:00
#[arg(required=true)]
asset_ids:Vec<AssetID>,
2024-03-08 17:35:10 +00:00
}
#[derive(Args)]
2024-04-26 00:38:06 +00:00
struct DownloadGroupInventoryJsonSubcommand{
2024-07-04 00:24:27 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-04-26 00:38:06 +00:00
#[arg(long)]
output_folder:Option<PathBuf>,
#[arg(long)]
group:u64,
}
#[derive(Args)]
2024-07-10 16:08:16 +00:00
struct CreateAssetSubcommand{
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
#[arg(long)]
input_file:PathBuf,
#[arg(long)]
model_name:String,
#[arg(long)]
description:Option<String>,
#[arg(long)]
free_model:Option<bool>,
#[arg(long)]
allow_comments:Option<bool>,
}
#[derive(Args)]
2024-07-10 15:35:13 +00:00
struct CreateAssetMediaSubcommand{
2024-07-02 21:26:14 +00:00
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
2024-04-25 04:31:53 +00:00
#[arg(long)]
model_name:String,
#[arg(long)]
2024-04-26 01:50:57 +00:00
description:Option<String>,
#[arg(long)]
2024-04-25 04:31:53 +00:00
input_file:PathBuf,
#[arg(long)]
asset_type:AssetType,
#[arg(long,group="creator",required=true)]
creator_user_id:Option<u64>,
#[arg(long,group="creator",required=true)]
2024-07-03 18:03:58 +00:00
creator_group_id:Option<u64>,
/// Expected price limits how much robux can be spent to create the asset (defaults to 0)
#[arg(long)]
expected_price:Option<u64>,
2024-04-25 04:31:53 +00:00
}
#[derive(Args)]
2024-08-16 22:57:54 +00:00
/// Automatically detect the media type from file extension and generate asset name and description
struct CreateAssetMediasSubcommand{
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
2024-08-17 01:34:28 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-08-16 22:57:54 +00:00
#[arg(long)]
2024-08-17 00:41:55 +00:00
description:Option<String>,
2024-08-16 22:57:54 +00:00
#[arg(long,group="creator",required=true)]
creator_user_id:Option<u64>,
#[arg(long,group="creator",required=true)]
creator_group_id:Option<u64>,
/// Expected price limits how much robux can be spent to create the asset (defaults to 0)
#[arg(long)]
expected_price:Option<u64>,
input_files:Vec<PathBuf>,
}
#[derive(Args)]
2024-07-10 16:08:16 +00:00
struct UpdateAssetSubcommand{
#[arg(long)]
asset_id:AssetID,
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
#[arg(long)]
input_file:PathBuf,
#[arg(long)]
change_name:Option<String>,
#[arg(long)]
change_description:Option<String>,
#[arg(long)]
change_free_model:Option<bool>,
#[arg(long)]
change_allow_comments:Option<bool>,
}
#[derive(Args)]
2024-07-10 15:35:13 +00:00
struct UpdateAssetMediaSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
asset_id:AssetID,
2024-07-02 21:26:14 +00:00
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
#[arg(long)]
input_file:PathBuf,
}
#[derive(Args)]
struct UpdatePlaceSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
place_id:u64,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
universe_id:u64,
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
input_file:PathBuf,
}
#[derive(Args)]
struct CompileSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
input_folder:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
output_file:PathBuf,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-01 18:18:34 +00:00
style:Option<Style>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
template:Option<PathBuf>,
}
#[derive(Args)]
2024-07-02 21:26:14 +00:00
struct CompileUploadAssetSubcommand{
2024-07-02 01:03:36 +00:00
#[arg(long)]
asset_id:AssetID,
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
2024-07-02 21:26:14 +00:00
#[arg(long)]
input_folder:Option<PathBuf>,
2024-07-02 01:03:36 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
style:Option<Style>,
2024-07-02 01:03:36 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
template:Option<PathBuf>,
}
#[derive(Args)]
struct CompileUploadPlaceSubcommand{
2024-07-02 01:03:36 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
place_id:u64,
2024-07-02 01:03:36 +00:00
#[arg(long)]
2024-07-02 21:26:14 +00:00
universe_id:u64,
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
2024-07-02 01:03:36 +00:00
#[arg(long)]
input_folder:Option<PathBuf>,
#[arg(long)]
style:Option<Style>,
#[arg(long)]
template:Option<PathBuf>,
}
#[derive(Args)]
2024-03-08 17:35:10 +00:00
struct DecompileSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
input_file:PathBuf,
2024-04-19 07:01:27 +00:00
#[arg(long)]
output_folder:Option<PathBuf>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-01 18:18:34 +00:00
style:Style,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_template:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_models:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_scripts:Option<bool>,
2024-01-17 05:50:35 +00:00
}
2023-12-31 01:59:40 +00:00
#[derive(Args)]
2024-07-02 03:41:52 +00:00
struct DownloadDecompileSubcommand{
2024-07-04 00:24:27 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-07-02 03:41:52 +00:00
#[arg(long)]
output_folder:Option<PathBuf>,
#[arg(long)]
asset_id:AssetID,
#[arg(long)]
style:Style,
#[arg(long)]
write_template:Option<bool>,
#[arg(long)]
write_models:Option<bool>,
#[arg(long)]
write_scripts:Option<bool>,
}
#[derive(Args)]
2024-03-08 17:35:10 +00:00
struct DecompileHistoryIntoGitSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
input_folder:PathBuf,
//currently output folder must be the current folder due to git2 limitations
//output_folder:cli.output.unwrap(),
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-01 18:18:34 +00:00
style:Style,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
git_committer_name:String,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
git_committer_email:String,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_template:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_models:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_scripts:Option<bool>,
2024-03-08 17:35:10 +00:00
}
#[derive(Args)]
struct DownloadAndDecompileHistoryIntoGitSubcommand{
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
asset_id:AssetID,
2024-07-04 00:24:27 +00:00
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
2024-03-08 17:35:10 +00:00
//currently output folder must be the current folder due to git2 limitations
//output_folder:cli.output.unwrap(),
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-07-01 18:18:34 +00:00
style:Style,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
git_committer_name:String,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-08 17:35:10 +00:00
git_committer_email:String,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_template:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_models:Option<bool>,
2024-04-19 07:01:27 +00:00
#[arg(long)]
2024-03-15 17:52:49 +00:00
write_scripts:Option<bool>,
2024-03-08 17:35:10 +00:00
}
#[derive(Clone,Copy,Debug,clap::ValueEnum)]
2024-07-01 18:18:34 +00:00
enum Style{
2024-03-08 17:35:10 +00:00
Rox,
Rojo,
RoxRojo,
2023-12-31 01:59:40 +00:00
}
2024-07-01 18:18:34 +00:00
impl Style{
fn rox(&self)->rox_compiler::Style{
match self{
Style::Rox=>rox_compiler::Style::Rox,
Style::Rojo=>rox_compiler::Style::Rojo,
Style::RoxRojo=>rox_compiler::Style::RoxRojo,
}
}
}
#[derive(Clone,Copy,Debug,clap::ValueEnum)]
enum AssetType{
Audio,
Decal,
Model,
}
impl AssetType{
fn cloud(&self)->rbx_asset::cloud::AssetType{
match self{
AssetType::Audio=>rbx_asset::cloud::AssetType::Audio,
AssetType::Decal=>rbx_asset::cloud::AssetType::Decal,
AssetType::Model=>rbx_asset::cloud::AssetType::Model,
}
}
}
2023-12-31 01:59:40 +00:00
2023-12-31 02:00:51 +00:00
#[tokio::main]
async fn main()->AResult<()>{
2023-12-31 01:59:40 +00:00
let cli=Cli::parse();
match cli.command{
2024-03-08 17:35:10 +00:00
Commands::DownloadHistory(subcommand)=>download_history(DownloadHistoryConfig{
2024-03-15 17:52:49 +00:00
continue_from_versions:subcommand.continue_from_versions.unwrap_or(false),
2024-03-08 17:35:10 +00:00
end_version:subcommand.end_version,
start_version:subcommand.start_version.unwrap_or(0),
output_folder:subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
2024-07-04 00:24:27 +00:00
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-03-08 17:35:10 +00:00
asset_id:subcommand.asset_id,
}).await,
2024-03-08 17:35:10 +00:00
Commands::Download(subcommand)=>{
let output_folder=subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap());
download_list(
2024-07-16 18:10:18 +00:00
cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-04-20 21:43:02 +00:00
subcommand.asset_ids.into_iter().map(|asset_id|{
2024-03-08 17:35:10 +00:00
let mut path=output_folder.clone();
path.push(asset_id.to_string());
(asset_id,path)
}).collect()
).await
},
2024-07-02 03:41:52 +00:00
Commands::DownloadDecompile(subcommand)=>{
download_decompile(DownloadDecompileConfig{
2024-07-04 00:24:27 +00:00
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-07-02 03:41:52 +00:00
asset_id:subcommand.asset_id,
output_folder:subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
2024-07-02 21:34:54 +00:00
style:subcommand.style.rox(),
2024-07-02 03:41:52 +00:00
write_template:subcommand.write_template.unwrap_or(false),
write_models:subcommand.write_models.unwrap_or(false),
write_scripts:subcommand.write_scripts.unwrap_or(true),
}).await
},
2024-04-26 00:38:06 +00:00
Commands::DownloadGroupInventoryJson(subcommand)=>download_group_inventory_json(
2024-07-04 00:24:27 +00:00
cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-04-26 00:38:06 +00:00
subcommand.group,
subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
).await,
2024-07-10 16:08:16 +00:00
Commands::CreateAsset(subcommand)=>create_asset(CreateAssetConfig{
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
group:subcommand.group_id,
input_file:subcommand.input_file,
model_name:subcommand.model_name,
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
free_model:subcommand.free_model.unwrap_or(false),
allow_comments:subcommand.allow_comments.unwrap_or(false),
}).await,
2024-07-10 15:35:13 +00:00
Commands::CreateAssetMedia(subcommand)=>create_asset_media(CreateAssetMediaConfig{
2024-07-03 19:20:11 +00:00
api_key:api_key_from_args(
2024-07-02 21:26:14 +00:00
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
2024-07-03 19:20:11 +00:00
).await?,
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()),
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
other=>Err(anyhow!("Invalid creator {other:?}"))?,
},
2024-04-26 01:50:57 +00:00
input_file:subcommand.input_file,
asset_type:subcommand.asset_type.cloud(),
2024-04-26 01:50:57 +00:00
model_name:subcommand.model_name,
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
expected_price:subcommand.expected_price,
2024-04-26 01:50:57 +00:00
}).await,
2024-08-16 22:57:54 +00:00
Commands::CreateAssetMedias(subcommand)=>create_asset_medias(CreateAssetMediasConfig{
api_key:api_key_from_args(
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
).await?,
2024-08-17 01:34:28 +00:00
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
2024-08-16 22:57:54 +00:00
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()),
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
other=>Err(anyhow!("Invalid creator {other:?}"))?,
},
2024-08-17 00:41:55 +00:00
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
2024-08-16 22:57:54 +00:00
input_files:subcommand.input_files,
expected_price:subcommand.expected_price,
}).await,
2024-07-10 16:08:16 +00:00
Commands::UploadAsset(subcommand)=>upload_asset(UploadAssetConfig{
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
asset_id:subcommand.asset_id,
group_id:subcommand.group_id,
input_file:subcommand.input_file,
change_name:subcommand.change_name,
change_description:subcommand.change_description,
change_free_model:subcommand.change_free_model,
change_allow_comments:subcommand.change_allow_comments,
}).await,
2024-07-10 15:35:13 +00:00
Commands::UploadAssetMedia(subcommand)=>upload_asset_media(UploadAssetMediaConfig{
2024-07-03 19:20:11 +00:00
api_key:api_key_from_args(
2024-07-02 21:26:14 +00:00
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-07-02 21:26:14 +00:00
asset_id:subcommand.asset_id,
input_file:subcommand.input_file,
}).await,
Commands::UploadPlace(subcommand)=>upload_place(UploadPlaceConfig{
2024-07-03 19:20:11 +00:00
api_key:api_key_from_args(
2024-07-02 21:26:14 +00:00
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-07-02 21:26:14 +00:00
place_id:subcommand.place_id,
universe_id:subcommand.universe_id,
input_file:subcommand.input_file,
}).await,
2024-03-08 17:35:10 +00:00
Commands::Compile(subcommand)=>compile(CompileConfig{
input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
2024-03-08 17:35:10 +00:00
output_file:subcommand.output_file,
template:subcommand.template,
2024-07-02 21:34:54 +00:00
style:subcommand.style.map(|s|s.rox()),
2024-03-08 17:39:27 +00:00
}).await,
2024-07-02 21:26:14 +00:00
Commands::CompileUploadAsset(subcommand)=>compile_upload_asset(CompileUploadAssetConfig{
2024-07-02 01:03:36 +00:00
input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
template:subcommand.template,
2024-07-02 21:34:54 +00:00
style:subcommand.style.map(|s|s.rox()),
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-07-02 01:03:36 +00:00
asset_id:subcommand.asset_id,
group_id:subcommand.group_id,
2024-07-02 01:03:36 +00:00
}).await,
2024-07-02 21:26:14 +00:00
Commands::CompileUploadPlace(subcommand)=>compile_upload_place(CompileUploadPlaceConfig{
input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
template:subcommand.template,
style:subcommand.style.map(|s|s.rox()),
2024-07-03 19:20:11 +00:00
api_key:api_key_from_args(
2024-07-02 21:26:14 +00:00
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-07-02 21:26:14 +00:00
place_id:subcommand.place_id,
universe_id:subcommand.universe_id,
}).await,
2024-03-08 17:35:10 +00:00
Commands::Decompile(subcommand)=>decompile(DecompileConfig{
2024-07-02 21:34:54 +00:00
style:subcommand.style.rox(),
2024-03-08 17:35:10 +00:00
input_file:subcommand.input_file,
output_folder:subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
2024-03-15 17:52:49 +00:00
write_template:subcommand.write_template.unwrap_or(false),
write_models:subcommand.write_models.unwrap_or(false),
write_scripts:subcommand.write_scripts.unwrap_or(true),
2024-01-11 21:59:32 +00:00
}).await,
2024-03-08 17:35:10 +00:00
Commands::DecompileHistoryIntoGit(subcommand)=>decompile_history_into_git(DecompileHistoryConfig{
git_committer_name:subcommand.git_committer_name,
git_committer_email:subcommand.git_committer_email,
input_folder:subcommand.input_folder,
output_folder:std::env::current_dir()?,
2024-07-02 21:34:54 +00:00
style:subcommand.style.rox(),
2024-03-15 17:52:49 +00:00
write_template:subcommand.write_template.unwrap_or(false),
write_models:subcommand.write_models.unwrap_or(false),
write_scripts:subcommand.write_scripts.unwrap_or(true),
2024-01-12 00:06:24 +00:00
}).await,
2024-03-08 17:35:10 +00:00
Commands::DownloadAndDecompileHistoryIntoGit(subcommand)=>download_and_decompile_history_into_git(DownloadAndDecompileHistoryConfig{
git_committer_name:subcommand.git_committer_name,
git_committer_email:subcommand.git_committer_email,
2024-07-04 00:24:27 +00:00
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
2024-07-03 19:20:11 +00:00
).await?,
2024-03-08 17:35:10 +00:00
asset_id:subcommand.asset_id,
output_folder:std::env::current_dir()?,
2024-07-02 21:34:54 +00:00
style:subcommand.style.rox(),
2024-03-15 17:52:49 +00:00
write_template:subcommand.write_template.unwrap_or(false),
write_models:subcommand.write_models.unwrap_or(false),
write_scripts:subcommand.write_scripts.unwrap_or(true),
}).await,
2023-12-31 01:59:40 +00:00
}
}
2024-07-04 00:24:27 +00:00
async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<Cookie>{
let cookie=match (literal,environment,file){
(Some(cookie_literal),None,None)=>cookie_literal,
(None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?,
(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
2024-07-10 17:07:21 +00:00
_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?,
2024-07-04 00:24:27 +00:00
};
Ok(Cookie::new(format!(".ROBLOSECURITY={cookie}")))
}
2024-07-03 19:20:11 +00:00
async fn api_key_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<ApiKey>{
let api_key=match (literal,environment,file){
(Some(api_key_literal),None,None)=>api_key_literal,
(None,Some(api_key_environment),None)=>std::env::var(api_key_environment)?,
(None,None,Some(api_key_file))=>tokio::fs::read_to_string(api_key_file).await?,
_=>Err(anyhow::Error::msg("Illegal api key argument triple"))?,
};
Ok(ApiKey::new(api_key))
}
2024-07-10 16:08:16 +00:00
struct CreateAssetConfig{
cookie:Cookie,
model_name:String,
description:String,
input_file:PathBuf,
group:Option<u64>,
free_model:bool,
allow_comments:bool,
}
async fn create_asset(config:CreateAssetConfig)->AResult<()>{
let resp=CookieContext::new(config.cookie)
.create(rbx_asset::cookie::CreateRequest{
name:config.model_name,
description:config.description,
ispublic:config.free_model,
allowComments:config.allow_comments,
groupId:config.group,
},tokio::fs::read(config.input_file).await?).await?;
println!("UploadResponse={:?}",resp);
Ok(())
}
2024-07-10 15:35:13 +00:00
struct CreateAssetMediaConfig{
2024-07-03 19:20:11 +00:00
api_key:ApiKey,
asset_type:rbx_asset::cloud::AssetType,
2024-04-26 01:50:57 +00:00
model_name:String,
description:String,
input_file:PathBuf,
creator:rbx_asset::cloud::Creator,
expected_price:Option<u64>,
2024-04-26 01:50:57 +00:00
}
async fn get_asset_exp_backoff(
context:&CloudContext,
create_asset_response:&rbx_asset::cloud::CreateAssetResponse
)->Result<rbx_asset::cloud::AssetResponse,rbx_asset::cloud::CreateAssetResponseGetAssetError>{
let mut backoff:u64=0;
loop{
match create_asset_response.try_get_asset(&context).await{
//try again when the operation is not done
Err(rbx_asset::cloud::CreateAssetResponseGetAssetError::Operation(rbx_asset::cloud::OperationError::NotDone))=>(),
//return all other results
other_result=>return other_result,
}
let wait=f32::exp(backoff as f32/3.0)*1000f32;
println!("Operation not complete; waiting {:.0}ms...",wait);
tokio::time::sleep(std::time::Duration::from_millis(wait as u64)).await;
backoff+=1;
}
}
2024-07-10 15:35:13 +00:00
async fn create_asset_media(config:CreateAssetMediaConfig)->AResult<()>{
let context=CloudContext::new(config.api_key);
let asset_response=context
2024-07-03 19:22:39 +00:00
.create_asset(rbx_asset::cloud::CreateAssetRequest{
assetType:config.asset_type,
2024-07-02 21:26:14 +00:00
displayName:config.model_name,
2024-04-28 06:36:21 +00:00
description:config.description,
2024-07-03 19:22:39 +00:00
creationContext:rbx_asset::cloud::CreationContext{
creator:config.creator,
expectedPrice:Some(config.expected_price.unwrap_or(0)),
2024-07-02 21:26:14 +00:00
}
},tokio::fs::read(config.input_file).await?).await?;
//hardcode a 2 second sleep because roblox be slow
println!("Asset submitted, waiting 2s...");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let asset=get_asset_exp_backoff(&context,&asset_response).await?;
println!("CreateResponse={:?}",asset);
2024-07-02 21:26:14 +00:00
Ok(())
}
2024-08-17 01:34:28 +00:00
// complex operation requires both api key and cookie! how horrible! roblox please fix!
2024-08-16 22:57:54 +00:00
struct CreateAssetMediasConfig{
api_key:ApiKey,
2024-08-17 01:34:28 +00:00
cookie:Cookie,
2024-08-16 22:57:54 +00:00
description:String,
input_files:Vec<PathBuf>,
creator:rbx_asset::cloud::Creator,
expected_price:Option<u64>,
}
#[derive(Debug)]
enum CreateAssetMediasError{
NoFileStem(PathBuf),
UnknownFourCC(Option<Vec<u8>>),
}
impl std::fmt::Display for CreateAssetMediasError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for CreateAssetMediasError{}
2024-08-17 01:34:28 +00:00
#[derive(Debug)]
enum DownloadDecalError{
ParseInt(std::num::ParseIntError),
Get(rbx_asset::cookie::GetError),
LoadDom(LoadDomError),
NoFirstInstance,
NoTextureProperty,
TexturePropertyInvalid,
}
impl std::fmt::Display for DownloadDecalError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for DownloadDecalError{}
2024-08-16 22:57:54 +00:00
async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{
let context=CloudContext::new(config.api_key);
2024-08-17 01:34:28 +00:00
let cookie_context=CookieContext::new(config.cookie);
let expected_price=Some(config.expected_price.unwrap_or(0));
2024-08-17 01:45:28 +00:00
let asset_id_list=futures::stream::iter(config.input_files.into_iter()
2024-08-16 22:57:54 +00:00
//step 1: read file, make create request
.map(|path|{
2024-08-17 01:34:28 +00:00
let description=&config.description;
let creator=&config.creator;
2024-08-16 22:57:54 +00:00
let context=&context;
async move{
let model_name=path.file_stem()
.and_then(std::ffi::OsStr::to_str)
2024-08-17 01:34:28 +00:00
.ok_or(CreateAssetMediasError::NoFileStem(path.clone()))?
.to_owned();
2024-08-16 22:57:54 +00:00
let file=tokio::fs::read(path).await?;
let asset_type=match file.get(0..4){
2024-08-17 01:34:28 +00:00
//png
2024-08-16 22:57:54 +00:00
Some(b"\x89PNG")=>rbx_asset::cloud::AssetType::Decal,
//jpeg
Some(b"\xFF\xD8\xFF\xE0")=>rbx_asset::cloud::AssetType::Decal,
//Some("fbx")=>rbx_asset::cloud::AssetType::Model,
//Some("ogg")=>rbx_asset::cloud::AssetType::Audio,
fourcc=>Err(CreateAssetMediasError::UnknownFourCC(fourcc.map(<[u8]>::to_owned)))?,
};
Ok(context.create_asset(rbx_asset::cloud::CreateAssetRequest{
assetType:asset_type,
2024-08-17 01:34:28 +00:00
displayName:model_name,
description:description.clone(),
2024-08-16 22:57:54 +00:00
creationContext:rbx_asset::cloud::CreationContext{
2024-08-17 01:34:28 +00:00
creator:creator.clone(),
expectedPrice:expected_price,
2024-08-16 22:57:54 +00:00
}
},file).await?)
}
}))
//parallel requests
.buffer_unordered(CONCURRENT_REQUESTS)
//step 2: poll operation until it completes (as fast as possible no exp backoff or anything just hammer roblox)
.filter_map(|create_result:AResult<_>|{
let context=&context;
async{
match create_result{
2024-08-17 00:54:23 +00:00
Ok(create_asset_response)=>match get_asset_exp_backoff(context,&create_asset_response).await{
Ok(asset_response)=>Some(asset_response),
2024-08-16 22:57:54 +00:00
Err(e)=>{
eprintln!("operation error: {}",e);
None
},
},
Err(e)=>{
eprintln!("create_asset error: {}",e);
None
},
}
}
})
//step 3: read decal id from operation and download it
2024-08-17 01:34:28 +00:00
.filter_map(|asset_response|{
let parse_result=asset_response.assetId.parse();
async{
match async{
let file=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id:parse_result.map_err(DownloadDecalError::ParseInt)?,
version:None,
}).await.map_err(DownloadDecalError::Get)?;
let dom=load_dom(std::io::Cursor::new(file)).map_err(DownloadDecalError::LoadDom)?;
let instance=dom.get_by_ref(
*dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)?
).ok_or(DownloadDecalError::NoFirstInstance)?;
match instance.properties.get("Texture").ok_or(DownloadDecalError::NoTextureProperty)?{
2024-08-17 01:45:28 +00:00
rbx_dom_weak::types::Variant::Content(url)=>Ok(url.clone().into_string()),
2024-08-17 01:34:28 +00:00
_=>Err(DownloadDecalError::TexturePropertyInvalid),
}
}.await{
2024-08-17 01:45:28 +00:00
Ok(asset_url)=>Some((asset_response.displayName,asset_url)),
2024-08-17 01:34:28 +00:00
Err(e)=>{
eprintln!("get_asset error: {}",e);
None
},
}
}
2024-08-17 01:45:28 +00:00
}).collect::<Vec<(String,String)>>().await;
for (file_name,asset_url) in asset_id_list{
println!("{}={}",file_name,asset_url);
}
2024-08-16 22:57:54 +00:00
Ok(())
}
2024-07-10 16:08:16 +00:00
struct UploadAssetConfig{
cookie:Cookie,
asset_id:AssetID,
change_name:Option<String>,
change_description:Option<String>,
change_free_model:Option<bool>,
change_allow_comments:Option<bool>,
group_id:Option<u64>,
input_file:PathBuf,
}
async fn upload_asset(config:UploadAssetConfig)->AResult<()>{
let context=CookieContext::new(config.cookie);
let resp=context.upload(rbx_asset::cookie::UploadRequest{
assetid:config.asset_id,
name:config.change_name,
description:config.change_description,
ispublic:config.change_free_model,
allowComments:config.change_allow_comments,
groupId:config.group_id,
},tokio::fs::read(config.input_file).await?).await?;
println!("UploadResponse={:?}",resp);
Ok(())
}
2024-07-10 15:35:13 +00:00
struct UploadAssetMediaConfig{
2024-07-03 19:20:11 +00:00
api_key:ApiKey,
2024-07-02 21:26:14 +00:00
asset_id:u64,
input_file:PathBuf,
}
2024-07-10 15:35:13 +00:00
async fn upload_asset_media(config:UploadAssetMediaConfig)->AResult<()>{
2024-07-03 19:20:11 +00:00
let context=CloudContext::new(config.api_key);
2024-07-02 21:26:14 +00:00
let resp=context.update_asset(rbx_asset::cloud::UpdateAssetRequest{
assetId:config.asset_id,
displayName:None,
description:None,
2024-04-28 06:36:21 +00:00
},tokio::fs::read(config.input_file).await?).await?;
println!("UploadResponse={:?}",resp);
2024-04-25 04:31:53 +00:00
Ok(())
}
2024-07-02 21:26:14 +00:00
struct UploadPlaceConfig{
2024-07-03 19:20:11 +00:00
api_key:ApiKey,
2024-07-02 21:26:14 +00:00
place_id:u64,
universe_id:u64,
input_file:PathBuf,
}
async fn upload_place(config:UploadPlaceConfig)->AResult<()>{
2024-07-03 19:20:11 +00:00
let context=CloudContext::new(config.api_key);
2024-07-03 19:22:39 +00:00
context.update_place(rbx_asset::cloud::UpdatePlaceRequest{
2024-07-02 21:26:14 +00:00
placeId:config.place_id,
universeId:config.universe_id,
},tokio::fs::read(config.input_file).await?).await?;
2023-12-31 01:59:40 +00:00
Ok(())
}
2024-07-16 18:10:18 +00:00
async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<()>{
let context=CookieContext::new(cookie);
futures::stream::iter(asset_id_file_map.into_iter()
.map(|(asset_id,file)|{
2024-04-28 06:36:21 +00:00
let context=&context;
2023-12-31 02:00:51 +00:00
async move{
2024-07-16 18:10:18 +00:00
Ok((file,context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:None}).await?))
2023-12-31 02:00:51 +00:00
}
}))
2023-12-31 02:00:51 +00:00
.buffer_unordered(CONCURRENT_REQUESTS)
2024-01-06 20:38:29 +00:00
.for_each(|b:AResult<_>|async{
match b{
2024-07-19 18:34:51 +00:00
Ok((dest,data))=>if let Err(e)=tokio::fs::write(dest,data).await{
eprintln!("fs error: {}",e);
2024-01-06 20:38:29 +00:00
},
Err(e)=>eprintln!("dl error: {}",e),
}
}).await;
Ok(())
}
2024-04-26 00:38:06 +00:00
2024-07-04 00:24:27 +00:00
async fn get_inventory_pages(context:&CookieContext,group:u64)->AResult<Vec<InventoryItem>>{
2024-04-26 00:38:06 +00:00
let mut cursor:Option<String>=None;
let mut asset_list=Vec::new();
loop{
2024-07-04 00:24:27 +00:00
let mut page=context.get_inventory_page(rbx_asset::cookie::InventoryPageRequest{group,cursor}).await?;
2024-04-26 00:38:06 +00:00
asset_list.append(&mut page.data);
if page.nextPageCursor.is_none(){
break;
}
cursor=page.nextPageCursor;
}
Ok(asset_list)
}
2024-07-04 00:24:27 +00:00
async fn download_group_inventory_json(cookie:Cookie,group:u64,output_folder:PathBuf)->AResult<()>{
let context=CookieContext::new(cookie);
2024-04-28 06:36:21 +00:00
let item_list=get_inventory_pages(&context,group).await?;
2024-04-26 00:38:06 +00:00
let mut path=output_folder.clone();
path.set_file_name("versions.json");
tokio::fs::write(path,serde_json::to_string(&item_list)?).await?;
Ok(())
}
2024-01-06 20:38:29 +00:00
2024-07-04 00:24:27 +00:00
async fn get_version_history(context:&CookieContext,asset_id:AssetID)->AResult<Vec<AssetVersion>>{
2024-01-06 20:38:29 +00:00
let mut cursor:Option<String>=None;
let mut asset_list=Vec::new();
loop{
2024-07-04 00:24:27 +00:00
let mut page=context.get_asset_versions_page(rbx_asset::cookie::AssetVersionsPageRequest{asset_id,cursor}).await?;
2024-01-13 00:19:01 +00:00
asset_list.append(&mut page.data);
if page.nextPageCursor.is_none(){
break;
2024-01-06 20:38:29 +00:00
}
2024-01-13 00:19:01 +00:00
cursor=page.nextPageCursor;
2024-01-06 20:38:29 +00:00
}
asset_list.sort_by(|a,b|a.assetVersionNumber.cmp(&b.assetVersionNumber));
2024-01-11 07:57:50 +00:00
Ok(asset_list)
}
struct DownloadHistoryConfig{
continue_from_versions:bool,
end_version:Option<u64>,
2024-01-13 00:27:31 +00:00
start_version:u64,
2024-03-08 17:48:47 +00:00
output_folder:PathBuf,
2024-07-04 00:24:27 +00:00
cookie:Cookie,
2024-01-11 07:57:50 +00:00
asset_id:AssetID,
}
async fn download_history(mut config:DownloadHistoryConfig)->AResult<()>{
2024-01-14 06:56:12 +00:00
let mut asset_list_contents=std::collections::HashSet::new();
let mut asset_list:Vec<AssetVersion>=Vec::new();
if config.end_version.is_none()&&config.continue_from_versions{
//load prexisting versions list
let mut versions_path=config.output_folder.clone();
versions_path.push("versions.json");
match std::fs::File::open(versions_path){
Ok(versions_file)=>asset_list.append(&mut serde_json::from_reader(versions_file)?),
Err(e)=>match e.kind(){
2024-03-08 17:39:57 +00:00
std::io::ErrorKind::NotFound=>Err(anyhow::Error::msg("Cannot continue from versions.json - file does not exist"))?,
_=>Err(e)?,
}
}
//write down which versions are contained
for asset_version in &asset_list{
2024-01-14 06:56:12 +00:00
asset_list_contents.insert(asset_version.assetVersionNumber);
}
//find the highest number
match asset_list.iter().map(|asset_version|asset_version.assetVersionNumber).max(){
Some(max)=>{
//count down contiguously until a number is missing
for i in (1..=max).rev(){
2024-01-14 06:56:12 +00:00
if !asset_list_contents.contains(&i){
//that is end_version
config.end_version=Some(i);
break;
}
}
//if all versions are contained, set start_version to the max + 1
if config.end_version.is_none(){
config.start_version=max+1;
}
},
None=>Err(anyhow::Error::msg("Cannot continue from versions.json - there are no previous versions"))?,
}
}
2024-07-04 00:24:27 +00:00
let context=CookieContext::new(config.cookie);
2024-01-13 00:19:01 +00:00
//limit concurrent downloads
let mut join_set=tokio::task::JoinSet::new();
2024-01-11 07:57:50 +00:00
//poll paged list of all asset versions
2024-01-13 00:19:01 +00:00
let mut cursor:Option<String>=None;
loop{
2024-07-04 00:24:27 +00:00
let mut page=context.get_asset_versions_page(rbx_asset::cookie::AssetVersionsPageRequest{asset_id:config.asset_id,cursor}).await?;
2024-04-28 06:36:21 +00:00
let context=&context;
let output_folder=config.output_folder.clone();
let data=&page.data;
2024-01-14 06:56:12 +00:00
let asset_list_contents=&asset_list_contents;
let join_set=&mut join_set;
let error_catcher=||async move{
2024-01-13 00:27:31 +00:00
let mut cancel_paging=false;
for asset_version in data{
2024-01-13 00:19:01 +00:00
let version_number=asset_version.assetVersionNumber;
2024-01-14 06:56:12 +00:00
//skip assets beyond specified end_version
if config.end_version.is_some_and(|v|v<version_number){
continue;
}
2024-01-14 06:56:12 +00:00
//skip assets lower than start_version and cancel paging asset versions
2024-01-13 00:27:31 +00:00
if version_number<config.start_version{
cancel_paging=true;
continue;//don't trust roblox returned order
}
2024-01-14 06:56:12 +00:00
//skip previously downloaded assets
if asset_list_contents.contains(&version_number){
continue;
}
while CONCURRENT_REQUESTS<=join_set.len(){
join_set.join_next().await.unwrap()??;
}
2024-04-28 06:36:21 +00:00
let context=context.clone();
2024-01-15 02:43:03 +00:00
let mut path=output_folder.clone();
path.push(format!("{}_v{}.rbxl",config.asset_id,version_number));
join_set.spawn(async move{
2024-07-04 00:24:27 +00:00
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?;
2024-01-13 00:19:01 +00:00
2024-04-28 06:36:21 +00:00
tokio::fs::write(path,file).await?;
2024-01-13 00:19:01 +00:00
Ok::<_,anyhow::Error>(())
});
2024-01-13 00:19:01 +00:00
}
Ok::<_,anyhow::Error>(cancel_paging)
};
let cancel_paging=match error_catcher().await{
Ok(cancel)=>cancel,
Err(e)=>{
println!("download error: {}",e);
//cancel download and write versions
true
},
};
2024-01-13 00:27:31 +00:00
if page.nextPageCursor.is_none()||cancel_paging{
for asset_version in page.data.into_iter(){
2024-01-14 06:56:12 +00:00
if !(asset_list_contents.contains(&asset_version.assetVersionNumber)
||config.end_version.is_some_and(|v|v<asset_version.assetVersionNumber)
||asset_version.assetVersionNumber<config.start_version){
2024-01-13 00:27:31 +00:00
asset_list.push(asset_version);
}
}
2024-01-13 00:19:01 +00:00
break;
2024-01-13 00:27:31 +00:00
}else{
asset_list.append(&mut page.data);
2024-01-13 00:19:01 +00:00
}
cursor=page.nextPageCursor;
}
asset_list.sort_by(|a,b|a.assetVersionNumber.cmp(&b.assetVersionNumber));
let mut path=config.output_folder.clone();
2024-01-06 20:38:29 +00:00
path.set_file_name("versions.json");
tokio::fs::write(path,serde_json::to_string(&asset_list)?).await?;
while let Some(result)=join_set.join_next().await{
result??;
2024-01-13 00:19:01 +00:00
}
2024-01-11 07:57:50 +00:00
2023-12-31 01:59:40 +00:00
Ok(())
2024-01-01 20:21:33 +00:00
}
2024-01-06 01:54:13 +00:00
2024-08-17 01:34:28 +00:00
#[derive(Debug)]
enum LoadDomError{
IO(std::io::Error),
RbxBinary(rbx_binary::DecodeError),
RbxXml(rbx_xml::DecodeError),
UnknownRobloxFile([u8;4]),
UnsupportedFile,
}
impl std::fmt::Display for LoadDomError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadDomError{}
fn load_dom<R:Read>(input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
2024-01-11 10:46:47 +00:00
let mut buf=std::io::BufReader::new(input);
2024-08-17 01:34:28 +00:00
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(LoadDomError::IO)?;
2024-01-11 10:46:47 +00:00
match &peek[0..4]{
b"<rob"=>{
match &peek[4..8]{
2024-08-17 01:34:28 +00:00
b"lox!"=>rbx_binary::from_reader(buf).map_err(LoadDomError::RbxBinary),
b"lox "=>rbx_xml::from_reader_default(buf).map_err(LoadDomError::RbxXml),
other=>Err(LoadDomError::UnknownRobloxFile(other.try_into().unwrap())),
2024-01-11 10:46:47 +00:00
}
},
2024-08-17 01:34:28 +00:00
_=>Err(LoadDomError::UnsupportedFile),
2024-01-06 01:54:13 +00:00
}
}
2024-01-11 07:57:50 +00:00
struct DecompileConfig{
2024-07-02 21:34:54 +00:00
style:rox_compiler::Style,
2024-03-08 17:48:47 +00:00
input_file:PathBuf,
output_folder:PathBuf,
2024-01-11 07:57:50 +00:00
write_template:bool,
write_models:bool,
write_scripts:bool,
}
2024-01-11 21:59:32 +00:00
async fn decompile(config:DecompileConfig)->AResult<()>{
2024-01-11 07:57:50 +00:00
//rules:
//Class Script|LocalScript|ModuleScript->$Name.lua
//Class Model->$Name.rbxmx
//overrides.json per-folder [Override{name,class}]
//Everything else goes into template.rbxlx
//read file
2024-07-01 18:18:34 +00:00
let dom=load_dom(std::io::BufReader::new(std::fs::File::open(config.input_file)?))?;
let context=rox_compiler::DecompiledContext::from_dom(dom);
2024-01-11 07:57:50 +00:00
//generate folders, models, and scripts
//delete models and scripts from dom
2024-07-01 18:18:34 +00:00
context.write_files(rox_compiler::WriteConfig{
2024-07-02 21:34:54 +00:00
style:config.style,
2024-01-11 07:57:50 +00:00
output_folder:config.output_folder,
write_template:config.write_template,
write_models:config.write_models,
write_scripts:config.write_scripts,
2024-07-01 18:18:34 +00:00
}).await?;
2024-01-11 07:57:50 +00:00
Ok(())
}
2024-07-02 03:41:52 +00:00
struct DownloadDecompileConfig{
2024-07-04 00:24:27 +00:00
cookie:Cookie,
2024-07-02 03:41:52 +00:00
asset_id:AssetID,
2024-07-02 21:34:54 +00:00
style:rox_compiler::Style,
2024-07-02 03:41:52 +00:00
output_folder:PathBuf,
write_template:bool,
write_models:bool,
write_scripts:bool,
}
async fn download_decompile(config:DownloadDecompileConfig)->AResult<()>{
2024-07-04 00:24:27 +00:00
let context=CookieContext::new(config.cookie);
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?;
2024-07-02 03:41:52 +00:00
let dom=load_dom(std::io::Cursor::new(file))?;
let context=rox_compiler::DecompiledContext::from_dom(dom);
context.write_files(rox_compiler::WriteConfig{
2024-07-02 21:34:54 +00:00
style:config.style,
2024-07-02 03:41:52 +00:00
output_folder:config.output_folder,
write_template:config.write_template,
write_models:config.write_models,
write_scripts:config.write_scripts,
}).await?;
Ok(())
}
struct WriteCommitConfig{
2024-01-11 21:48:57 +00:00
git_committer_name:String,
git_committer_email:String,
2024-03-08 17:48:47 +00:00
output_folder:PathBuf,
2024-07-02 21:34:54 +00:00
style:rox_compiler::Style,
write_template:bool,
write_models:bool,
write_scripts:bool,
}
2024-07-01 18:18:34 +00:00
async fn write_commit(config:WriteCommitConfig,b:Result<AResult<(AssetVersion,rox_compiler::DecompiledContext)>,tokio::task::JoinError>,repo:&git2::Repository)->AResult<()>{
let (asset_version,context)=b??;
2024-01-12 00:37:48 +00:00
println!("writing files for version {}",asset_version.assetVersionNumber);
//clean output dir
2024-01-12 04:23:44 +00:00
if config.write_models||config.write_scripts{
let mut src=config.output_folder.clone();
src.push("src");
match std::fs::remove_dir_all(src){
Ok(())=>(),
Err(e)=>println!("remove_dir_all src failed {}",e),
}
2024-01-12 04:23:44 +00:00
}
if config.write_template{
let mut template=config.output_folder.clone();
template.push("template.rbxlx");
match std::fs::remove_file(template){
Ok(())=>(),
Err(e)=>println!("remove_file template.rbxlx failed {}",e),
}
}
//write files
2024-07-01 18:18:34 +00:00
context.write_files(rox_compiler::WriteConfig{
2024-07-02 21:34:54 +00:00
style:config.style,
output_folder:config.output_folder.clone(),
write_template:config.write_template,
write_models:config.write_models,
write_scripts:config.write_scripts,
2024-07-01 18:18:34 +00:00
}).await?;
let date=asset_version.created;
2024-01-12 04:29:22 +00:00
//let sig=repo.signature()?; //this pulls default name and email
2024-01-11 21:48:57 +00:00
let sig=git2::Signature::new(config.git_committer_name.as_str(),config.git_committer_email.as_str(),&git2::Time::new(date.timestamp(),0)).unwrap();
let tree_id={
let mut tree_index = repo.index()?;
2024-03-08 17:40:25 +00:00
match tree_index.add_all(std::iter::once(config.output_folder.as_path()),git2::IndexAddOption::DEFAULT,None){
Ok(_)=>(),
Err(e)=>println!("tree_index.add_all error: {}",e),
}
match tree_index.update_all(std::iter::once(config.output_folder.as_path()),None){
2024-01-14 19:12:57 +00:00
Ok(_)=>(),
Err(e)=>println!("tree_index.update_all error: {}",e),
2024-01-12 04:33:12 +00:00
}
tree_index.write()?;
tree_index.write_tree()?
};
let tree=repo.find_tree(tree_id)?;
2024-01-12 04:33:04 +00:00
let mut parents=Vec::new();
match repo.head(){
2024-01-12 04:33:12 +00:00
Ok(reference)=>{
let commit=reference.peel_to_commit()?;
//test tree against commit tree to see if there is any changes
let commit_tree=commit.tree()?;
let diff=repo.diff_tree_to_tree(Some(&commit_tree),Some(&tree),None)?;
2024-03-08 17:40:25 +00:00
if diff.get_delta(0).is_none(){
2024-01-12 04:33:12 +00:00
println!("no changes");
return Ok(());
}
parents.push(commit);
},
2024-01-12 04:33:04 +00:00
Err(e)=>println!("repo head error {:?}",e),
};
2024-01-12 04:33:04 +00:00
repo.commit(
Some("HEAD"),//update_ref
&sig,//author
&sig,//commiter
&format!("v{}", asset_version.assetVersionNumber),//message
&tree,//tree (basically files)
parents.iter().collect::<Vec<&git2::Commit<'_>>>().as_slice(),//parents
)?;
//commit
Ok(())
}
struct DecompileHistoryConfig{
2024-01-12 00:06:24 +00:00
git_committer_name:String,
git_committer_email:String,
2024-03-08 17:48:47 +00:00
input_folder:PathBuf,
2024-07-02 21:34:54 +00:00
style:rox_compiler::Style,
2024-03-08 17:48:47 +00:00
output_folder:PathBuf,
2024-01-12 00:06:24 +00:00
write_template:bool,
write_models:bool,
write_scripts:bool,
}
async fn decompile_history_into_git(config:DecompileHistoryConfig)->AResult<()>{
//use prexisting versions list
2024-01-12 00:06:24 +00:00
let mut versions_path=config.input_folder.clone();
versions_path.push("versions.json");
let asset_list:Vec<AssetVersion>=serde_json::from_reader(std::fs::File::open(versions_path)?)?;
2024-01-22 20:28:32 +00:00
let repo=git2::Repository::init(config.output_folder.as_path())?;
2024-01-12 00:06:24 +00:00
//decompile all versions
futures::stream::iter(asset_list.into_iter()
2024-01-12 00:06:24 +00:00
.map(|asset_version|{
let mut file_path=config.input_folder.clone();
2024-01-12 06:25:00 +00:00
tokio::task::spawn_blocking(move||{
2024-01-12 00:06:24 +00:00
file_path.push(format!("{}_v{}.rbxl",asset_version.assetId,asset_version.assetVersionNumber));
2024-01-12 00:37:26 +00:00
let file=std::fs::File::open(file_path)?;
2024-07-01 18:18:34 +00:00
let dom=load_dom(file)?;
let contents=rox_compiler::DecompiledContext::from_dom(dom);
2024-01-12 00:06:24 +00:00
Ok::<_,anyhow::Error>((asset_version,contents))
})
}))
2024-01-12 01:11:44 +00:00
.buffered(CONCURRENT_DECODE)
2024-01-12 00:06:24 +00:00
.for_each(|join_handle_result|async{
match write_commit(WriteCommitConfig{
git_committer_name:config.git_committer_name.clone(),
git_committer_email:config.git_committer_email.clone(),
2024-01-12 19:24:03 +00:00
style:config.style,
2024-01-12 00:06:24 +00:00
output_folder:config.output_folder.clone(),
write_template:config.write_template,
write_models:config.write_models,
write_scripts:config.write_scripts,
},join_handle_result,&repo).await{
Ok(())=>(),
2024-01-14 21:24:33 +00:00
Err(e)=>println!("decompile/write/commit error: {}",e),
2024-01-12 00:06:24 +00:00
}
}).await;
Ok(())
}
struct DownloadAndDecompileHistoryConfig{
2024-07-04 00:24:27 +00:00
cookie:Cookie,
asset_id:AssetID,
2024-01-11 21:48:57 +00:00
git_committer_name:String,
git_committer_email:String,
2024-07-02 21:34:54 +00:00
style:rox_compiler::Style,
2024-03-08 17:48:47 +00:00
output_folder:PathBuf,
write_template:bool,
write_models:bool,
write_scripts:bool,
}
2024-01-12 00:06:24 +00:00
async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHistoryConfig)->AResult<()>{
2024-07-04 00:24:27 +00:00
let context=CookieContext::new(config.cookie);
//poll paged list of all asset versions
2024-04-28 06:36:21 +00:00
let asset_list=get_version_history(&context,config.asset_id).await?;
let repo=git2::Repository::init(config.output_folder.clone())?;
//download all versions
2024-04-28 06:36:21 +00:00
let asset_id=config.asset_id;
futures::stream::iter(asset_list.into_iter()
.map(|asset_version|{
2024-04-28 06:36:21 +00:00
let context=context.clone();
tokio::task::spawn(async move{
2024-07-04 00:24:27 +00:00
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?;
2024-07-01 18:18:34 +00:00
let dom=load_dom(std::io::Cursor::new(file))?;
Ok::<_,anyhow::Error>((asset_version,rox_compiler::DecompiledContext::from_dom(dom)))
})
}))
2024-01-12 01:11:44 +00:00
.buffered(CONCURRENT_DECODE)
.for_each(|join_handle_result|async{
match write_commit(WriteCommitConfig{
2024-01-12 19:24:03 +00:00
style:config.style,
2024-01-11 21:48:57 +00:00
git_committer_name:config.git_committer_name.clone(),
git_committer_email:config.git_committer_email.clone(),
output_folder:config.output_folder.clone(),
write_template:config.write_template,
write_models:config.write_models,
write_scripts:config.write_scripts,
},join_handle_result,&repo).await{
Ok(())=>(),
Err(e)=>println!("download/unzip/decompile/write/commit error: {}",e),
}
}).await;
Ok(())
}
2024-03-08 17:39:27 +00:00
struct CompileConfig{
2024-03-08 17:48:47 +00:00
input_folder:PathBuf,
output_file:PathBuf,
template:Option<PathBuf>,
2024-07-02 21:34:54 +00:00
style:Option<rox_compiler::Style>,
2024-03-08 17:39:27 +00:00
}
async fn compile(config:CompileConfig)->AResult<()>{
//basically decompile in reverse order
//load template dom
let mut dom=match config.template{
2024-03-08 17:39:27 +00:00
//mr dom doesn't like tokio files
Some(template_path)=>load_dom(std::io::BufReader::new(std::fs::File::open(template_path)?))?,
2024-07-01 21:42:36 +00:00
None=>rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel")),
2024-03-08 17:39:27 +00:00
};
2024-07-01 18:18:34 +00:00
rox_compiler::compile(rox_compiler::CompileConfig{
input_folder:config.input_folder,
2024-07-02 21:34:54 +00:00
style:config.style,
2024-07-01 18:18:34 +00:00
},&mut dom).await?;
2024-03-08 17:39:27 +00:00
let mut output_place=config.output_file.clone();
if output_place.extension().is_none()&&tokio::fs::try_exists(output_place.as_path()).await?{
output_place.push("place.rbxl");
}
let output=std::io::BufWriter::new(std::fs::File::create(output_place)?);
//write inner objects
rbx_binary::to_writer(output,&dom,dom.root().children())?;
2024-01-06 01:54:13 +00:00
Ok(())
}
2024-07-02 01:03:36 +00:00
2024-07-02 21:26:14 +00:00
struct CompileUploadAssetConfig{
2024-07-02 01:03:36 +00:00
input_folder:PathBuf,
template:Option<PathBuf>,
2024-07-02 21:34:54 +00:00
style:Option<rox_compiler::Style>,
cookie:Cookie,
group_id:Option<u64>,
2024-07-02 01:03:36 +00:00
asset_id:AssetID,
}
2024-07-02 21:26:14 +00:00
async fn compile_upload_asset(config:CompileUploadAssetConfig)->AResult<()>{
2024-07-02 01:03:36 +00:00
let mut dom=match config.template{
//mr dom doesn't like tokio files
Some(template_path)=>load_dom(std::io::BufReader::new(std::fs::File::open(template_path)?))?,
None=>rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel")),
};
rox_compiler::compile(rox_compiler::CompileConfig{
input_folder:config.input_folder,
2024-07-02 21:34:54 +00:00
style:config.style,
2024-07-02 01:03:36 +00:00
},&mut dom).await?;
//make a binary file in a buffer in memory
let mut data=Vec::new();
rbx_binary::to_writer(std::io::Cursor::new(&mut data),&dom,dom.root().children())?;
//upload it
let context=CookieContext::new(config.cookie);
let resp=context.upload(rbx_asset::cookie::UploadRequest{
groupId:config.group_id,
assetid:config.asset_id,
name:None,
2024-07-02 01:03:36 +00:00
description:None,
ispublic:None,
allowComments:None,
2024-07-02 21:26:14 +00:00
},data).await?;
println!("UploadResponse={:?}",resp);
Ok(())
}
struct CompileUploadPlaceConfig{
input_folder:PathBuf,
template:Option<PathBuf>,
style:Option<rox_compiler::Style>,
2024-07-03 19:20:11 +00:00
api_key:ApiKey,
2024-07-02 21:26:14 +00:00
place_id:u64,
universe_id:u64,
}
async fn compile_upload_place(config:CompileUploadPlaceConfig)->AResult<()>{
let mut dom=match config.template{
//mr dom doesn't like tokio files
Some(template_path)=>load_dom(std::io::BufReader::new(std::fs::File::open(template_path)?))?,
None=>rbx_dom_weak::WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel")),
};
rox_compiler::compile(rox_compiler::CompileConfig{
input_folder:config.input_folder,
style:config.style,
},&mut dom).await?;
//make a binary file in a buffer in memory
let mut data=Vec::new();
rbx_binary::to_writer(std::io::Cursor::new(&mut data),&dom,dom.root().children())?;
//upload it
2024-07-03 19:20:11 +00:00
let context=CloudContext::new(config.api_key);
let resp=context.update_place(rbx_asset::cloud::UpdatePlaceRequest{
2024-07-02 21:26:14 +00:00
universeId:config.universe_id,
placeId:config.place_id,
2024-07-02 01:03:36 +00:00
},data).await?;
println!("UploadResponse={:?}",resp);
2024-07-02 01:03:36 +00:00
Ok(())
}