diff --git a/Cargo.lock b/Cargo.lock index fa80476..44ce2c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,12 +118,12 @@ dependencies = [ "git2", "lazy-regex", "pollster", - "rayon", "rbx_asset", "rbx_binary", "rbx_dom_weak", "rbx_reflection_database", "rbx_xml", + "rox_compiler", "serde_json", "tokio", ] @@ -1351,6 +1351,18 @@ dependencies = [ "serde", ] +[[package]] +name = "rox_compiler" +version = "0.1.0" +dependencies = [ + "futures", + "lazy-regex", + "rayon", + "rbx_dom_weak", + "rbx_xml", + "tokio", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index 591195c..2849d7f 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" @@ -13,12 +13,12 @@ futures = "0.3.30" git2 = "0.18.1" lazy-regex = "3.1.0" pollster = "0.3.0" -rayon = "1.8.0" rbx_asset = { path = "rbx_asset" } 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..4b57ee8 --- /dev/null +++ b/rox_compiler/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rox_compiler" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3.30" +lazy-regex = "3.1.0" +rayon = "1.8.0" +rbx_dom_weak = "2.7.0" +rbx_xml = "0.13.3" +tokio = { version = "1.35.1", features = ["fs"] } diff --git a/rox_compiler/src/common.rs b/rox_compiler/src/common.rs new file mode 100644 index 0000000..970aec9 --- /dev/null +++ b/rox_compiler/src/common.rs @@ -0,0 +1,33 @@ +#[derive(Clone,Copy,Debug)] +pub enum Style{ + Rox, + Rojo, + RoxRojo, +} + +#[derive(Default)] +pub(crate) struct PropertiesOverride{ + pub name:Option, + pub class:Option, +} +impl PropertiesOverride{ + pub 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(()) + } +} + +pub(crate) fn sanitize<'a>(s:&'a str)->std::borrow::Cow<'a,str>{ + lazy_regex::regex!(r"[^A-z0-9.-]").replace_all(s,"_") +} diff --git a/rox_compiler/src/compile.rs b/rox_compiler/src/compile.rs new file mode 100644 index 0000000..81c5b95 --- /dev/null +++ b/rox_compiler/src/compile.rs @@ -0,0 +1,563 @@ +use std::path::PathBuf; +use futures::{StreamExt, TryStreamExt}; +use tokio::io::AsyncReadExt; + +use crate::common::{sanitize,Style,PropertiesOverride}; + +//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 +pub 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, +} + +#[derive(Debug)] +pub enum ScriptWithOverridesError{ + UnimplementedProperty(String), +} +impl std::fmt::Display for ScriptWithOverridesError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ScriptWithOverridesError{} + +impl ScriptWithOverrides{ + fn from_source(mut source:String)->Result{ + 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(ScriptWithOverridesError::UnimplementedProperty(other.to_owned()))?, + } + }else{ + break; + } + } + Ok(ScriptWithOverrides{overrides,source:source.split_off(count)}) + } +} + +enum CompileClass{ + Folder, + Script(String), + LocalScript(String), + ModuleScript(String), + Model(Vec), +} + +struct CompileNode{ + name:String, + blacklist:Option, + class:CompileClass, +} + +#[derive(Debug)] +pub enum CompileNodeError{ + IO(std::io::Error), + ScriptWithOverrides(ScriptWithOverridesError), + InvalidClassOrHint{ + class:Option, + hint:ScriptHint + }, + QueryResolveError(QueryResolveError), + /// Conversion from OsString to String failed + FileName(std::ffi::OsString), + ExtensionNotSupportedInStyle{ + extension:String, + style:Option