From ff7e32d2a685e175078f31cad4ab8fd8b24aa2ba Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Fri, 12 Jan 2024 11:24:03 -0800
Subject: [PATCH] DecompileStyle

---
 src/main.rs | 130 +++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 102 insertions(+), 28 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 48e116e..661b214 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -33,6 +33,8 @@ struct Cli{
 	no_scripts:Option<bool>,
 	#[arg(long)]
 	no_template:Option<bool>,
+	#[arg(long)]
+	style:Option<String>,
 
 	//git options
 	#[arg(long)]
@@ -61,6 +63,13 @@ enum Commands{
 	DownloadAndDecompileHistoryIntoGit,
 }
 
+#[derive(Clone,Copy)]
+enum DecompileStyle{
+	Rox,
+	Rojo,
+	RoxRojo,
+}
+
 #[derive(Args)]
 struct PathBufList{
 	paths:Vec<std::path::PathBuf>
@@ -108,6 +117,21 @@ async fn main()->AResult<()>{
 		None=>None,
 	};
 
+	let decompile_style=match cli.style.as_deref(){
+		Some("rox")
+		|Some("Rox")=>Some(DecompileStyle::Rox),
+		Some("rojo")
+		|Some("Rojo")=>Some(DecompileStyle::Rojo),
+		Some("rox-rojo")
+		|Some("rojo-rox")
+		|Some("roxrojo")
+		|Some("rojorox")
+		|Some("RoxRojo")
+		|Some("RojoRox")=>Some(DecompileStyle::RoxRojo),
+		None=>None,
+		_=>return Err(anyhow::Error::msg("Invalid style")),
+	};
+
 	match cli.command{
 		Commands::DownloadHistory=>download_history(DownloadHistoryConfig{
 			output_folder:cli.output.unwrap(),
@@ -118,6 +142,7 @@ async fn main()->AResult<()>{
 		Commands::Upload=>upload_list(cookie.unwrap(),cli.group,vec![(cli.asset_id.unwrap(),cli.output.unwrap())]).await,
 		Commands::Compile=>compile(cli.input.unwrap(),cli.output.unwrap()),
 		Commands::Decompile=>decompile(DecompileConfig{
+			style:decompile_style.unwrap(),
 			input_file:cli.input.unwrap(),
 			output_folder:cli.output.unwrap(),
 			write_template:!cli.no_template.unwrap_or(false),
@@ -129,6 +154,7 @@ async fn main()->AResult<()>{
 			git_committer_email:cli.git_committer_email.unwrap(),
 			input_folder:cli.input.unwrap(),
 			output_folder:cli.output.unwrap(),
+			style:decompile_style.unwrap(),
 			write_template:!cli.no_template.unwrap_or(false),
 			write_models:!cli.no_models.unwrap_or(false),
 			write_scripts:!cli.no_scripts.unwrap_or(false),
@@ -139,6 +165,7 @@ async fn main()->AResult<()>{
 			cookie:cookie.unwrap(),
 			asset_id:cli.asset_id.unwrap(),
 			output_folder:cli.output.unwrap(),
+			style:decompile_style.unwrap(),
 			write_template:!cli.no_template.unwrap_or(false),
 			write_models:!cli.no_models.unwrap_or(false),
 			write_scripts:!cli.no_scripts.unwrap_or(false),
@@ -427,23 +454,34 @@ enum WriteStackInstruction<'a>{
 	Destroy(Ref),
 }
 
-#[derive(Default,serde::Deserialize,serde::Serialize)]
-#[allow(nonstandard_style,dead_code)]
+#[derive(Default)]
 struct PropertiesOverride{
-	//Name:Option<String>,
-	ClassName:Option<String>,
+	name:Option<String>,
+	class_name:Option<String>,
 }
 impl PropertiesOverride{
 	fn is_some(&self)->bool{
-		self.ClassName.is_some()
+		self.name.is_some()
+		||self.class_name.is_some()
 	}
 }
+impl std::fmt::Display for PropertiesOverride{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		if let Some(name)=self.name.as_deref(){
+			writeln!(f,"--! Properties.Name=\"{}\"",name)?;
+		}
+		if let Some(class_name)=self.class_name.as_deref(){
+			writeln!(f,"--! Properties.ClassName=\"{}\"",class_name)?;
+		}
+		Ok(())
+    }
+}
 
 fn sanitize<'a>(s:&'a str)->std::borrow::Cow<'a,str>{
 	lazy_regex::regex!(r"[^a-zA-Z0-9._-]").replace_all(s,"_")
 }
 
-fn write_item(dom:&rbx_dom_weak::WeakDom,mut file:std::path::PathBuf,node:&TreeNode,node_name_override:String,write_models:bool,write_scripts:bool)->AResult<()>{
+fn write_item(dom:&rbx_dom_weak::WeakDom,mut file:std::path::PathBuf,node:&TreeNode,node_name_override:String,mut properties:PropertiesOverride,style:DecompileStyle,write_models:bool,write_scripts:bool)->AResult<()>{
 	file.push(sanitize(node_name_override.as_str()).as_ref());
 	match node.class{
 		Class::Folder=>(),
@@ -451,11 +489,41 @@ fn write_item(dom:&rbx_dom_weak::WeakDom,mut file:std::path::PathBuf,node:&TreeN
 			if !write_scripts{
 				return Ok(())
 			}
+
+			//fix name
+			match style{
+				DecompileStyle::Rox=>(),
+				DecompileStyle::RoxRojo|DecompileStyle::Rojo=>{
+					match properties.class_name.as_deref(){
+						Some("LocalScript")=>{
+							file.set_extension("client");
+							properties.class_name=None;
+						},
+						Some("Script")=>{
+							file.set_extension("server");
+							properties.class_name=None;
+						},
+						// Some("ModuleScript")=>{
+						// 	file.set_extension("module");
+						// 	properties.class_name=None;
+						// },
+						None=>(),
+						Some(other)=>return Err(anyhow::Error::msg(format!("Attempt to write a {} as a script",other))),
+					}
+				}
+			}
+
 			assert!(file.set_extension("lua"),"could not set extension");
 			if let Some(item)=dom.get_by_ref(node.referent){
 				//TODO: delete disabled scripts
 				if let Some(rbx_dom_weak::types::Variant::String(source))=item.properties.get("Source"){
-					std::fs::write(file,source)?;
+					if properties.is_some(){
+						//rox style
+						let source=properties.to_string()+source.as_str();
+						std::fs::write(file,source)?;
+					}else{
+						std::fs::write(file,source)?;
+					}
 				}
 			}
 		},
@@ -574,6 +642,7 @@ fn generate_decompiled_context<R:Read>(input:R)->AResult<DecompiledContext>{
 }
 
 struct WriteConfig{
+	style:DecompileStyle,
 	output_folder:std::path::PathBuf,
 	write_template:bool,
 	write_models:bool,
@@ -593,44 +662,41 @@ async fn write_files(config:WriteConfig,mut context:DecompiledContext)->AResult<
 			WriteStackInstruction::PopFolder=>assert!(folder.pop(),"weirdness"),
 			WriteStackInstruction::Destroy(referent)=>destroy_queue.push(referent),
 			WriteStackInstruction::Node(node,name_count)=>{
-				//properties.json to override class or other simple properties
+				//track properties that must be overriden to compile folder structure back into a place file
 				let mut properties=PropertiesOverride::default();
 				let has_children=node.children.len()!=0;
 				match node.class{
 					Class::Folder=>(),
-					Class::ModuleScript=>{
-						//.lua files are ModuleScript by default
-						if has_children{
-							properties.ClassName=Some("ModuleScript".to_string())
-						}
-					},
-					Class::LocalScript=>properties.ClassName=Some("LocalScript".to_string()),
-					Class::Script=>properties.ClassName=Some("Script".to_string()),
+					Class::ModuleScript=>(),//.lua files are ModuleScript by default
+					Class::LocalScript=>properties.class_name=Some("LocalScript".to_string()),
+					Class::Script=>properties.class_name=Some("Script".to_string()),
 					Class::Model=>(),
 				}
 				let name_override=if 0<name_count{
+					properties.name=Some(node.name.clone());
 					format!("{}_{}",node.name,name_count)
 				}else{
 					node.name.clone()
 				};
-				if has_children||properties.is_some(){
+
+				if has_children{
 					//push temp subfolder
 					let mut subfolder=folder.clone();
 					subfolder.push(sanitize(name_override.as_str()).as_ref());
 					//make folder
 					tokio::fs::create_dir(subfolder.clone()).await?;
-					//write properties
-					if properties.is_some(){
-						let mut file=subfolder.clone();
-						file.push("properties");
-						assert!(file.set_extension("json"),"could not set extension");
-						tokio::fs::write(file,serde_json::to_string(&properties)?).await?;
-					}
+
+					let name_final=match config.style{
+						DecompileStyle::Rox
+						|DecompileStyle::RoxRojo=>name_override.clone(),
+						DecompileStyle::Rojo=>"init".to_owned(),
+					};
+
 					//write item in subfolder
-					write_queue.push((subfolder,node,name_override.clone()));
+					write_queue.push((subfolder,node,name_final,properties,config.style));
 				}else{
 					//write item
-					write_queue.push((folder.clone(),node,name_override.clone()));
+					write_queue.push((folder.clone(),node,name_override.clone(),properties,config.style));
 				}
 				//queue item to be deleted from dom after child objects are handled (stack is popped from the back)
 				match node.class{
@@ -657,8 +723,8 @@ async fn write_files(config:WriteConfig,mut context:DecompiledContext)->AResult<
 		let dom=&context.dom;
 		let write_models=config.write_models;
 		let write_scripts=config.write_scripts;
-		let results:Vec<AResult<()>>=rayon::iter::ParallelIterator::collect(rayon::iter::ParallelIterator::map(rayon::iter::IntoParallelIterator::into_par_iter(write_queue),|(write_path,node,node_name_override)|{
-			write_item(&dom,write_path,node,node_name_override,write_models,write_scripts)
+		let results:Vec<AResult<()>>=rayon::iter::ParallelIterator::collect(rayon::iter::ParallelIterator::map(rayon::iter::IntoParallelIterator::into_par_iter(write_queue),|(write_path,node,node_name_override,properties,style)|{
+			write_item(&dom,write_path,node,node_name_override,properties,style,write_models,write_scripts)
 		}));
 		for result in results{
 			result?;
@@ -683,6 +749,7 @@ async fn write_files(config:WriteConfig,mut context:DecompiledContext)->AResult<
 }
 
 struct DecompileConfig{
+	style:DecompileStyle,
 	input_file:std::path::PathBuf,
 	output_folder:std::path::PathBuf,
 	write_template:bool,
@@ -703,6 +770,7 @@ async fn decompile(config:DecompileConfig)->AResult<()>{
 	//generate folders, models, and scripts
 	//delete models and scripts from dom
 	write_files(WriteConfig{
+		style:config.style,
 		output_folder:config.output_folder,
 		write_template:config.write_template,
 		write_models:config.write_models,
@@ -716,6 +784,7 @@ struct WriteCommitConfig{
 	git_committer_name:String,
 	git_committer_email:String,
 	output_folder:std::path::PathBuf,
+	style:DecompileStyle,
 	write_template:bool,
 	write_models:bool,
 	write_scripts:bool,
@@ -745,6 +814,7 @@ async fn write_commit(config:WriteCommitConfig,b:Result<AResult<(AssetVersion,De
 
 	//write files
 	write_files(WriteConfig{
+		style:config.style,
 		output_folder:config.output_folder.clone(),
 		write_template:config.write_template,
 		write_models:config.write_models,
@@ -804,6 +874,7 @@ struct DecompileHistoryConfig{
 	git_committer_name:String,
 	git_committer_email:String,
 	input_folder:std::path::PathBuf,
+	style:DecompileStyle,
 	output_folder:std::path::PathBuf,
 	write_template:bool,
 	write_models:bool,
@@ -834,6 +905,7 @@ async fn decompile_history_into_git(config:DecompileHistoryConfig)->AResult<()>{
 		match write_commit(WriteCommitConfig{
 			git_committer_name:config.git_committer_name.clone(),
 			git_committer_email:config.git_committer_email.clone(),
+			style:config.style,
 			output_folder:config.output_folder.clone(),
 			write_template:config.write_template,
 			write_models:config.write_models,
@@ -851,6 +923,7 @@ struct DownloadAndDecompileHistoryConfig{
 	asset_id:AssetID,
 	git_committer_name:String,
 	git_committer_email:String,
+	style:DecompileStyle,
 	output_folder:std::path::PathBuf,
 	write_template:bool,
 	write_models:bool,
@@ -884,6 +957,7 @@ async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHist
 	.buffered(CONCURRENT_DECODE)
 	.for_each(|join_handle_result|async{
 		match write_commit(WriteCommitConfig{
+			style:config.style,
 			git_committer_name:config.git_committer_name.clone(),
 			git_committer_email:config.git_committer_email.clone(),
 			output_folder:config.output_folder.clone(),