From 173d5d91037ee7ef7e9ce8e88a28bb55df6d0a2b Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 17:42:49 -0700 Subject: [PATCH 1/9] rbx_asset: Luau Execution API --- rbx_asset/src/cloud.rs | 108 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index acaa7d6..7b9909d 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -326,6 +326,94 @@ impl RobloxOperation{ } } +#[derive(Debug)] +pub enum LuauSessionError{ + Get(GetError), + NoLuauSessionId, + NotDone, +} +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, + pub user:Option<&'a str>, + pub timeout:Option<&'a str>, + pub binaryInput:Option<&'a str>, + pub enableBinaryOutput:Option, + pub binaryOutputUri:Option<&'a str>, +} +#[derive(Debug,serde::Deserialize)] +#[expect(nonstandard_style)] +pub enum LuauSessionState{ + STATE_UNSPECIFIED, + PROCESSING, + COMPLETE, + ERROR, +} +#[derive(Debug,serde::Deserialize)] +#[expect(nonstandard_style)] +pub struct LuauSessionResponse{ + path:String, + pub createTime:chrono::DateTime, + pub updateTime:chrono::DateTime, + pub user:String, + pub state:LuauSessionState, + pub script:String, + pub timeout:String, + pub error:String, + pub output:String, + pub binaryInput:String, + pub enableBinaryOutput:bool, + pub binaryOutputUri:String, +} +impl LuauSessionResponse{ + pub fn path(&self)->&str{ + &self.path + } + pub fn session_id(&self)->Option<&str>{ + // universes/123/places/123/luau-execution-session-tasks/123e4567-e89b-12d3-a456-426655440000 + self.path.rfind('/').map(|index| + self.path.split_at(index+1).1 + ) + } + 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::NotDone), + LuauSessionState::PROCESSING=>Err(LuauSessionError::NotDone), + LuauSessionState::COMPLETE=>Ok(Ok(response.output)), + LuauSessionState::ERROR=>Ok(Err(response.error)), + } + } +} +pub trait IntoSessionPath{ + fn into_session_path(&self)->impl AsRef; +} +impl IntoSessionPath 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 IntoSessionPath 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{ @@ -423,6 +511,26 @@ impl Context{ ).await.map_err(GetError::Response)? .json::().await.map_err(GetError::Reqwest) } + pub async fn create_luau_session(&self,config:&impl IntoSessionPath,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,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 IntoSessionPath)->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)?; -- 2.49.1 From e3683822e2cde170cd795c38a7a226bfbbada5ab Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:09:52 -0700 Subject: [PATCH 2/9] asset-tool: run luau --- src/main.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/main.rs b/src/main.rs index 0992465..c9123f5 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(()) +} -- 2.49.1 From 9d202ba773a5b306620b0f5e5ed4c37ec6bd725f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:14:37 -0700 Subject: [PATCH 3/9] rbx_asset: clean up LuauSessionError --- rbx_asset/src/cloud.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 7b9909d..77dc408 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -329,7 +329,6 @@ impl RobloxOperation{ #[derive(Debug)] pub enum LuauSessionError{ Get(GetError), - NoLuauSessionId, NotDone, } impl std::fmt::Display for LuauSessionError{ -- 2.49.1 From 9774b95f8f6a85b6f6c79fb4d629efdfd4dfed98 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:14:53 -0700 Subject: [PATCH 4/9] rbx_asset: set Content-Type header --- rbx_asset/src/cloud.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 77dc408..26c3564 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}; @@ -442,9 +443,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 } @@ -517,7 +519,7 @@ impl Context{ let body=serde_json::to_string(&session).map_err(CreateError::Serialize)?; response_ok( - self.post(url,body).await.map_err(CreateError::Reqwest)? + self.post(url,Json(body)).await.map_err(CreateError::Reqwest)? ).await.map_err(CreateError::Response)? .json::().await.map_err(CreateError::Reqwest) } @@ -610,7 +612,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) } -- 2.49.1 From d6e930cf004f60c76f535bb915cf7b2e537caf44 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:40:45 -0700 Subject: [PATCH 5/9] rbx_asset: rename trait --- rbx_asset/src/cloud.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 26c3564..a73e1be 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -392,10 +392,10 @@ impl LuauSessionResponse{ } } } -pub trait IntoSessionPath{ +pub trait AsSessionPath{ fn into_session_path(&self)->impl AsRef; } -impl IntoSessionPath for LuauSessionResponse{ +impl AsSessionPath for LuauSessionResponse{ fn into_session_path(&self)->impl AsRef{ &self.path } @@ -405,7 +405,7 @@ pub struct LuauSessionRequest{ pub place_id:u64, pub version_id:u64, } -impl IntoSessionPath for LuauSessionRequest{ +impl AsSessionPath for LuauSessionRequest{ fn into_session_path(&self)->impl AsRef{ let universe_id=self.universe_id; let place_id=self.place_id; @@ -512,7 +512,7 @@ impl Context{ ).await.map_err(GetError::Response)? .json::().await.map_err(GetError::Reqwest) } - pub async fn create_luau_session(&self,config:&impl IntoSessionPath,session:LuauSessionCreate<'_>)->Result{ + 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)?; @@ -523,7 +523,7 @@ impl Context{ ).await.map_err(CreateError::Response)? .json::().await.map_err(CreateError::Reqwest) } - pub async fn get_luau_session(&self,config:&impl IntoSessionPath)->Result{ + 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)?; -- 2.49.1 From a23bd07e642495ab20b5af226951677d3a6c1770 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:41:23 -0700 Subject: [PATCH 6/9] rbx_asset: skip serializing if none --- rbx_asset/src/cloud.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index a73e1be..07e82a8 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -342,10 +342,15 @@ impl std::error::Error for LuauSessionError{} #[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)] -- 2.49.1 From 81517abde1e3517d08abae85fd173a13852e4fa4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:55:26 -0700 Subject: [PATCH 7/9] rbx_asset: fix up Luau Execution --- rbx_asset/src/cloud.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 07e82a8..9112401 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -330,7 +330,10 @@ 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 { @@ -359,20 +362,18 @@ pub enum LuauSessionState{ STATE_UNSPECIFIED, PROCESSING, COMPLETE, - ERROR, + FAILED, } #[derive(Debug,serde::Deserialize)] #[expect(nonstandard_style)] pub struct LuauSessionResponse{ path:String, - pub createTime:chrono::DateTime, - pub updateTime:chrono::DateTime, - pub user:String, + #[serde(deserialize_with="deserialize_u64")] + pub user:u64, pub state:LuauSessionState, pub script:String, - pub timeout:String, - pub error:String, - pub output:String, + pub error:Option, + pub output:Option, pub binaryInput:String, pub enableBinaryOutput:bool, pub binaryOutputUri:String, @@ -381,19 +382,13 @@ impl LuauSessionResponse{ pub fn path(&self)->&str{ &self.path } - pub fn session_id(&self)->Option<&str>{ - // universes/123/places/123/luau-execution-session-tasks/123e4567-e89b-12d3-a456-426655440000 - self.path.rfind('/').map(|index| - self.path.split_at(index+1).1 - ) - } 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::NotDone), + LuauSessionState::STATE_UNSPECIFIED=>Err(LuauSessionError::Unspecified), LuauSessionState::PROCESSING=>Err(LuauSessionError::NotDone), - LuauSessionState::COMPLETE=>Ok(Ok(response.output)), - LuauSessionState::ERROR=>Ok(Err(response.error)), + LuauSessionState::COMPLETE=>Ok(Ok(response.output.ok_or(LuauSessionError::NoOutput)?)), + LuauSessionState::FAILED=>Ok(Err(response.error.ok_or(LuauSessionError::NoError)?)), } } } -- 2.49.1 From b06c837491fb9f943423ba0d46248c58427aa6f6 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 18:58:53 -0700 Subject: [PATCH 8/9] add output & error structs --- rbx_asset/src/cloud.rs | 15 ++++++++++++--- src/main.rs | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 9112401..0496439 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -365,6 +365,15 @@ pub enum LuauSessionState{ 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, @@ -372,8 +381,8 @@ pub struct LuauSessionResponse{ pub user:u64, pub state:LuauSessionState, pub script:String, - pub error:Option, - pub output:Option, + pub error:Option, + pub output:Option, pub binaryInput:String, pub enableBinaryOutput:bool, pub binaryOutputUri:String, @@ -382,7 +391,7 @@ impl LuauSessionResponse{ pub fn path(&self)->&str{ &self.path } - pub async fn try_get_result(&self,context:&Context)->Result,LuauSessionError>{ + 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), diff --git a/src/main.rs b/src/main.rs index c9123f5..6412fc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1785,7 +1785,7 @@ async fn compile_upload_place(config:CompileUploadPlaceConfig)->AResult<()>{ async fn get_luau_result_exp_backoff( context:&CloudContext, luau_session:&rbx_asset::cloud::LuauSessionResponse -)->Result,rbx_asset::cloud::LuauSessionError>{ +)->Result,rbx_asset::cloud::LuauSessionError>{ const BACKOFF_MUL:f32=1.395_612_5;//exp(1/3) let mut backoff=1000f32; loop{ -- 2.49.1 From d4f22f04d197e52ce6a6d0c52012e6a00dd4664c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 19:48:20 -0700 Subject: [PATCH 9/9] fix LuauResults --- rbx_asset/src/cloud.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index 0496439..3f79365 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -371,7 +371,7 @@ pub struct LuauError{ } #[derive(Debug,serde::Deserialize)] pub struct LuauResults{ - pub results:Vec, + pub results:Vec, } #[derive(Debug,serde::Deserialize)] #[expect(nonstandard_style)] -- 2.49.1