diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index acaa7d6..3f79365 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -1,3 +1,4 @@ +use crate::body::{Binary,ContentType,Json}; use crate::util::{serialize_u64,deserialize_u64,response_ok}; use crate::types::{ResponseError,MaybeGzippedBytes}; @@ -326,6 +327,102 @@ impl RobloxOperation{ } } +#[derive(Debug)] +pub enum LuauSessionError{ + Get(GetError), + Unspecified, + NotDone, + NoOutput, + NoError, +} +impl std::fmt::Display for LuauSessionError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"{self:?}") + } +} +impl std::error::Error for LuauSessionError{} +#[derive(Debug,serde::Serialize)] +#[expect(nonstandard_style)] +pub struct LuauSessionCreate<'a>{ + pub script:&'a str, + #[serde(skip_serializing_if="Option::is_none")] + pub user:Option<&'a str>, + #[serde(skip_serializing_if="Option::is_none")] + pub timeout:Option<&'a str>, + #[serde(skip_serializing_if="Option::is_none")] + pub binaryInput:Option<&'a str>, + #[serde(skip_serializing_if="Option::is_none")] + pub enableBinaryOutput:Option, + #[serde(skip_serializing_if="Option::is_none")] + pub binaryOutputUri:Option<&'a str>, +} +#[derive(Debug,serde::Deserialize)] +#[expect(nonstandard_style)] +pub enum LuauSessionState{ + STATE_UNSPECIFIED, + PROCESSING, + COMPLETE, + FAILED, +} +#[derive(Debug,serde::Deserialize)] +pub struct LuauError{ + pub code:String, + pub message:String, +} +#[derive(Debug,serde::Deserialize)] +pub struct LuauResults{ + pub results:Vec, +} +#[derive(Debug,serde::Deserialize)] +#[expect(nonstandard_style)] +pub struct LuauSessionResponse{ + path:String, + #[serde(deserialize_with="deserialize_u64")] + pub user:u64, + pub state:LuauSessionState, + pub script:String, + pub error:Option, + pub output:Option, + pub binaryInput:String, + pub enableBinaryOutput:bool, + pub binaryOutputUri:String, +} +impl LuauSessionResponse{ + pub fn path(&self)->&str{ + &self.path + } + pub async fn try_get_result(&self,context:&Context)->Result,LuauSessionError>{ + let response=context.get_luau_session(self).await.map_err(LuauSessionError::Get)?; + match response.state{ + LuauSessionState::STATE_UNSPECIFIED=>Err(LuauSessionError::Unspecified), + LuauSessionState::PROCESSING=>Err(LuauSessionError::NotDone), + LuauSessionState::COMPLETE=>Ok(Ok(response.output.ok_or(LuauSessionError::NoOutput)?)), + LuauSessionState::FAILED=>Ok(Err(response.error.ok_or(LuauSessionError::NoError)?)), + } + } +} +pub trait AsSessionPath{ + fn into_session_path(&self)->impl AsRef; +} +impl AsSessionPath for LuauSessionResponse{ + fn into_session_path(&self)->impl AsRef{ + &self.path + } +} +pub struct LuauSessionRequest{ + pub universe_id:u64, + pub place_id:u64, + pub version_id:u64, +} +impl AsSessionPath for LuauSessionRequest{ + fn into_session_path(&self)->impl AsRef{ + let universe_id=self.universe_id; + let place_id=self.place_id; + let version_id=self.version_id; + format!("universes/{universe_id}/places/{place_id}/versions/{version_id}/luau-execution-session-tasks") + } +} + #[derive(Clone)] pub struct ApiKey(String); impl ApiKey{ @@ -355,9 +452,10 @@ impl Context{ .header("x-api-key",self.api_key.as_str()) .send().await } - async fn post(&self,url:url::Url,body:impl Into+Clone)->Result{ + async fn post(&self,url:url::Url,body:impl ContentType)->Result{ self.client.post(url) .header("x-api-key",self.api_key.as_str()) + .header("Content-Type",body.content_type()) .body(body) .send().await } @@ -423,6 +521,26 @@ impl Context{ ).await.map_err(GetError::Response)? .json::().await.map_err(GetError::Reqwest) } + pub async fn create_luau_session(&self,config:&impl AsSessionPath,session:LuauSessionCreate<'_>)->Result{ + let raw_url=format!("https://apis.roblox.com/cloud/v2/{}",config.into_session_path().as_ref()); + let url=reqwest::Url::parse(raw_url.as_str()).map_err(CreateError::Parse)?; + + let body=serde_json::to_string(&session).map_err(CreateError::Serialize)?; + + response_ok( + self.post(url,Json(body)).await.map_err(CreateError::Reqwest)? + ).await.map_err(CreateError::Response)? + .json::().await.map_err(CreateError::Reqwest) + } + pub async fn get_luau_session(&self,config:&impl AsSessionPath)->Result{ + let raw_url=format!("https://apis.roblox.com/cloud/v2/{}",config.into_session_path().as_ref()); + let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::Parse)?; + + response_ok( + self.get(url).await.map_err(GetError::Reqwest)? + ).await.map_err(GetError::Response)? + .json::().await.map_err(GetError::Reqwest) + } pub async fn get_asset_info(&self,config:GetAssetLatestRequest)->Result{ let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.asset_id); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::Parse)?; @@ -503,7 +621,7 @@ impl Context{ } response_ok( - self.post(url,body).await.map_err(UpdateError::Reqwest)? + self.post(url,Binary(body)).await.map_err(UpdateError::Reqwest)? ).await.map_err(UpdateError::Response)? .json::().await.map_err(UpdateError::Reqwest) } diff --git a/src/main.rs b/src/main.rs index 0992465..6412fc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ enum Commands{ Decompile(DecompileSubcommand), DecompileHistoryIntoGit(DecompileHistoryIntoGitSubcommand), DownloadAndDecompileHistoryIntoGit(DownloadAndDecompileHistoryIntoGitSubcommand), + RunLuau(RunLuauSubcommand), } /// Download a range of assets from the asset version history. Download summary is saved to `output_folder/versions.json`, and can be optionally used to download only new versions the next time. @@ -427,6 +428,26 @@ struct DownloadAndDecompileHistoryIntoGitSubcommand{ #[arg(long)] write_scripts:Option, } +/// Run a Luau script. +#[derive(Args)] +struct RunLuauSubcommand{ + #[arg(long,group="api_key",required=true)] + api_key_literal:Option, + #[arg(long,group="api_key",required=true)] + api_key_envvar:Option, + #[arg(long,group="api_key",required=true)] + api_key_file:Option, + #[arg(long,group="script",required=true)] + script_literal:Option, + #[arg(long,group="script",required=true)] + script_file:Option, + #[arg(long)] + universe_id:u64, + #[arg(long)] + place_id:u64, + #[arg(long)] + version_id:u64, +} #[derive(Clone,Copy,Debug,clap::ValueEnum)] enum Style{ @@ -738,6 +759,22 @@ async fn main()->AResult<()>{ write_models:subcommand.write_models.unwrap_or(false), write_scripts:subcommand.write_scripts.unwrap_or(true), }).await, + Commands::RunLuau(subcommand)=>run_luau(RunLuauConfig{ + api_key:api_key_from_args( + subcommand.api_key_literal, + subcommand.api_key_envvar, + subcommand.api_key_file, + ).await?, + script:match subcommand.script_literal{ + Some(script)=>script, + None=>std::fs::read_to_string(subcommand.script_file.unwrap())?, + }, + request:rbx_asset::cloud::LuauSessionRequest{ + place_id:subcommand.place_id, + universe_id:subcommand.universe_id, + version_id:subcommand.version_id, + }, + }).await, } } @@ -1744,3 +1781,43 @@ async fn compile_upload_place(config:CompileUploadPlaceConfig)->AResult<()>{ println!("UploadResponse={:?}",resp); Ok(()) } + +async fn get_luau_result_exp_backoff( + context:&CloudContext, + luau_session:&rbx_asset::cloud::LuauSessionResponse +)->Result,rbx_asset::cloud::LuauSessionError>{ + const BACKOFF_MUL:f32=1.395_612_5;//exp(1/3) + let mut backoff=1000f32; + loop{ + match luau_session.try_get_result(context).await{ + //try again when the operation is not done + Err(rbx_asset::cloud::LuauSessionError::NotDone)=>(), + //return all other results + other_result=>return other_result, + } + println!("Operation not complete; waiting {:.0}ms...",backoff); + tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await; + backoff*=BACKOFF_MUL; + } +} +struct RunLuauConfig{ + api_key:ApiKey, + script:String, + request:rbx_asset::cloud::LuauSessionRequest, +} +async fn run_luau(config:RunLuauConfig)->AResult<()>{ + let context=CloudContext::new(config.api_key); + let session=rbx_asset::cloud::LuauSessionCreate{ + script:&config.script, + user:None, + timeout:None, + binaryInput:None, + enableBinaryOutput:None, + binaryOutputUri:None, + }; + let response=context.create_luau_session(&config.request,session).await?; + dbg!(&response); + let result=get_luau_result_exp_backoff(&context,&response).await?; + dbg!(&result); + Ok(()) +}