diff --git a/Cargo.lock b/Cargo.lock index fa80476..15e18e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,7 @@ dependencies = [ "rbx_dom_weak", "rbx_reflection_database", "rbx_xml", + "rox_compiler", "serde_json", "tokio", ] @@ -1351,6 +1352,15 @@ dependencies = [ "serde", ] +[[package]] +name = "rox_compiler" +version = "0.1.0" +dependencies = [ + "lazy-regex", + "rbx_dom_weak", + "tokio", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index 591195c..bcf00cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ -workspace = { members = ["rbx_asset"] } +workspace = { members = ["rbx_asset", "rox_compiler"] } [package] name = "asset-tool" version = "0.3.1" @@ -19,6 +19,7 @@ rbx_binary = "0.7.4" rbx_dom_weak = "2.7.0" rbx_reflection_database = "0.2.10" rbx_xml = "0.13.3" +rox_compiler = { path = "rox_compiler" } serde_json = "1.0.111" tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "fs"] } diff --git a/rox_compiler/Cargo.toml b/rox_compiler/Cargo.toml new file mode 100644 index 0000000..97c4a0d --- /dev/null +++ b/rox_compiler/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rox_compiler" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy-regex = "3.1.0" +rbx_dom_weak = "2.7.0" +tokio = { version = "1.35.1", features = ["fs"] } diff --git a/rox_compiler/src/compile.rs b/rox_compiler/src/compile.rs new file mode 100644 index 0000000..03a1867 --- /dev/null +++ b/rox_compiler/src/compile.rs @@ -0,0 +1,491 @@ +use rbx_dom_weak::types::Ref; + +//holy smokes what am I doing lmao +//This giant machine is supposed to search for files according to style rules +//e.g. ScriptName.server.lua or init.lua +//Obviously I got carried away +//I could use an enum! +//I could use a struct! +//I could use a trait! +//I could use an error! +//I could use a match! +//I could use a function! +//eventually: +#[derive(Debug)] +#[allow(dead_code)]//idk why this thinks it's dead code, the errors are printed out in various places +enum QueryResolveError{ + NotFound,//0 results + Ambiguous,//>1 results + JoinError(tokio::task::JoinError), + IO(std::io::Error), +} +impl std::fmt::Display for QueryResolveError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for QueryResolveError{} + +struct FileWithName{ + file:tokio::fs::File, + name:String, +} + +async fn get_file_async(mut path:PathBuf,file_name:impl AsRef)->Result{ + let name=file_name.as_ref().to_str().unwrap().to_owned(); + path.push(file_name); + match tokio::fs::File::open(path).await{ + Ok(file)=>Ok(FileWithName{file,name}), + Err(e)=>match e.kind(){ + std::io::ErrorKind::NotFound=>Err(QueryResolveError::NotFound), + _=>Err(QueryResolveError::IO(e)), + }, + } +} +type QueryHintResult=Result; +trait Query{ + async fn resolve(self)->QueryHintResult; +} +type QueryHandle=tokio::task::JoinHandle>; +struct QuerySingle{ + script:QueryHandle, +} +impl QuerySingle{ + fn rox(search_path:&PathBuf,search_name:&str)->Self{ + Self{ + script:tokio::spawn(get_file_async(search_path.clone(),format!("{}.lua",search_name))) + } + } +} +impl Query for QuerySingle{ + async fn resolve(self)->QueryHintResult{ + match self.script.await{ + Ok(Ok(file))=>Ok(FileHint{file,hint:ScriptHint::ModuleScript}), + Ok(Err(e))=>Err(e), + Err(e)=>Err(QueryResolveError::JoinError(e)), + } + } +} +struct QueryTriple{ + module:QueryHandle, + server:QueryHandle, + client:QueryHandle, +} +impl QueryTriple{ + fn rox_rojo(search_path:&PathBuf,search_name:&str,search_module:bool)->Self{ + //this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion + let module_name=if search_module{ + format!("{}.module.lua",search_name) + }else{ + format!("{}.lua",search_name) + }; + Self{ + module:tokio::spawn(get_file_async(search_path.clone(),module_name)), + server:tokio::spawn(get_file_async(search_path.clone(),format!("{}.server.lua",search_name))), + client:tokio::spawn(get_file_async(search_path.clone(),format!("{}.client.lua",search_name))), + } + } + fn rojo(search_path:&PathBuf)->Self{ + QueryTriple::rox_rojo(search_path,"init",false) + } +} +//these functions can be achieved with macros, but I have not learned that yet +fn mega_triple_join(query_triplet:(QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ + match query_triplet{ + //unambiguously locate file + (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), + //multiple files located + (Ok(_),Ok(_),Err(QueryResolveError::NotFound)) + |(Ok(_),Err(QueryResolveError::NotFound),Ok(_)) + |(Err(QueryResolveError::NotFound),Ok(_),Ok(_)) + |(Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), + //no files located + (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), + //other error + (Err(e),_,_) + |(_,Err(e),_) + |(_,_,Err(e))=>Err(e), + } +} +//LETS GOOOOOOOOOOOOOOOO +fn mega_quadruple_join(query_quad:(QueryHintResult,QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ + match query_quad{ + //unambiguously locate file + (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), + //multiple files located + (Ok(_),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) + |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) + |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) + |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) + |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound)) + |(Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_)) + |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Ok(_)) + |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) + |(Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) + |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) + |(Ok(_),Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), + //no files located + (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), + //other error + (Err(e),_,_,_) + |(_,Err(e),_,_) + |(_,_,Err(e),_) + |(_,_,_,Err(e))=>Err(e), + } +} +impl Query for QueryTriple{ + async fn resolve(self)->QueryHintResult{ + let (module,server,client)=tokio::join!(self.module,self.server,self.client); + mega_triple_join(( + module.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), + server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), + client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), + )) + } +} +struct QueryQuad{ + module_implicit:QueryHandle, + module_explicit:QueryHandle, + server:QueryHandle, + client:QueryHandle, +} +impl QueryQuad{ + fn rox_rojo(search_path:&PathBuf,search_name:&str)->Self{ + let fill=QueryTriple::rox_rojo(search_path,search_name,true); + Self{ + module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua + module_explicit:fill.module,//Script.module.lua + server:fill.server, + client:fill.client, + } + } +} +impl Query for QueryQuad{ + async fn resolve(self)->QueryHintResult{ + let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client); + mega_quadruple_join(( + module_implicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), + module_explicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), + server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), + client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), + )) + } +} + +struct ScriptWithOverrides{ + overrides:PropertiesOverride, + source:String, +} + +fn extract_script_overrides(mut source:String)->AResult{ + let mut overrides=PropertiesOverride::default(); + let mut count=0; + for line in source.lines(){ + //only string type properties are supported atm + if let Some(captures)=lazy_regex::regex!(r#"^\-\-\!\s*Properties\.([A-z]\w*)\s*\=\s*"(\w+)"$"#) + .captures(line){ + count+=line.len(); + match &captures[1]{ + "Name"=>overrides.name=Some(captures[2].to_owned()), + "ClassName"=>overrides.class=Some(captures[2].to_owned()), + other=>Err(anyhow::Error::msg(format!("Unimplemented property {other}")))?, + } + }else{ + break; + } + } + Ok(ScriptWithOverrides{overrides,source:source.split_off(count)}) +} + +async fn script_node(search_name:&str,mut file:FileWithName,hint:ScriptHint)->AResult{ + //read entire file + let mut buf=String::new(); + file.file.read_to_string(&mut buf).await?; + //regex script according to Properties lines at the top + let script_with_overrides=extract_script_overrides(buf)?; + //script + Ok(CompileNode{ + blacklist:Some(file.name), + name:script_with_overrides.overrides.name.unwrap_or_else(||search_name.to_owned()), + class:match (script_with_overrides.overrides.class.as_deref(),hint){ + (Some("ModuleScript"),_) + |(None,ScriptHint::ModuleScript)=>CompileClass::ModuleScript(script_with_overrides.source), + (Some("LocalScript"),_) + |(None,ScriptHint::LocalScript)=>CompileClass::LocalScript(script_with_overrides.source), + (Some("Script"),_) + |(None,ScriptHint::Script)=>CompileClass::Script(script_with_overrides.source), + other=>Err(anyhow::Error::msg(format!("Invalid hint or class {other:?}")))?, + }, + }) +} + +async fn model_node(search_name:&str,mut file:FileWithName)->AResult{ + //read entire file + let mut buf=Vec::new(); + file.file.read_to_end(&mut buf).await?; + //model + Ok(CompileNode{ + blacklist:Some(file.name), + name:search_name.to_owned(),//wrong but gets overwritten by internal model name + class:CompileClass::Model(buf), + }) +} + +async fn locate_override_file(entry:&tokio::fs::DirEntry,style:Option)->AResult{ + let contents_folder=entry.path(); + let file_name=entry.file_name(); + let search_name=file_name.to_str().unwrap(); + //scan inside the folder for an object to define the class of the folder + let script_query=async {match style{ + Some(DecompileStyle::Rox)=>QuerySingle::rox(&contents_folder,search_name).resolve().await, + Some(DecompileStyle::RoxRojo)=>QueryQuad::rox_rojo(&contents_folder,search_name).resolve().await, + Some(DecompileStyle::Rojo)=>QueryTriple::rojo(&contents_folder).resolve().await, + //try all three and complain if there is ambiguity + None=>mega_triple_join(tokio::join!( + QuerySingle::rox(&contents_folder,search_name).resolve(), + //true=search for module here to avoid ambiguity with QuerySingle::rox results + QueryTriple::rox_rojo(&contents_folder,search_name,true).resolve(), + QueryTriple::rojo(&contents_folder).resolve(), + )) + }}; + //model files are rox & rox-rojo only, so it's a lot less work... + let model_query=get_file_async(contents_folder.clone(),format!("{}.rbxmx",search_name)); + //model? script? both? + Ok(match tokio::join!(script_query,model_query){ + (Ok(FileHint{file,hint}),Err(QueryResolveError::NotFound))=>script_node(search_name,file,hint).await?, + (Err(QueryResolveError::NotFound),Ok(file))=>model_node(search_name,file).await?, + (Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous)?, + //neither + (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>CompileNode{ + name:search_name.to_owned(), + blacklist:None, + class:CompileClass::Folder, + }, + //other error + (Err(e),_) + |(_,Err(e))=>Err(e)? + }) +} + + +enum FileDiscernment{ + Model, + Script(ScriptHint), +} + +async fn discern_file(entry:&tokio::fs::DirEntry,style:Option)->AResult{ + let mut file_name=entry + .file_name() + .into_string() + .map_err(|e|anyhow::Error::msg(format!("insane file name {e:?}")))?; + //reject goobers + let is_goober=match style{ + Some(DecompileStyle::Rojo)=>true, + _=>false, + }; + let (ext_len,file_discernment)={ + if let Some(captures)=lazy_regex::regex!(r"^.*(.module.lua|.client.lua|.server.lua)$") + .captures(file_name.as_str()){ + let ext=&captures[1]; + (ext.len(),match ext{ + ".module.lua"=>{ + if is_goober{ + Err(anyhow::Error::msg(format!("File extension {ext} not supported in style {style:?}")))?; + } + FileDiscernment::Script(ScriptHint::ModuleScript) + }, + ".client.lua"=>FileDiscernment::Script(ScriptHint::LocalScript), + ".server.lua"=>FileDiscernment::Script(ScriptHint::Script), + _=>panic!("Regex failed"), + }) + }else if let Some(captures)=lazy_regex::regex!(r"^.*(.rbxmx|.lua)$") + .captures(file_name.as_str()){ + let ext=&captures[1]; + (ext.len(),match ext{ + ".rbxmx"=>{ + if is_goober{ + Err(anyhow::Error::msg(format!("File extension {ext} not supported in style {style:?}")))?; + } + FileDiscernment::Model + }, + ".lua"=>FileDiscernment::Script(ScriptHint::ModuleScript), + _=>panic!("Regex failed"), + }) + }else{ + return Err(anyhow::Error::msg("No file extension")); + } + }; + file_name.truncate(file_name.len()-ext_len); + let file=tokio::fs::File::open(entry.path()).await?; + Ok(match file_discernment{ + FileDiscernment::Model=>model_node(file_name.as_str(),FileWithName{file,name:file_name.clone()}).await?, + FileDiscernment::Script(hint)=>script_node(file_name.as_str(),FileWithName{file,name:file_name.clone()},hint).await?, + }) +} + +#[derive(Debug)] +enum ScriptHint{ + Script, + LocalScript, + ModuleScript, +} +struct FileHint{ + file:FileWithName, + hint:ScriptHint, +} + +enum PreparedData{ + Model(rbx_dom_weak::WeakDom), + Builder(rbx_dom_weak::InstanceBuilder), +} + +enum CompileClass{ + Folder, + Script(String), + LocalScript(String), + ModuleScript(String), + Model(Vec), +} + +struct CompileNode{ + name:String, + blacklist:Option, + class:CompileClass, +} + +enum CompileStackInstruction{ + TraverseReferent(rbx_dom_weak::types::Ref,Option), + PopFolder, +} + +fn script_builder(class:&str,name:&str,source:String)->rbx_dom_weak::InstanceBuilder{ + let mut builder=rbx_dom_weak::InstanceBuilder::new(class); + builder.set_name(name); + builder.add_property("Source",rbx_dom_weak::types::Variant::String(source)); + builder +} + +enum TooComplicated{ + Stop, + Value(T), + Skip, +} + +async fn compile(config:CompileConfig,&mut dom:rbx_dom_weak::WeakDom)->Result<(),CompileError>{ + //add in scripts and models + let mut folder=config.input_folder.clone(); + let mut stack:Vec=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)]; + while let Some(instruction)=stack.pop(){ + match instruction{ + CompileStackInstruction::TraverseReferent(item_ref,blacklist)=>{ + let sans={ + let item=dom.get_by_ref(item_ref).ok_or(anyhow::Error::msg("null child ref"))?; + sanitize(item.name.as_str()).to_string() + }; + folder.push(sans.as_str()); + stack.push(CompileStackInstruction::PopFolder); + //check if a folder exists with item.name + if let Ok(dir)=tokio::fs::read_dir(folder.as_path()).await{ + let mut exist_names:std::collections::HashSet={ + let item=dom.get_by_ref(item_ref).ok_or(anyhow::Error::msg("null child ref"))?; + //push existing dom children objects onto stack (unrelated to exist_names) + stack.extend(item.children().into_iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None))); + //get names of existing objects + item.children().into_iter().map(|&child_ref|{ + let child=dom.get_by_ref(child_ref).ok_or(anyhow::Error::msg("null child ref"))?; + Ok::<_,anyhow::Error>(sanitize(child.name.as_str()).to_string()) + }).collect::>()? + }; + if let Some(dont)=blacklist{ + exist_names.insert(dont); + } + //generate children from folder contents UNLESS! item already has a child of the same name + + let style=config.style; + let exist_names=&exist_names; + futures::stream::unfold(dir,|mut dir1|async{ + //thread the needle! follow the path that dir takes! + let ret1={ + //capture a scoped mutable reference so we can forward dir to the next call even on an error + let dir2=&mut dir1; + (||async move{//error catcher so I can use ? + let ret2=if let Some(entry)=dir2.next_entry().await?{ + //cull early even if supporting things with identical names is possible + if exist_names.contains(entry.file_name().to_str().unwrap()){ + TooComplicated::Skip + }else{ + TooComplicated::Value(entry) + } + }else{ + TooComplicated::Stop + }; + Ok::<_,anyhow::Error>(ret2) + })().await + }; + match ret1{ + Ok(TooComplicated::Stop)=>None, + Ok(TooComplicated::Skip)=>Some((Ok(None),dir1)), + Ok(TooComplicated::Value(v))=>Some((Ok(Some(v)),dir1)), + Err(e)=>Some((Err(e),dir1)), + } + }) + + //gotta spawn off the worker threads (Model is slow) + .then(|bog|async{ + match bog{ + Ok(Some(entry))=>tokio::spawn(async move{ + let met=entry.metadata().await?; + //discern that bad boy + let compile_class=match met.is_dir(){ + true=>locate_override_file(&entry,style).await?, + false=>discern_file(&entry,style).await?, + }; + //prepare data structure + Ok(Some((compile_class.blacklist,match compile_class.class{ + CompileClass::Folder=>PreparedData::Builder(rbx_dom_weak::InstanceBuilder::new("Folder").with_name(compile_class.name.as_str())), + CompileClass::Script(source)=>PreparedData::Builder(script_builder("Script",compile_class.name.as_str(),source)), + CompileClass::LocalScript(source)=>PreparedData::Builder(script_builder("LocalScript",compile_class.name.as_str(),source)), + CompileClass::ModuleScript(source)=>PreparedData::Builder(script_builder("ModuleScript",compile_class.name.as_str(),source)), + CompileClass::Model(buf)=>PreparedData::Model(rbx_xml::from_reader_default(std::io::Cursor::new(buf))?), + }))) + }).await?, + Ok(None)=>Ok(None), + Err(e)=>Err(e), + } + }) + + //is this even what I want? + .map(|f|async{f}).buffer_unordered(32) + + //begin processing immediately + .fold((&mut stack,&mut dom),|(stack,dom),bog:Result<_,anyhow::Error>|async{ + //push child objects onto dom serially as they arrive + match bog{ + Ok(Some((blacklist,data)))=>{ + let referent=match data{ + PreparedData::Model(mut model_dom)=>{ + let referent=model_dom.root().children()[0]; + model_dom.transfer(referent,dom,item_ref); + referent + }, + PreparedData::Builder(script)=>dom.insert(item_ref,script), + }; + //new children need to be traversed + stack.push(CompileStackInstruction::TraverseReferent(referent,blacklist)); + }, + Ok(None)=>(), + Err(e)=>println!("error lole {e:?}"), + } + (stack,dom) + }).await; + } + }, + CompileStackInstruction::PopFolder=>assert!(folder.pop(),"pop folder bad"), + } + } +} diff --git a/rox_compiler/src/decompile.rs b/rox_compiler/src/decompile.rs new file mode 100644 index 0000000..b7e7c82 --- /dev/null +++ b/rox_compiler/src/decompile.rs @@ -0,0 +1,337 @@ +use std::path::PathBuf; + +use rbx_dom_weak::types::Ref; + +#[derive(PartialEq)] +enum Class{ + Folder, + ModuleScript, + LocalScript, + Script, + Model, +} + +struct TreeNode{ + name:String, + referent:Ref, + parent:Ref, + class:Class, + children:Vec, +} +impl TreeNode{ + fn new(name:String,referent:Ref,parent:Ref,class:Class)->Self{ + Self{ + name, + referent, + parent, + class, + children:Vec::new(), + } + } +} + +enum TrimStackInstruction{ + Referent(Ref), + IncrementScript, + DecrementScript, +} + +enum WriteStackInstruction<'a>{ + Node(&'a TreeNode,u32),//(Node,NameTally) + PushFolder(String), + PopFolder, + Destroy(Ref), +} + +#[derive(Default)] +struct PropertiesOverride{ + name:Option, + class:Option, +} +impl PropertiesOverride{ + fn is_some(&self)->bool{ + self.name.is_some() + ||self.class.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)=self.class.as_deref(){ + writeln!(f,"--!Properties.ClassName = \"{}\"",class)?; + } + Ok(()) + } +} + +fn sanitize<'a>(s:&'a str)->std::borrow::Cow<'a,str>{ + lazy_regex::regex!(r"[^A-z0-9.-]").replace_all(s,"_") +} + +fn write_item(dom:&rbx_dom_weak::WeakDom,mut file: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=>(), + Class::ModuleScript|Class::LocalScript|Class::Script=>{ + if !write_scripts{ + return Ok(()) + } + + //set extension + match style{ + DecompileStyle::Rox=>assert!(file.set_extension("lua"),"could not set extension"), + DecompileStyle::RoxRojo|DecompileStyle::Rojo=>{ + match properties.class.as_deref(){ + Some("LocalScript")=>{ + file.set_extension("client.lua"); + properties.class=None; + }, + Some("Script")=>{ + file.set_extension("server.lua"); + properties.class=None; + }, + // Some("ModuleScript")=>{ + // file.set_extension("module"); + // properties.class=None; + // }, + None=>assert!(file.set_extension("lua"),"could not set extension"), + Some(other)=>Err(anyhow::Error::msg(format!("Attempt to write a {} as a script",other)))?, + } + } + } + + 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"){ + 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)?; + } + } + } + }, + Class::Model=>{ + if !write_models{ + return Ok(()) + } + assert!(file.set_extension("rbxmx")); + let output=std::io::BufWriter::new(std::fs::File::create(file)?); + rbx_xml::to_writer_default(output,dom,&[node.referent])?; + }, + } + Ok(()) +} + +struct DecompiledContext{ + dom:rbx_dom_weak::WeakDom, + tree_refs:std::collections::HashMap, +} + +fn generate_decompiled_context(input:R)->AResult{ + let dom=load_dom(input)?; + + let mut tree_refs=std::collections::HashMap::new(); + tree_refs.insert(dom.root_ref(),TreeNode::new( + "src".to_owned(), + dom.root_ref(), + Ref::none(), + Class::Folder + )); + + //run rules + let mut stack=vec![dom.root()]; + while let Some(item)=stack.pop(){ + let class=match item.class.as_str(){ + "ModuleScript"=>Class::ModuleScript, + "LocalScript"=>Class::LocalScript, + "Script"=>Class::Script, + "Model"=>Class::Model, + _=>Class::Folder, + }; + let skip=match class{ + Class::Model=>true, + _=>false, + }; + if let Some(parent_node)=tree_refs.get_mut(&item.parent()){ + let referent=item.referent(); + let node=TreeNode::new(item.name.clone(),referent,parent_node.referent,class); + parent_node.children.push(referent); + tree_refs.insert(referent,node); + } + //look no further, turn this node and all its children into a model + if skip{ + continue; + } + for &referent in item.children(){ + if let Some(c)=dom.get_by_ref(referent){ + stack.push(c); + } + } + } + + //trim empty folders + let mut script_count=0; + let mut stack:Vec=tree_refs.get(&dom.root_ref()).unwrap().children + .iter().map(|&c|TrimStackInstruction::Referent(c)).collect(); + while let Some(instruction)=stack.pop(){ + match instruction{ + TrimStackInstruction::IncrementScript=>script_count+=1, + TrimStackInstruction::DecrementScript=>script_count-=1, + TrimStackInstruction::Referent(referent)=>{ + let mut delete=None; + if let Some(node)=tree_refs.get_mut(&referent){ + if node.class==Class::Folder&&script_count!=0{ + node.class=Class::Model + } + if node.class==Class::Folder&&node.children.len()==0{ + delete=Some(node.parent); + }else{ + //how the hell do I do this better without recursion + let is_script=match node.class{ + Class::ModuleScript|Class::LocalScript|Class::Script=>true, + _=>false, + }; + //stack is popped from back + if is_script{ + stack.push(TrimStackInstruction::DecrementScript); + } + for &child_referent in &node.children{ + stack.push(TrimStackInstruction::Referent(child_referent)); + } + if is_script{ + stack.push(TrimStackInstruction::IncrementScript); + } + } + } + //trim referent + if let Some(parent_ref)=delete{ + let parent_node=tree_refs.get_mut(&parent_ref) + .expect("parent_ref does not exist in tree_refs"); + parent_node.children.remove( + parent_node.children.iter() + .position(|&r|r==referent) + .expect("parent.children does not contain referent") + ); + tree_refs.remove(&referent); + } + }, + } + } + + Ok(DecompiledContext{ + dom, + tree_refs, + }) +} + +struct WriteConfig{ + style:DecompileStyle, + output_folder:PathBuf, + write_template:bool, + write_models:bool, + write_scripts:bool, +} + +async fn write_files(config:WriteConfig,mut context:DecompiledContext)->AResult<()>{ + let mut write_queue=Vec::new(); + let mut destroy_queue=Vec::new(); + + let mut name_tally=std::collections::HashMap::::new(); + let mut folder=config.output_folder.clone(); + let mut stack=vec![WriteStackInstruction::Node(context.tree_refs.get(&context.dom.root_ref()).unwrap(),0)]; + while let Some(instruction)=stack.pop(){ + match instruction{ + WriteStackInstruction::PushFolder(component)=>folder.push(component), + WriteStackInstruction::PopFolder=>assert!(folder.pop(),"weirdness"), + WriteStackInstruction::Destroy(referent)=>destroy_queue.push(referent), + WriteStackInstruction::Node(node,name_count)=>{ + //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 + Class::LocalScript=>properties.class=Some("LocalScript".to_owned()), + Class::Script=>properties.class=Some("Script".to_owned()), + Class::Model=>(), + } + let name_override=if 0name_override.clone(), + DecompileStyle::Rojo=>"init".to_owned(), + }; + + //write item in subfolder + write_queue.push((subfolder,node,name_final,properties,config.style)); + }else{ + //write item + 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{ + Class::Folder=>(), + _=>stack.push(WriteStackInstruction::Destroy(node.referent)), + } + if has_children{ + stack.push(WriteStackInstruction::PopFolder); + name_tally.clear(); + for referent in &node.children{ + if let Some(c)=context.tree_refs.get(referent){ + let v=name_tally.entry(c.name.clone()).and_modify(|v|*v+=1).or_default(); + stack.push(WriteStackInstruction::Node(c,*v)); + } + } + stack.push(WriteStackInstruction::PushFolder(sanitize(name_override.as_str()).to_string())); + } + }, + } + } + + //run the async + { + let dom=&context.dom; + let write_models=config.write_models; + let write_scripts=config.write_scripts; + let results:Vec>=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?; + } + } + + //run the destroy + for destroy_ref in destroy_queue{ + context.dom.destroy(destroy_ref); + } + + //write what remains in template.rbxlx + if config.write_template{ + let mut file=config.output_folder.clone(); + file.push("template"); + assert!(file.set_extension("rbxlx")); + let output=std::io::BufWriter::new(std::fs::File::create(file)?); + rbx_xml::to_writer_default(output,&context.dom,context.dom.root().children())?; + } + + Ok(()) +} diff --git a/rox_compiler/src/lib.rs b/rox_compiler/src/lib.rs new file mode 100644 index 0000000..9758e5d --- /dev/null +++ b/rox_compiler/src/lib.rs @@ -0,0 +1,2 @@ +pub mod compile; +pub mod decompile; diff --git a/src/main.rs b/src/main.rs index e00a3c1..509702a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -537,339 +537,6 @@ fn load_dom(input:R)->AResult{ } } -#[derive(PartialEq)] -enum Class{ - Folder, - ModuleScript, - LocalScript, - Script, - Model, -} - -struct TreeNode{ - name:String, - referent:Ref, - parent:Ref, - class:Class, - children:Vec, -} -impl TreeNode{ - fn new(name:String,referent:Ref,parent:Ref,class:Class)->Self{ - Self{ - name, - referent, - parent, - class, - children:Vec::new(), - } - } -} - -enum TrimStackInstruction{ - Referent(Ref), - IncrementScript, - DecrementScript, -} - -enum WriteStackInstruction<'a>{ - Node(&'a TreeNode,u32),//(Node,NameTally) - PushFolder(String), - PopFolder, - Destroy(Ref), -} - -#[derive(Default)] -struct PropertiesOverride{ - name:Option, - class:Option, -} -impl PropertiesOverride{ - fn is_some(&self)->bool{ - self.name.is_some() - ||self.class.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)=self.class.as_deref(){ - writeln!(f,"--!Properties.ClassName = \"{}\"",class)?; - } - Ok(()) - } -} - -fn sanitize<'a>(s:&'a str)->std::borrow::Cow<'a,str>{ - lazy_regex::regex!(r"[^A-z0-9.-]").replace_all(s,"_") -} - -fn write_item(dom:&rbx_dom_weak::WeakDom,mut file: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=>(), - Class::ModuleScript|Class::LocalScript|Class::Script=>{ - if !write_scripts{ - return Ok(()) - } - - //set extension - match style{ - DecompileStyle::Rox=>assert!(file.set_extension("lua"),"could not set extension"), - DecompileStyle::RoxRojo|DecompileStyle::Rojo=>{ - match properties.class.as_deref(){ - Some("LocalScript")=>{ - file.set_extension("client.lua"); - properties.class=None; - }, - Some("Script")=>{ - file.set_extension("server.lua"); - properties.class=None; - }, - // Some("ModuleScript")=>{ - // file.set_extension("module"); - // properties.class=None; - // }, - None=>assert!(file.set_extension("lua"),"could not set extension"), - Some(other)=>Err(anyhow::Error::msg(format!("Attempt to write a {} as a script",other)))?, - } - } - } - - 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"){ - 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)?; - } - } - } - }, - Class::Model=>{ - if !write_models{ - return Ok(()) - } - assert!(file.set_extension("rbxmx")); - let output=std::io::BufWriter::new(std::fs::File::create(file)?); - rbx_xml::to_writer_default(output,dom,&[node.referent])?; - }, - } - Ok(()) -} - -struct DecompiledContext{ - dom:rbx_dom_weak::WeakDom, - tree_refs:std::collections::HashMap, -} - -fn generate_decompiled_context(input:R)->AResult{ - let dom=load_dom(input)?; - - let mut tree_refs=std::collections::HashMap::new(); - tree_refs.insert(dom.root_ref(),TreeNode::new( - "src".to_owned(), - dom.root_ref(), - Ref::none(), - Class::Folder - )); - - //run rules - let mut stack=vec![dom.root()]; - while let Some(item)=stack.pop(){ - let class=match item.class.as_str(){ - "ModuleScript"=>Class::ModuleScript, - "LocalScript"=>Class::LocalScript, - "Script"=>Class::Script, - "Model"=>Class::Model, - _=>Class::Folder, - }; - let skip=match class{ - Class::Model=>true, - _=>false, - }; - if let Some(parent_node)=tree_refs.get_mut(&item.parent()){ - let referent=item.referent(); - let node=TreeNode::new(item.name.clone(),referent,parent_node.referent,class); - parent_node.children.push(referent); - tree_refs.insert(referent,node); - } - //look no further, turn this node and all its children into a model - if skip{ - continue; - } - for &referent in item.children(){ - if let Some(c)=dom.get_by_ref(referent){ - stack.push(c); - } - } - } - - //trim empty folders - let mut script_count=0; - let mut stack:Vec=tree_refs.get(&dom.root_ref()).unwrap().children - .iter().map(|&c|TrimStackInstruction::Referent(c)).collect(); - while let Some(instruction)=stack.pop(){ - match instruction{ - TrimStackInstruction::IncrementScript=>script_count+=1, - TrimStackInstruction::DecrementScript=>script_count-=1, - TrimStackInstruction::Referent(referent)=>{ - let mut delete=None; - if let Some(node)=tree_refs.get_mut(&referent){ - if node.class==Class::Folder&&script_count!=0{ - node.class=Class::Model - } - if node.class==Class::Folder&&node.children.len()==0{ - delete=Some(node.parent); - }else{ - //how the hell do I do this better without recursion - let is_script=match node.class{ - Class::ModuleScript|Class::LocalScript|Class::Script=>true, - _=>false, - }; - //stack is popped from back - if is_script{ - stack.push(TrimStackInstruction::DecrementScript); - } - for &child_referent in &node.children{ - stack.push(TrimStackInstruction::Referent(child_referent)); - } - if is_script{ - stack.push(TrimStackInstruction::IncrementScript); - } - } - } - //trim referent - if let Some(parent_ref)=delete{ - let parent_node=tree_refs.get_mut(&parent_ref) - .expect("parent_ref does not exist in tree_refs"); - parent_node.children.remove( - parent_node.children.iter() - .position(|&r|r==referent) - .expect("parent.children does not contain referent") - ); - tree_refs.remove(&referent); - } - }, - } - } - - Ok(DecompiledContext{ - dom, - tree_refs, - }) -} - -struct WriteConfig{ - style:DecompileStyle, - output_folder:PathBuf, - write_template:bool, - write_models:bool, - write_scripts:bool, -} - -async fn write_files(config:WriteConfig,mut context:DecompiledContext)->AResult<()>{ - let mut write_queue=Vec::new(); - let mut destroy_queue=Vec::new(); - - let mut name_tally=std::collections::HashMap::::new(); - let mut folder=config.output_folder.clone(); - let mut stack=vec![WriteStackInstruction::Node(context.tree_refs.get(&context.dom.root_ref()).unwrap(),0)]; - while let Some(instruction)=stack.pop(){ - match instruction{ - WriteStackInstruction::PushFolder(component)=>folder.push(component), - WriteStackInstruction::PopFolder=>assert!(folder.pop(),"weirdness"), - WriteStackInstruction::Destroy(referent)=>destroy_queue.push(referent), - WriteStackInstruction::Node(node,name_count)=>{ - //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 - Class::LocalScript=>properties.class=Some("LocalScript".to_owned()), - Class::Script=>properties.class=Some("Script".to_owned()), - Class::Model=>(), - } - let name_override=if 0name_override.clone(), - DecompileStyle::Rojo=>"init".to_owned(), - }; - - //write item in subfolder - write_queue.push((subfolder,node,name_final,properties,config.style)); - }else{ - //write item - 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{ - Class::Folder=>(), - _=>stack.push(WriteStackInstruction::Destroy(node.referent)), - } - if has_children{ - stack.push(WriteStackInstruction::PopFolder); - name_tally.clear(); - for referent in &node.children{ - if let Some(c)=context.tree_refs.get(referent){ - let v=name_tally.entry(c.name.clone()).and_modify(|v|*v+=1).or_default(); - stack.push(WriteStackInstruction::Node(c,*v)); - } - } - stack.push(WriteStackInstruction::PushFolder(sanitize(name_override.as_str()).to_string())); - } - }, - } - } - - //run the async - { - let dom=&context.dom; - let write_models=config.write_models; - let write_scripts=config.write_scripts; - let results:Vec>=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?; - } - } - - //run the destroy - for destroy_ref in destroy_queue{ - context.dom.destroy(destroy_ref); - } - - //write what remains in template.rbxlx - if config.write_template{ - let mut file=config.output_folder.clone(); - file.push("template"); - assert!(file.set_extension("rbxlx")); - let output=std::io::BufWriter::new(std::fs::File::create(file)?); - rbx_xml::to_writer_default(output,&context.dom,context.dom.root().children())?; - } - - Ok(()) -} struct DecompileConfig{ style:DecompileStyle, @@ -1090,369 +757,6 @@ async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHist Ok(()) } -//holy smokes what am I doing lmao -//This giant machine is supposed to search for files according to style rules -//e.g. ScriptName.server.lua or init.lua -//Obviously I got carried away -//I could use an enum! -//I could use a struct! -//I could use a trait! -//I could use an error! -//I could use a match! -//I could use a function! -//eventually: -#[derive(Debug)] -#[allow(dead_code)]//idk why this thinks it's dead code, the errors are printed out in various places -enum QueryResolveError{ - NotFound,//0 results - Ambiguous,//>1 results - JoinError(tokio::task::JoinError), - IO(std::io::Error), -} -impl std::fmt::Display for QueryResolveError{ - fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ - write!(f,"{self:?}") - } -} -impl std::error::Error for QueryResolveError{} - -struct FileWithName{ - file:tokio::fs::File, - name:String, -} - -async fn get_file_async(mut path:PathBuf,file_name:impl AsRef)->Result{ - let name=file_name.as_ref().to_str().unwrap().to_owned(); - path.push(file_name); - match tokio::fs::File::open(path).await{ - Ok(file)=>Ok(FileWithName{file,name}), - Err(e)=>match e.kind(){ - std::io::ErrorKind::NotFound=>Err(QueryResolveError::NotFound), - _=>Err(QueryResolveError::IO(e)), - }, - } -} -type QueryHintResult=Result; -trait Query{ - async fn resolve(self)->QueryHintResult; -} -type QueryHandle=tokio::task::JoinHandle>; -struct QuerySingle{ - script:QueryHandle, -} -impl QuerySingle{ - fn rox(search_path:&PathBuf,search_name:&str)->Self{ - Self{ - script:tokio::spawn(get_file_async(search_path.clone(),format!("{}.lua",search_name))) - } - } -} -impl Query for QuerySingle{ - async fn resolve(self)->QueryHintResult{ - match self.script.await{ - Ok(Ok(file))=>Ok(FileHint{file,hint:ScriptHint::ModuleScript}), - Ok(Err(e))=>Err(e), - Err(e)=>Err(QueryResolveError::JoinError(e)), - } - } -} -struct QueryTriple{ - module:QueryHandle, - server:QueryHandle, - client:QueryHandle, -} -impl QueryTriple{ - fn rox_rojo(search_path:&PathBuf,search_name:&str,search_module:bool)->Self{ - //this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion - let module_name=if search_module{ - format!("{}.module.lua",search_name) - }else{ - format!("{}.lua",search_name) - }; - Self{ - module:tokio::spawn(get_file_async(search_path.clone(),module_name)), - server:tokio::spawn(get_file_async(search_path.clone(),format!("{}.server.lua",search_name))), - client:tokio::spawn(get_file_async(search_path.clone(),format!("{}.client.lua",search_name))), - } - } - fn rojo(search_path:&PathBuf)->Self{ - QueryTriple::rox_rojo(search_path,"init",false) - } -} -//these functions can be achieved with macros, but I have not learned that yet -fn mega_triple_join(query_triplet:(QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ - match query_triplet{ - //unambiguously locate file - (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), - //multiple files located - (Ok(_),Ok(_),Err(QueryResolveError::NotFound)) - |(Ok(_),Err(QueryResolveError::NotFound),Ok(_)) - |(Err(QueryResolveError::NotFound),Ok(_),Ok(_)) - |(Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), - //no files located - (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), - //other error - (Err(e),_,_) - |(_,Err(e),_) - |(_,_,Err(e))=>Err(e), - } -} -//LETS GOOOOOOOOOOOOOOOO -fn mega_quadruple_join(query_quad:(QueryHintResult,QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ - match query_quad{ - //unambiguously locate file - (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), - //multiple files located - (Ok(_),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) - |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) - |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) - |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) - |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound)) - |(Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_)) - |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Ok(_)) - |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) - |(Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) - |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) - |(Ok(_),Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), - //no files located - (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), - //other error - (Err(e),_,_,_) - |(_,Err(e),_,_) - |(_,_,Err(e),_) - |(_,_,_,Err(e))=>Err(e), - } -} -impl Query for QueryTriple{ - async fn resolve(self)->QueryHintResult{ - let (module,server,client)=tokio::join!(self.module,self.server,self.client); - mega_triple_join(( - module.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), - server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), - client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), - )) - } -} -struct QueryQuad{ - module_implicit:QueryHandle, - module_explicit:QueryHandle, - server:QueryHandle, - client:QueryHandle, -} -impl QueryQuad{ - fn rox_rojo(search_path:&PathBuf,search_name:&str)->Self{ - let fill=QueryTriple::rox_rojo(search_path,search_name,true); - Self{ - module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua - module_explicit:fill.module,//Script.module.lua - server:fill.server, - client:fill.client, - } - } -} -impl Query for QueryQuad{ - async fn resolve(self)->QueryHintResult{ - let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client); - mega_quadruple_join(( - module_implicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), - module_explicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), - server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), - client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), - )) - } -} - -struct ScriptWithOverrides{ - overrides:PropertiesOverride, - source:String, -} - -fn extract_script_overrides(mut source:String)->AResult{ - let mut overrides=PropertiesOverride::default(); - let mut count=0; - for line in source.lines(){ - //only string type properties are supported atm - if let Some(captures)=lazy_regex::regex!(r#"^\-\-\!\s*Properties\.([A-z]\w*)\s*\=\s*"(\w+)"$"#) - .captures(line){ - count+=line.len(); - match &captures[1]{ - "Name"=>overrides.name=Some(captures[2].to_owned()), - "ClassName"=>overrides.class=Some(captures[2].to_owned()), - other=>Err(anyhow::Error::msg(format!("Unimplemented property {other}")))?, - } - }else{ - break; - } - } - Ok(ScriptWithOverrides{overrides,source:source.split_off(count)}) -} - -async fn script_node(search_name:&str,mut file:FileWithName,hint:ScriptHint)->AResult{ - //read entire file - let mut buf=String::new(); - file.file.read_to_string(&mut buf).await?; - //regex script according to Properties lines at the top - let script_with_overrides=extract_script_overrides(buf)?; - //script - Ok(CompileNode{ - blacklist:Some(file.name), - name:script_with_overrides.overrides.name.unwrap_or_else(||search_name.to_owned()), - class:match (script_with_overrides.overrides.class.as_deref(),hint){ - (Some("ModuleScript"),_) - |(None,ScriptHint::ModuleScript)=>CompileClass::ModuleScript(script_with_overrides.source), - (Some("LocalScript"),_) - |(None,ScriptHint::LocalScript)=>CompileClass::LocalScript(script_with_overrides.source), - (Some("Script"),_) - |(None,ScriptHint::Script)=>CompileClass::Script(script_with_overrides.source), - other=>Err(anyhow::Error::msg(format!("Invalid hint or class {other:?}")))?, - }, - }) -} - -async fn model_node(search_name:&str,mut file:FileWithName)->AResult{ - //read entire file - let mut buf=Vec::new(); - file.file.read_to_end(&mut buf).await?; - //model - Ok(CompileNode{ - blacklist:Some(file.name), - name:search_name.to_owned(),//wrong but gets overwritten by internal model name - class:CompileClass::Model(buf), - }) -} - -async fn locate_override_file(entry:&tokio::fs::DirEntry,style:Option)->AResult{ - let contents_folder=entry.path(); - let file_name=entry.file_name(); - let search_name=file_name.to_str().unwrap(); - //scan inside the folder for an object to define the class of the folder - let script_query=async {match style{ - Some(DecompileStyle::Rox)=>QuerySingle::rox(&contents_folder,search_name).resolve().await, - Some(DecompileStyle::RoxRojo)=>QueryQuad::rox_rojo(&contents_folder,search_name).resolve().await, - Some(DecompileStyle::Rojo)=>QueryTriple::rojo(&contents_folder).resolve().await, - //try all three and complain if there is ambiguity - None=>mega_triple_join(tokio::join!( - QuerySingle::rox(&contents_folder,search_name).resolve(), - //true=search for module here to avoid ambiguity with QuerySingle::rox results - QueryTriple::rox_rojo(&contents_folder,search_name,true).resolve(), - QueryTriple::rojo(&contents_folder).resolve(), - )) - }}; - //model files are rox & rox-rojo only, so it's a lot less work... - let model_query=get_file_async(contents_folder.clone(),format!("{}.rbxmx",search_name)); - //model? script? both? - Ok(match tokio::join!(script_query,model_query){ - (Ok(FileHint{file,hint}),Err(QueryResolveError::NotFound))=>script_node(search_name,file,hint).await?, - (Err(QueryResolveError::NotFound),Ok(file))=>model_node(search_name,file).await?, - (Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous)?, - //neither - (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>CompileNode{ - name:search_name.to_owned(), - blacklist:None, - class:CompileClass::Folder, - }, - //other error - (Err(e),_) - |(_,Err(e))=>Err(e)? - }) -} - - -enum FileDiscernment{ - Model, - Script(ScriptHint), -} - -async fn discern_file(entry:&tokio::fs::DirEntry,style:Option)->AResult{ - let mut file_name=entry - .file_name() - .into_string() - .map_err(|e|anyhow::Error::msg(format!("insane file name {e:?}")))?; - //reject goobers - let is_goober=match style{ - Some(DecompileStyle::Rojo)=>true, - _=>false, - }; - let (ext_len,file_discernment)={ - if let Some(captures)=lazy_regex::regex!(r"^.*(.module.lua|.client.lua|.server.lua)$") - .captures(file_name.as_str()){ - let ext=&captures[1]; - (ext.len(),match ext{ - ".module.lua"=>{ - if is_goober{ - Err(anyhow::Error::msg(format!("File extension {ext} not supported in style {style:?}")))?; - } - FileDiscernment::Script(ScriptHint::ModuleScript) - }, - ".client.lua"=>FileDiscernment::Script(ScriptHint::LocalScript), - ".server.lua"=>FileDiscernment::Script(ScriptHint::Script), - _=>panic!("Regex failed"), - }) - }else if let Some(captures)=lazy_regex::regex!(r"^.*(.rbxmx|.lua)$") - .captures(file_name.as_str()){ - let ext=&captures[1]; - (ext.len(),match ext{ - ".rbxmx"=>{ - if is_goober{ - Err(anyhow::Error::msg(format!("File extension {ext} not supported in style {style:?}")))?; - } - FileDiscernment::Model - }, - ".lua"=>FileDiscernment::Script(ScriptHint::ModuleScript), - _=>panic!("Regex failed"), - }) - }else{ - return Err(anyhow::Error::msg("No file extension")); - } - }; - file_name.truncate(file_name.len()-ext_len); - let file=tokio::fs::File::open(entry.path()).await?; - Ok(match file_discernment{ - FileDiscernment::Model=>model_node(file_name.as_str(),FileWithName{file,name:file_name.clone()}).await?, - FileDiscernment::Script(hint)=>script_node(file_name.as_str(),FileWithName{file,name:file_name.clone()},hint).await?, - }) -} - -#[derive(Debug)] -enum ScriptHint{ - Script, - LocalScript, - ModuleScript, -} -struct FileHint{ - file:FileWithName, - hint:ScriptHint, -} - -enum PreparedData{ - Model(rbx_dom_weak::WeakDom), - Builder(rbx_dom_weak::InstanceBuilder), -} - -enum CompileClass{ - Folder, - Script(String), - LocalScript(String), - ModuleScript(String), - Model(Vec), -} - -struct CompileNode{ - name:String, - blacklist:Option, - class:CompileClass, -} - -enum CompileStackInstruction{ - TraverseReferent(rbx_dom_weak::types::Ref,Option), - PopFolder, -} - struct CompileConfig{ input_folder:PathBuf, output_file:PathBuf, @@ -1460,19 +764,6 @@ struct CompileConfig{ style:Option, } -fn script_builder(class:&str,name:&str,source:String)->rbx_dom_weak::InstanceBuilder{ - let mut builder=rbx_dom_weak::InstanceBuilder::new(class); - builder.set_name(name); - builder.add_property("Source",rbx_dom_weak::types::Variant::String(source)); - builder -} - -enum TooComplicated{ - Stop, - Value(T), - Skip, -} - async fn compile(config:CompileConfig)->AResult<()>{ //basically decompile in reverse order //load template dom @@ -1484,117 +775,7 @@ async fn compile(config:CompileConfig)->AResult<()>{ //hack to traverse root folder as the root object dom.root_mut().name="src".to_owned(); - //add in scripts and models - let mut folder=config.input_folder.clone(); - let mut stack:Vec=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)]; - while let Some(instruction)=stack.pop(){ - match instruction{ - CompileStackInstruction::TraverseReferent(item_ref,blacklist)=>{ - let sans={ - let item=dom.get_by_ref(item_ref).ok_or(anyhow::Error::msg("null child ref"))?; - sanitize(item.name.as_str()).to_string() - }; - folder.push(sans.as_str()); - stack.push(CompileStackInstruction::PopFolder); - //check if a folder exists with item.name - if let Ok(dir)=tokio::fs::read_dir(folder.as_path()).await{ - let mut exist_names:std::collections::HashSet={ - let item=dom.get_by_ref(item_ref).ok_or(anyhow::Error::msg("null child ref"))?; - //push existing dom children objects onto stack (unrelated to exist_names) - stack.extend(item.children().into_iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None))); - //get names of existing objects - item.children().into_iter().map(|&child_ref|{ - let child=dom.get_by_ref(child_ref).ok_or(anyhow::Error::msg("null child ref"))?; - Ok::<_,anyhow::Error>(sanitize(child.name.as_str()).to_string()) - }).collect::>()? - }; - if let Some(dont)=blacklist{ - exist_names.insert(dont); - } - //generate children from folder contents UNLESS! item already has a child of the same name - - let style=config.style; - let exist_names=&exist_names; - futures::stream::unfold(dir,|mut dir1|async{ - //thread the needle! follow the path that dir takes! - let ret1={ - //capture a scoped mutable reference so we can forward dir to the next call even on an error - let dir2=&mut dir1; - (||async move{//error catcher so I can use ? - let ret2=if let Some(entry)=dir2.next_entry().await?{ - //cull early even if supporting things with identical names is possible - if exist_names.contains(entry.file_name().to_str().unwrap()){ - TooComplicated::Skip - }else{ - TooComplicated::Value(entry) - } - }else{ - TooComplicated::Stop - }; - Ok::<_,anyhow::Error>(ret2) - })().await - }; - match ret1{ - Ok(TooComplicated::Stop)=>None, - Ok(TooComplicated::Skip)=>Some((Ok(None),dir1)), - Ok(TooComplicated::Value(v))=>Some((Ok(Some(v)),dir1)), - Err(e)=>Some((Err(e),dir1)), - } - }) - - //gotta spawn off the worker threads (Model is slow) - .then(|bog|async{ - match bog{ - Ok(Some(entry))=>tokio::spawn(async move{ - let met=entry.metadata().await?; - //discern that bad boy - let compile_class=match met.is_dir(){ - true=>locate_override_file(&entry,style).await?, - false=>discern_file(&entry,style).await?, - }; - //prepare data structure - Ok(Some((compile_class.blacklist,match compile_class.class{ - CompileClass::Folder=>PreparedData::Builder(rbx_dom_weak::InstanceBuilder::new("Folder").with_name(compile_class.name.as_str())), - CompileClass::Script(source)=>PreparedData::Builder(script_builder("Script",compile_class.name.as_str(),source)), - CompileClass::LocalScript(source)=>PreparedData::Builder(script_builder("LocalScript",compile_class.name.as_str(),source)), - CompileClass::ModuleScript(source)=>PreparedData::Builder(script_builder("ModuleScript",compile_class.name.as_str(),source)), - CompileClass::Model(buf)=>PreparedData::Model(rbx_xml::from_reader_default(std::io::Cursor::new(buf))?), - }))) - }).await?, - Ok(None)=>Ok(None), - Err(e)=>Err(e), - } - }) - - //is this even what I want? - .map(|f|async{f}).buffer_unordered(32) - - //begin processing immediately - .fold((&mut stack,&mut dom),|(stack,dom),bog:Result<_,anyhow::Error>|async{ - //push child objects onto dom serially as they arrive - match bog{ - Ok(Some((blacklist,data)))=>{ - let referent=match data{ - PreparedData::Model(mut model_dom)=>{ - let referent=model_dom.root().children()[0]; - model_dom.transfer(referent,dom,item_ref); - referent - }, - PreparedData::Builder(script)=>dom.insert(item_ref,script), - }; - //new children need to be traversed - stack.push(CompileStackInstruction::TraverseReferent(referent,blacklist)); - }, - Ok(None)=>(), - Err(e)=>println!("error lole {e:?}"), - } - (stack,dom) - }).await; - } - }, - CompileStackInstruction::PopFolder=>assert!(folder.pop(),"pop folder bad"), - } - } + something_something_dom_write(&mut dom).await?; let mut output_place=config.output_file.clone(); if output_place.extension().is_none()&&tokio::fs::try_exists(output_place.as_path()).await?{