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){