From 8b11462cc5e1e202cc7e9118e1b96b90b5ee7902 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Wed, 2 Apr 2025 14:45:11 -0700 Subject: [PATCH] validator: implement create operations --- validation/src/create.rs | 77 ++++++++++++++++++ validation/src/create_mapfix.rs | 43 ++++++++++ validation/src/create_submission.rs | 42 ++++++++++ validation/src/main.rs | 4 + validation/src/message_handler.rs | 4 + validation/src/nats_types.rs | 16 ++++ validation/src/rbx_util.rs | 119 ++++++++++++++++++++++++++++ validation/src/validator.rs | 46 +---------- 8 files changed, 306 insertions(+), 45 deletions(-) create mode 100644 validation/src/create.rs create mode 100644 validation/src/create_mapfix.rs create mode 100644 validation/src/create_submission.rs create mode 100644 validation/src/rbx_util.rs diff --git a/validation/src/create.rs b/validation/src/create.rs new file mode 100644 index 0000000..1513904 --- /dev/null +++ b/validation/src/create.rs @@ -0,0 +1,77 @@ +use crate::rbx_util::{get_mapinfo,read_dom,MapInfo,ReadDomError,GetMapInfoError,ParseGameIDError}; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + ModelVersionsPage(rbx_asset::cookie::PageError), + EmptyVersionsPage, + WrongCreatorType, + ModelFileDownload(rbx_asset::cookie::GetError), + ModelFileDecode(ReadDomError), + GetMapInfo(GetMapInfoError), + ParseGameID(ParseGameIDError), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +#[allow(nonstandard_style)] +pub struct CreateRequest{ + pub ModelID:u64, +} +#[allow(nonstandard_style)] +pub struct CreateResult{ + pub AssetOwner:i64, + pub DisplayName:String, + pub Creator:String, + pub GameID:i32, + pub AssetVersion:u64, +} +impl crate::message_handler::MessageHandler{ + pub async fn create_inner(&self,create_info:CreateRequest)->Result<CreateResult,Error>{ + // discover the latest asset version + let asset_versions_page=self.cookie_context.get_asset_versions_page(rbx_asset::cookie::AssetVersionsPageRequest{ + asset_id:create_info.ModelID, + cursor:None + }).await.map_err(Error::ModelVersionsPage)?; + + // grab version info + let first_version=asset_versions_page.data.first().ok_or(Error::EmptyVersionsPage)?; + + if first_version.creatorType!="User"{ + return Err(Error::WrongCreatorType); + } + + let asset_creator_id=first_version.creatorTargetId; + let asset_version=first_version.assetVersionNumber; + + // download the map model version + let model_data=self.cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{ + asset_id:create_info.ModelID, + version:Some(asset_version), + }).await.map_err(Error::ModelFileDownload)?; + + // decode dom (slow!) + let dom=read_dom(&mut std::io::Cursor::new(model_data)).map_err(Error::ModelFileDecode)?; + + // parse create fields out of asset + let MapInfo{ + display_name, + creator, + game_id, + }=get_mapinfo(&dom).map_err(Error::GetMapInfo)?; + + let game_id=game_id.map_err(Error::ParseGameID)?; + + Ok(CreateResult{ + AssetOwner:asset_creator_id as i64, + DisplayName:display_name.unwrap_or_default().to_owned(), + Creator:creator.unwrap_or_default().to_owned(), + GameID:game_id as i32, + AssetVersion:asset_version, + }) + } +} diff --git a/validation/src/create_mapfix.rs b/validation/src/create_mapfix.rs new file mode 100644 index 0000000..84c33b1 --- /dev/null +++ b/validation/src/create_mapfix.rs @@ -0,0 +1,43 @@ +use crate::nats_types::CreateMapfixRequest; +use crate::create::CreateRequest; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + ApiActionMapfixCreate(submissions_api::Error), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn create_mapfix(&self,create_info:CreateMapfixRequest)->Result<(),Error>{ + let create_result=self.create_inner(CreateRequest{ + ModelID:create_info.ModelID, + }).await; + + match create_result{ + Ok(create_request)=>{ + // call create on api + self.api.create_mapfix(submissions_api::types::CreateMapfixRequest{ + OperationID:create_info.OperationID, + AssetOwner:create_request.AssetOwner, + DisplayName:create_request.DisplayName.as_str(), + Creator:create_request.Creator.as_str(), + GameID:create_request.GameID, + AssetID:create_info.ModelID, + AssetVersion:create_request.AssetVersion, + TargetAssetID:create_info.TargetAssetID, + }).await.map_err(Error::ApiActionMapfixCreate)?; + }, + Err(e)=>{ + println!("oh no! {e}"); + }, + } + + Ok(()) + } +} diff --git a/validation/src/create_submission.rs b/validation/src/create_submission.rs new file mode 100644 index 0000000..a5f57cf --- /dev/null +++ b/validation/src/create_submission.rs @@ -0,0 +1,42 @@ +use crate::nats_types::CreateSubmissionRequest; +use crate::create::CreateRequest; + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + ApiActionSubmissionCreate(submissions_api::Error), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn create_submission(&self,create_info:CreateSubmissionRequest)->Result<(),Error>{ + let create_result=self.create_inner(CreateRequest{ + ModelID:create_info.ModelID, + }).await; + + match create_result{ + Ok(create_request)=>{ + // call create on api + self.api.create_submission(submissions_api::types::CreateSubmissionRequest{ + OperationID:create_info.OperationID, + AssetOwner:create_request.AssetOwner, + DisplayName:create_request.DisplayName.as_str(), + Creator:create_request.Creator.as_str(), + GameID:create_request.GameID, + AssetID:create_info.ModelID, + AssetVersion:create_request.AssetVersion, + }).await.map_err(Error::ApiActionSubmissionCreate)?; + }, + Err(e)=>{ + println!("oh no! {e}"); + }, + } + + Ok(()) + } +} diff --git a/validation/src/main.rs b/validation/src/main.rs index 355b0b4..30b443d 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -1,8 +1,12 @@ use futures::StreamExt; +mod rbx_util; mod message_handler; mod nats_types; mod types; +mod create; +mod create_mapfix; +mod create_submission; mod upload_mapfix; mod upload_submission; mod validator; diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 1f1f453..bff0f10 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -5,6 +5,8 @@ pub enum HandleMessageError{ DoubleAck(async_nats::Error), Json(serde_json::Error), UnknownSubject(String), + CreateMapfix(crate::create_mapfix::Error), + CreateSubmission(crate::create_submission::Error), UploadMapfix(crate::upload_mapfix::Error), UploadSubmission(crate::upload_submission::Error), ValidateMapfix(crate::validate_mapfix::Error), @@ -45,6 +47,8 @@ impl MessageHandler{ let message=message_result.map_err(HandleMessageError::Messages)?; message.double_ack().await.map_err(HandleMessageError::DoubleAck)?; match message.subject.as_str(){ + "maptest.mapfixes.create"=>self.create_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::CreateMapfix), + "maptest.submissions.create"=>self.create_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::CreateSubmission), "maptest.mapfixes.upload"=>self.upload_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadMapfix), "maptest.submissions.upload"=>self.upload_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadSubmission), "maptest.mapfixes.validate"=>self.validate_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::ValidateMapfix), diff --git a/validation/src/nats_types.rs b/validation/src/nats_types.rs index 15f0924..1401f83 100644 --- a/validation/src/nats_types.rs +++ b/validation/src/nats_types.rs @@ -4,6 +4,22 @@ // Requests are sent from maps-service to validator // Validation invokes the REST api to update the submissions +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct CreateSubmissionRequest{ + // operation_id is passed back in the response message + pub OperationID:i32, + pub ModelID:u64, +} + +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct CreateMapfixRequest{ + pub OperationID:i32, + pub ModelID:u64, + pub TargetAssetID:u64, +} + #[allow(nonstandard_style)] #[derive(serde::Deserialize)] pub struct ValidateSubmissionRequest{ diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs new file mode 100644 index 0000000..947bb9c --- /dev/null +++ b/validation/src/rbx_util.rs @@ -0,0 +1,119 @@ + +#[allow(dead_code)] +#[derive(Debug)] +pub enum ReadDomError{ + Binary(rbx_binary::DecodeError), + Xml(rbx_xml::DecodeError), + Read(std::io::Error), + Seek(std::io::Error), + UnknownFormat([u8;8]), +} +impl std::fmt::Display for ReadDomError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ReadDomError{} + +pub fn read_dom<R:std::io::Read+std::io::Seek>(input:&mut R)->Result<rbx_dom_weak::WeakDom,ReadDomError>{ + let mut first_8=[0u8;8]; + std::io::Read::read_exact(input,&mut first_8).map_err(ReadDomError::Read)?; + std::io::Seek::rewind(input).map_err(ReadDomError::Seek)?; + match &first_8[0..4]{ + b"<rob"=>{ + match &first_8[4..8]{ + b"lox!"=>rbx_binary::from_reader(input).map_err(ReadDomError::Binary), + b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(ReadDomError::Xml), + _=>Err(ReadDomError::UnknownFormat(first_8)), + } + }, + _=>Err(ReadDomError::UnknownFormat(first_8)), + } +} + +pub fn class_is_a(class:&str,superclass:&str)->bool{ + if class==superclass{ + return true + } + let class_descriptor=rbx_reflection_database::get().classes.get(class); + if let Some(descriptor)=&class_descriptor{ + if let Some(class_super)=&descriptor.superclass{ + return class_is_a(&class_super,superclass) + } + } + false +} + +pub fn find_first_child_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str,class:&str)->Option<&'a rbx_dom_weak::Instance>{ + for &referent in instance.children(){ + if let Some(c)=dom.get_by_ref(referent){ + if c.name==name&&class_is_a(c.class.as_str(),class) { + return Some(c); + } + } + } + None +} + +pub enum GameID{ + Bhop=1, + Surf=2, + FlyTrials=5, +} +#[derive(Debug)] +pub struct ParseGameIDError; +impl std::str::FromStr for GameID{ + type Err=ParseGameIDError; + fn from_str(s:&str)->Result<Self,Self::Err>{ + if s.starts_with("bhop_"){ + return Ok(GameID::Bhop); + } + if s.starts_with("surf_"){ + return Ok(GameID::Surf); + } + if s.starts_with("flytrials_"){ + return Ok(GameID::FlyTrials); + } + return Err(ParseGameIDError); + } +} + +pub struct MapInfo<'a>{ + pub display_name:Result<&'a str,StringValueError>, + pub creator:Result<&'a str,StringValueError>, + pub game_id:Result<GameID,ParseGameIDError>, +} + +pub enum StringValueError{ + ObjectNotFound, + ValueNotSet, + NonStringValue, +} + +fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{ + let instance=instance.ok_or(StringValueError::ObjectNotFound)?; + let value=instance.properties.get("Value").ok_or(StringValueError::ValueNotSet)?; + match value{ + rbx_dom_weak::types::Variant::String(value)=>Ok(value), + _=>Err(StringValueError::NonStringValue), + } +} + +#[derive(Debug)] +pub enum GetMapInfoError{ + ModelFileRootMustHaveOneChild, + ModelFileChildRefIsNil, +} + +pub fn get_mapinfo(dom:&rbx_dom_weak::WeakDom)->Result<MapInfo,GetMapInfoError>{ + let &[map_ref]=dom.root().children()else{ + return Err(GetMapInfoError::ModelFileRootMustHaveOneChild); + }; + let model_instance=dom.get_by_ref(map_ref).ok_or(GetMapInfoError::ModelFileChildRefIsNil)?; + + Ok(MapInfo{ + display_name:string_value(find_first_child_class(dom,model_instance,"DisplayName","StringValue")), + creator:string_value(find_first_child_class(dom,model_instance,"Creator","StringValue")), + game_id:model_instance.name.parse(), + }) +} diff --git a/validation/src/validator.rs b/validation/src/validator.rs index 0302c4e..c114ca3 100644 --- a/validation/src/validator.rs +++ b/validation/src/validator.rs @@ -1,6 +1,7 @@ use futures::TryStreamExt; use submissions_api::types::ResourceType; +use crate::rbx_util::{class_is_a,read_dom,ReadDomError}; use crate::types::ResourceID; const SCRIPT_CONCURRENCY:usize=16; @@ -275,51 +276,6 @@ impl crate::message_handler::MessageHandler{ } } -#[allow(dead_code)] -#[derive(Debug)] -pub enum ReadDomError{ - Binary(rbx_binary::DecodeError), - Xml(rbx_xml::DecodeError), - Read(std::io::Error), - Seek(std::io::Error), - UnknownFormat([u8;8]), -} -impl std::fmt::Display for ReadDomError{ - fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ - write!(f,"{self:?}") - } -} -impl std::error::Error for ReadDomError{} - -fn read_dom<R:std::io::Read+std::io::Seek>(input:&mut R)->Result<rbx_dom_weak::WeakDom,ReadDomError>{ - let mut first_8=[0u8;8]; - std::io::Read::read_exact(input,&mut first_8).map_err(ReadDomError::Read)?; - std::io::Seek::rewind(input).map_err(ReadDomError::Seek)?; - match &first_8[0..4]{ - b"<rob"=>{ - match &first_8[4..8]{ - b"lox!"=>rbx_binary::from_reader(input).map_err(ReadDomError::Binary), - b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(ReadDomError::Xml), - _=>Err(ReadDomError::UnknownFormat(first_8)), - } - }, - _=>Err(ReadDomError::UnknownFormat(first_8)), - } -} - -fn class_is_a(class:&str,superclass:&str)->bool{ - if class==superclass{ - return true - } - let class_descriptor=rbx_reflection_database::get().classes.get(class); - if let Some(descriptor)=&class_descriptor{ - if let Some(class_super)=&descriptor.superclass{ - return class_is_a(&class_super,superclass) - } - } - false -} - fn recursive_collect_superclass(objects:&mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str){ for &referent in instance.children(){ if let Some(c)=dom.get_by_ref(referent){