From 8f6012c7ef8d9b2a6db9158a37a64b89c9af631c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 11 Aug 2025 16:33:16 -0700 Subject: [PATCH 1/6] validator: remove MessageHandler constructor (too many arguments) --- validation/src/main.rs | 21 +++++++++++++++------ validation/src/message_handler.rs | 21 --------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/validation/src/main.rs b/validation/src/main.rs index 980e2f5..5ef6f33 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -59,12 +59,21 @@ async fn main()->Result<(),StartupError>{ let api_host_internal=std::env::var("API_HOST_INTERNAL").expect("API_HOST_INTERNAL env required"); let endpoint=tonic::transport::Endpoint::new(api_host_internal).map_err(StartupError::API)?; let channel=endpoint.connect_lazy(); - let mapfixes=crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone()); - let operations=crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone()); - let scripts=crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone()); - let script_policy=crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone()); - let submissions=crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel); - let message_handler=message_handler::MessageHandler::new(cloud_context,cookie_context,group_id,mapfixes,operations,scripts,script_policy,submissions); + let mapfixes=crate::grpc::mapfixes::Service::new(crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone())); + let operations=crate::grpc::operations::Service::new(crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone())); + let scripts=crate::grpc::scripts::Service::new(crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone())); + let script_policy=crate::grpc::script_policy::Service::new(crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone())); + let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel)); + let message_handler=message_handler::MessageHandler{ + cloud_context, + cookie_context, + group_id, + mapfixes, + operations, + scripts, + script_policy, + submissions, + }; // nats let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required"); diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 4508369..033e979 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -39,27 +39,6 @@ pub struct MessageHandler{ } impl MessageHandler{ - pub fn new( - cloud_context:rbx_asset::cloud::Context, - cookie_context:rbx_asset::cookie::Context, - group_id:Option, - mapfixes:crate::grpc::mapfixes::ValidatorMapfixesServiceClient, - operations:crate::grpc::operations::ValidatorOperationsServiceClient, - scripts:crate::grpc::scripts::ValidatorScriptsServiceClient, - script_policy:crate::grpc::script_policy::ValidatorScriptPolicyServiceClient, - submissions:crate::grpc::submissions::ValidatorSubmissionsServiceClient, - )->Self{ - Self{ - cloud_context, - cookie_context, - group_id, - mapfixes:crate::grpc::mapfixes::Service::new(mapfixes), - operations:crate::grpc::operations::Service::new(operations), - scripts:crate::grpc::scripts::Service::new(scripts), - script_policy:crate::grpc::script_policy::Service::new(script_policy), - submissions:crate::grpc::submissions::Service::new(submissions), - } - } pub async fn handle_message_result(&self,message_result:MessageResult)->Result<(),HandleMessageError>{ let message=message_result.map_err(HandleMessageError::Messages)?; message.double_ack().await.map_err(HandleMessageError::DoubleAck)?; -- 2.49.1 From 75682a23750a620b882e40d2319f9150df93b8f9 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 11 Aug 2025 16:56:04 -0700 Subject: [PATCH 2/6] validator: connect to maps_extended --- validation/src/grpc/maps_extended.rs | 21 +++++++++++++++++++++ validation/src/grpc/mod.rs | 1 + validation/src/main.rs | 4 +++- validation/src/message_handler.rs | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 validation/src/grpc/maps_extended.rs diff --git a/validation/src/grpc/maps_extended.rs b/validation/src/grpc/maps_extended.rs new file mode 100644 index 0000000..1c339ee --- /dev/null +++ b/validation/src/grpc/maps_extended.rs @@ -0,0 +1,21 @@ +use crate::endpoint; +use rust_grpc::maps_extended::*; +pub type ServiceClient=rust_grpc::maps_extended::maps_service_client::MapsServiceClient; +#[derive(Clone)] +pub struct Client{ + client:ServiceClient, +} +impl Client{ + pub fn new( + client:ServiceClient, + )->Self{ + Self{client} + } + // endpoint!(get,MapId,MapResponse); + // endpoint!(get_list,MapIdList,MapList); + endpoint!(update,MapUpdate,NullResponse); + // endpoint!(create,MapCreate,MapId); + // endpoint!(delete,MapId,NullResponse); + // endpoint!(list,ListRequest,MapList); + // endpoint!(increment_load_count,MapId,NullResponse); +} diff --git a/validation/src/grpc/mod.rs b/validation/src/grpc/mod.rs index 2fff8c9..c8e88a1 100644 --- a/validation/src/grpc/mod.rs +++ b/validation/src/grpc/mod.rs @@ -1,6 +1,7 @@ pub mod error; +pub mod maps_extended; pub mod mapfixes; pub mod operations; pub mod scripts; diff --git a/validation/src/main.rs b/validation/src/main.rs index 5ef6f33..05c1999 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -63,11 +63,13 @@ async fn main()->Result<(),StartupError>{ let operations=crate::grpc::operations::Service::new(crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone())); let scripts=crate::grpc::scripts::Service::new(crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone())); let script_policy=crate::grpc::script_policy::Service::new(crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone())); - let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel)); + let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel.clone())); + let maps_extended=crate::grpc::maps_extended::Client::new(crate::grpc::maps_extended::ServiceClient::new(channel)); let message_handler=message_handler::MessageHandler{ cloud_context, cookie_context, group_id, + maps_extended, mapfixes, operations, scripts, diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 033e979..87a2c0c 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -31,6 +31,7 @@ pub struct MessageHandler{ pub(crate) cloud_context:rbx_asset::cloud::Context, pub(crate) cookie_context:rbx_asset::cookie::Context, pub(crate) group_id:Option, + pub(crate) maps_extended:crate::grpc::maps_extended::Client, pub(crate) mapfixes:crate::grpc::mapfixes::Service, pub(crate) operations:crate::grpc::operations::Service, pub(crate) scripts:crate::grpc::scripts::Service, -- 2.49.1 From d37a8b9030677f642934355c7a0e501fce6c38ec Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 8 Aug 2025 21:19:58 -0700 Subject: [PATCH 3/6] validator: call Luau Execution API to get LoadAssetVersion --- Cargo.lock | 4 +- validation/Cargo.toml | 2 +- validation/src/main.rs | 4 ++ validation/src/message_handler.rs | 2 + validation/src/rbx_util.rs | 19 ++++++++- validation/src/upload_mapfix.rs | 69 +++++++++++++++++++++++++++++-- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b728c94..6b2c52a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1520,9 +1520,9 @@ dependencies = [ [[package]] name = "rbx_asset" -version = "0.4.9" +version = "0.4.10" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" -checksum = "32c7c8efca16fc09ac623ea41cde2bf48e2da761ac8edd575b0b7022f5dc5bd5" +checksum = "a711a8c43b4bbcd3c72832e51a680e407b3a062e1ddc66cb90e57b86c0e65f80" dependencies = [ "bytes", "chrono", diff --git a/validation/Cargo.toml b/validation/Cargo.toml index 1421471..fb58734 100644 --- a/validation/Cargo.toml +++ b/validation/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] async-nats = "0.42.0" futures = "0.3.31" -rbx_asset = { version = "0.4.9", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" } +rbx_asset = { version = "0.4.10", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" } rbx_binary = "1.0.0" rbx_dom_weak = "3.0.0" rbx_reflection_database = "1.0.3" diff --git a/validation/src/main.rs b/validation/src/main.rs index 05c1999..e472e78 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -47,6 +47,8 @@ async fn main()->Result<(),StartupError>{ }, Err(e)=>panic!("{e}: ROBLOX_GROUP_ID env required"), }; + let load_asset_version_place_id=std::env::var("LOAD_ASSET_VERSION_PLACE_ID").expect("LOAD_ASSET_VERSION_PLACE_ID env required").parse().expect("LOAD_ASSET_VERSION_PLACE_ID int parse failed"); + let load_asset_version_universe_id=std::env::var("LOAD_ASSET_VERSION_UNIVERSE_ID").expect("LOAD_ASSET_VERSION_UNIVERSE_ID env required").parse().expect("LOAD_ASSET_VERSION_UNIVERSE_ID int parse failed"); // create / upload models through STRAFESNET_CI2 account let cookie=std::env::var("RBXCOOKIE").expect("RBXCOOKIE env required"); @@ -69,6 +71,8 @@ async fn main()->Result<(),StartupError>{ cloud_context, cookie_context, group_id, + load_asset_version_place_id, + load_asset_version_universe_id, maps_extended, mapfixes, operations, diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 87a2c0c..1404476 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -31,6 +31,8 @@ pub struct MessageHandler{ pub(crate) cloud_context:rbx_asset::cloud::Context, pub(crate) cookie_context:rbx_asset::cookie::Context, pub(crate) group_id:Option, + pub(crate) load_asset_version_place_id:u64, + pub(crate) load_asset_version_universe_id:u64, pub(crate) maps_extended:crate::grpc::maps_extended::Client, pub(crate) mapfixes:crate::grpc::mapfixes::Service, pub(crate) operations:crate::grpc::operations::Service, diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs index 14297ed..a8185f4 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -1,4 +1,3 @@ - #[allow(dead_code)] #[derive(Debug)] pub enum ReadDomError{ @@ -112,3 +111,21 @@ pub fn get_mapinfo<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&rbx_dom_wea game_id:model_instance.name.parse(), } } + +pub async fn get_luau_result_exp_backoff( + context:&rbx_asset::cloud::Context, + 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, + } + tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await; + backoff*=BACKOFF_MUL; + } +} diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index 8e567cd..d8c0499 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -9,6 +9,17 @@ pub enum Error{ Json(serde_json::Error), Upload(rbx_asset::cookie::UploadError), ApiActionMapfixUploaded(tonic::Status), + CreateSession(rbx_asset::cloud::CreateError), + NonPositiveNumber(serde_json::Number), + Script(rbx_asset::cloud::LuauError), + InvalidResult(Vec), + LuauSession(rbx_asset::cloud::LuauSessionError), + GetAssetInfo(rbx_asset::cloud::GetError), + RevisionMismatch{ + after:u64, + before:u64, + }, + } impl std::fmt::Display for Error{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ @@ -29,7 +40,7 @@ impl crate::message_handler::MessageHandler{ let model_data=maybe_gzip.to_vec().map_err(Error::IO)?; // upload the map to the strafesnet group - let _upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ + let upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ assetid:upload_info.TargetAssetID, groupId:self.group_id, name:None, @@ -38,13 +49,65 @@ impl crate::message_handler::MessageHandler{ allowComments:None, },model_data).await.map_err(Error::Upload)?; - // that's it, the database entry does not need to be changed. - // mark mapfix as uploaded, TargetAssetID is unchanged self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{ id:upload_info.MapfixID, }).await.map_err(Error::ApiActionMapfixUploaded)?; + // update asset version and modes using Roblox Luau API + let script=format!("return game:GetService(\"InsertService\"):GetLatestAssetVersionAsync({})",upload_info.TargetAssetID); + let request=rbx_asset::cloud::LuauSessionLatestRequest{ + place_id:self.load_asset_version_place_id, + universe_id:self.load_asset_version_universe_id, + }; + let session=rbx_asset::cloud::LuauSessionCreate{ + script:&script, + user:None, + timeout:None, + binaryInput:None, + enableBinaryOutput:None, + binaryOutputUri:None, + }; + let session_response=self.cloud_context.create_luau_session(&request,session).await.map_err(Error::CreateSession)?; + + let result=crate::rbx_util::get_luau_result_exp_backoff(&self.cloud_context,&session_response).await; + + let load_asset_version=match result{ + Ok(Ok(rbx_asset::cloud::LuauResults{results}))=>match results.as_slice(){ + [serde_json::Value::Number(load_asset_version)]=>load_asset_version.as_u64().ok_or_else(||Error::NonPositiveNumber(load_asset_version.clone())), + _=>Err(Error::InvalidResult(results)) + }, + Ok(Err(e))=>Err(Error::Script(e)), + Err(e)=>Err(Error::LuauSession(e)), + }?; + + // check asset version to make sure it hasn't been updated + let asset_response=self.cloud_context.get_asset_info(rbx_asset::cloud::GetAssetLatestRequest{ + asset_id:upload_info.TargetAssetID, + }).await.map_err(Error::GetAssetInfo)?; + + if upload_response.AssetVersion!=asset_response.revisionId{ + // the model was updated while we were fetching LoadAssetVersion. + // the number we got may be invalid. + return Err(Error::RevisionMismatch{ + before:upload_response.AssetVersion, + after:asset_response.revisionId, + }); + } + + // write AssetVersion directly to map + self.maps_extended.update(rust_grpc::maps_extended::MapUpdate{ + id:upload_info.TargetAssetID as i64, + asset_version:Some(load_asset_version), + modes:None, + display_name:None, + creator:None, + game_id:None, + date:None, + submitter:None, + thumbnail:None, + }).await.map_err(Error::ApiActionMapfixUploaded)?; + Ok(()) } } -- 2.49.1 From 9c1e1e43476eb0b520aeb481e1395c9db0356730 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 11 Aug 2025 17:40:02 -0700 Subject: [PATCH 4/6] validator: move count_sequential fn --- validation/src/check.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 24db010..ddf9e3a 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -214,6 +214,15 @@ impl std::fmt::Display for WormholeElement{ } } +fn count_sequential(modes:&HashMap>)->usize{ + for mode_id in 0..modes.len(){ + if !modes.contains_key(&ModeID(mode_id as u64)){ + return mode_id; + } + } + return modes.len(); +} + /// Count various map elements #[derive(Default)] struct Counts<'a>{ @@ -525,14 +534,6 @@ impl<'a> ModelInfo<'a>{ .check(&self.counts.mode_start_counts); // There must not be non-sequential modes. If Bonus100 exists, Bonuses 1-99 had better also exist. - fn count_sequential(modes:&HashMap>)->usize{ - for mode_id in 0..modes.len(){ - if !modes.contains_key(&ModeID(mode_id as u64)){ - return mode_id; - } - } - return modes.len(); - } let modes_sequential={ let sequential=count_sequential(&self.counts.mode_start_counts); if sequential==self.counts.mode_start_counts.len(){ -- 2.49.1 From c98c3fe47e520c97c3ed76348596d1eaf8e51444 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 11 Aug 2025 17:40:14 -0700 Subject: [PATCH 5/6] validator: update modes on mapfix --- validation/src/check.rs | 18 ++++++++++++++++++ validation/src/upload_mapfix.rs | 29 +++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index ddf9e3a..0d2939f 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -242,6 +242,24 @@ pub struct ModelInfo<'a>{ counts:Counts<'a>, unanchored_parts:Vec<&'a Instance>, } +impl ModelInfo<'_>{ + pub fn count_modes(&self)->Option{ + let start_zones_count=self.counts.mode_start_counts.len(); + let finish_zones_count=self.counts.mode_finish_counts.len(); + let sequential_start_zones=count_sequential(&self.counts.mode_start_counts); + let sequential_finish_zones=count_sequential(&self.counts.mode_finish_counts); + // all counts must match + if start_zones_count==finish_zones_count + && sequential_start_zones==sequential_finish_zones + && start_zones_count==sequential_start_zones + && finish_zones_count==sequential_finish_zones + { + Some(start_zones_count) + }else{ + None + } + } +} pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{ // extract model info diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index d8c0499..f9b32ca 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -9,6 +9,10 @@ pub enum Error{ Json(serde_json::Error), Upload(rbx_asset::cookie::UploadError), ApiActionMapfixUploaded(tonic::Status), + ModelFileDecode(crate::rbx_util::ReadDomError), + GetRootInstance(crate::rbx_util::GetRootInstanceError), + NonSequentialModes, + TooManyModes(usize), CreateSession(rbx_asset::cloud::CreateError), NonPositiveNumber(serde_json::Number), Script(rbx_asset::cloud::LuauError), @@ -47,13 +51,34 @@ impl crate::message_handler::MessageHandler{ description:None, ispublic:None, allowComments:None, - },model_data).await.map_err(Error::Upload)?; + },model_data.clone()).await.map_err(Error::Upload)?; // mark mapfix as uploaded, TargetAssetID is unchanged self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{ id:upload_info.MapfixID, }).await.map_err(Error::ApiActionMapfixUploaded)?; + // count modes + + // decode dom (slow!) + let dom=crate::rbx_util::read_dom(model_data.as_slice()).map_err(Error::ModelFileDecode)?; + + // extract the root instance + let model_instance=crate::rbx_util::get_root_instance(&dom).map_err(Error::GetRootInstance)?; + + // extract information from the model + let model_info=crate::check::get_model_info(&dom,model_instance); + + // count modes + let modes=model_info.count_modes().ok_or(Error::NonSequentialModes)?; + + // hard limit LOL + let modes=if modes Date: Mon, 11 Aug 2025 17:50:39 -0700 Subject: [PATCH 6/6] readme: document env vars --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7930a81..0f71937 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,11 @@ Prerequisite: rust installed Environment Variables: - ROBLOX_GROUP_ID - RBXCOOKIE +- RBX_API_KEY - API_HOST_INTERNAL - NATS_HOST +- LOAD_ASSET_VERSION_PLACE_ID +- LOAD_ASSET_VERSION_UNIVERSE_ID #### License -- 2.49.1